forked from FoundKeyGang/FoundKey
Compare commits
8 commits
e4890de172
...
5d6cceda49
Author | SHA1 | Date | |
---|---|---|---|
5d6cceda49 | |||
6a40ef3569 | |||
|
09fe55379e | ||
27b912b9b0 | |||
48fd543d0f | |||
|
af272ce358 | ||
c1ae134c0a | |||
3ad6323c23 |
15 changed files with 99 additions and 43 deletions
|
@ -6,7 +6,7 @@ export class registryRemoveDomain1675375940759 {
|
|||
await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "domain"`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE text USING "key"::text`);
|
||||
// delete existing duplicated entries, keeping the latest updated one
|
||||
await queryRunner.query(`DELETE FROM "registry_item" AS "a" WHERE "updatedAt" != (SELECT MAX("updatedAt") OVER (PARTITION BY "userId", "key", "scope") FROM "registry_item" AS "b" WHERE "a"."userId" = "b"."userId" AND "a"."key" = "b"."key" AND "a"."scope" = "b"."scope")`);
|
||||
await queryRunner.query(`DELETE FROM "registry_item" AS "a" WHERE "updatedAt" != (SELECT MAX("updatedAt") FROM "registry_item" AS "b" WHERE "a"."userId" = "b"."userId" AND "a"."key" = "b"."key" AND "a"."scope" = "b"."scope" GROUP BY "userId", "key", "scope")`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "UQ_b8d6509f847331273ab99daccc7" UNIQUE ("userId", "key", "scope")`);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export function safeForSql(text: string): boolean {
|
||||
return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text);
|
||||
}
|
|
@ -58,6 +58,10 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
|||
});
|
||||
|
||||
for (const emoji of customEmojis) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
|
||||
this.logger.error(`invalid emoji name: ${emoji.name}, skipping in emoji export`);
|
||||
continue;
|
||||
}
|
||||
const ext = mime.extension(emoji.type);
|
||||
const fileName = emoji.name + (ext ? '.' + ext : '');
|
||||
const emojiPath = path + '/' + fileName;
|
||||
|
|
|
@ -50,6 +50,10 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
|
|
|
@ -34,21 +34,38 @@ export function getApIds(value: ApObject | undefined): string[] {
|
|||
return array.map(x => getApId(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first ActivityStreams Object id
|
||||
*/
|
||||
export function getOneApId(value: ApObject): string {
|
||||
const firstOne = Array.isArray(value) ? value[0] : value;
|
||||
return getApId(firstOne);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ActivityStreams Object id
|
||||
*/
|
||||
export function getApId(value: string | Object): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value.id === 'string') return value.id;
|
||||
throw new Error('cannot detemine id');
|
||||
let url = null;
|
||||
if (typeof value === 'string') url = value;
|
||||
else if (typeof value.id === 'string') url = value.id;
|
||||
|
||||
if (!url || !['https:', 'http:'].includes(new URL(url).protocol)) {
|
||||
throw new Error('cannot determine id');
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first (valid) ActivityStreams Object id
|
||||
*/
|
||||
export function getOneApId(value: ApObject): string {
|
||||
if (Array.isArray(value)) {
|
||||
// find the first valid ID
|
||||
for (const id of value) {
|
||||
try {
|
||||
return getApId(x);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error('cannot determine id');
|
||||
} else {
|
||||
return getApId(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,15 +77,34 @@ export function getApType(value: Object): string {
|
|||
throw new Error('cannot detect type');
|
||||
}
|
||||
|
||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||
const firstOne = Array.isArray(value) ? value[0] : value;
|
||||
return getApHrefNullable(firstOne);
|
||||
export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
|
||||
let url = null;
|
||||
if (typeof value === 'string') url = value;
|
||||
else if (typeof value?.href === 'string') url = value.href;
|
||||
|
||||
if (!url || !['https:', 'http:'].includes(new URL(url).protocol)) {
|
||||
return undefined;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value?.href === 'string') return value.href;
|
||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return;
|
||||
} else if (Array.isArray(value)) {
|
||||
// find the first valid href
|
||||
for (const href of value) {
|
||||
try {
|
||||
return getApHrefNullable(href);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
} else {
|
||||
return getApHrefNullable(value);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActivity extends IObject {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { MINUTE, HOUR } from '@/const.js';
|
|||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { safeForSql } from '@/misc/safe-for-sql.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
|
@ -122,7 +121,7 @@ export default define(meta, paramDef, async () => {
|
|||
for (let i = 0; i < range; i++) {
|
||||
countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
|
||||
.select('count(distinct note.userId)')
|
||||
.where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`)
|
||||
.where(':tag = ANY(note.tags)', { tag })
|
||||
.andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) })
|
||||
.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) })
|
||||
.cache(60000) // 1 min
|
||||
|
@ -136,7 +135,7 @@ export default define(meta, paramDef, async () => {
|
|||
|
||||
const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
|
||||
.select('count(distinct note.userId)')
|
||||
.where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`)
|
||||
.where(':tag = ANY(note.tags)', { tag })
|
||||
.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) })
|
||||
.cache(60000 * 60) // 60 min
|
||||
.getRawOne()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Brackets } from 'typeorm';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { safeForSql } from '@/misc/safe-for-sql.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import define from '../../define.js';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||
|
@ -86,15 +85,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(ps.tag)) throw new Error('Injection');
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
query.andWhere(':tag = ANY(note.tags)', { tag: normalizeForSearch(ps.tag) });
|
||||
} else {
|
||||
let i = 0;
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(tag)) throw new Error('Injection');
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
qb.andWhere(`:tag${++i} = ANY(note.tags)`, { ['tag' + i]: normalizeForSearch(tag) });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -38,6 +38,14 @@ export const urlPreviewHandler = async (ctx: Koa.Context): Promise<void> => {
|
|||
|
||||
logger.succ(`Got preview of ${url}: ${summary.title}`);
|
||||
|
||||
if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) {
|
||||
throw new Error('unsupported schema included');
|
||||
}
|
||||
|
||||
if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) {
|
||||
throw new Error('unsupported schema included');
|
||||
}
|
||||
|
||||
summary.icon = wrap(summary.icon);
|
||||
summary.thumbnail = wrap(summary.thumbnail);
|
||||
|
||||
|
@ -54,12 +62,10 @@ export const urlPreviewHandler = async (ctx: Koa.Context): Promise<void> => {
|
|||
};
|
||||
|
||||
function wrap(url?: string): string | null {
|
||||
return url != null
|
||||
? url.match(/^https?:\/\//)
|
||||
? `${config.url}/proxy/preview.webp?${query({
|
||||
if (url == null) return null;
|
||||
if (!['http:', 'https:'].includes(new URL(url).protocol)) return null;
|
||||
return config.url + '/proxy/preview.webp?' + query({
|
||||
url,
|
||||
preview: '1',
|
||||
})}`
|
||||
: url
|
||||
: null;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const self = props.url.startsWith(local);
|
||||
const uri = new URL(props.url);
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
let el: HTMLElement | null = $ref(null);
|
||||
|
||||
let schema = $ref(uri.protocol);
|
||||
|
|
|
@ -54,6 +54,7 @@ let player = $ref({
|
|||
let playerEnabled = $ref(false);
|
||||
|
||||
const requestUrl = new URL(props.url);
|
||||
if(!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
|
||||
|
||||
if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
|
||||
requestUrl.hostname = 'www.youtube.com';
|
||||
|
@ -72,7 +73,9 @@ fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).the
|
|||
icon = info.icon;
|
||||
sitename = info.sitename;
|
||||
fetching = false;
|
||||
if (['http:', 'https:'].includes(new URL(info.player.url).protocol)) {
|
||||
player = info.player;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -59,6 +59,7 @@ async function run() {
|
|||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
if (canceled) return;
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,8 @@ export function install(plugin) {
|
|||
return new Promise(ok => {
|
||||
inputText({
|
||||
title: q,
|
||||
}).then(({ result: a }) => {
|
||||
}).then(({ canceled, result: a }) => {
|
||||
if (canceled) return;
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,11 @@ export function createAiScriptEnv(opts) {
|
|||
return confirm.canceled ? values.FALSE : values.TRUE;
|
||||
}),
|
||||
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
|
||||
if (token) utils.assertString(token);
|
||||
if (token) {
|
||||
utils.assertString(token);
|
||||
// In case there is a bug, it could be undefined.
|
||||
if (typeof token.value !== 'string') throw new Error('invalid token');
|
||||
}
|
||||
apiRequests++;
|
||||
if (apiRequests > 16) return values.NULL;
|
||||
const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
|
||||
|
|
|
@ -72,7 +72,8 @@ const run = async (): Promise<void> => {
|
|||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ result: a }) => {
|
||||
}).then(({ canceled, result: a }) => {
|
||||
if (canceled) return;
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,7 +60,8 @@ const run = async (): Promise<void> => {
|
|||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ result: a }) => {
|
||||
}).then(({ canceled, result: a }) => {
|
||||
if (canceled) return;
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue