forked from FoundKeyGang/FoundKey
Merge branch 'main' into mk.absturztau.be
This commit is contained in:
commit
5d6cceda49
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" DROP COLUMN "domain"`);
|
||||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE text USING "key"::text`);
|
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE text USING "key"::text`);
|
||||||
// delete existing duplicated entries, keeping the latest updated one
|
// 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")`);
|
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) {
|
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 ext = mime.extension(emoji.type);
|
||||||
const fileName = emoji.name + (ext ? '.' + ext : '');
|
const fileName = emoji.name + (ext ? '.' + ext : '');
|
||||||
const emojiPath = path + '/' + fileName;
|
const emojiPath = path + '/' + fileName;
|
||||||
|
|
|
@ -50,6 +50,10 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
||||||
|
|
||||||
for (const record of meta.emojis) {
|
for (const record of meta.emojis) {
|
||||||
if (!record.downloaded) continue;
|
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 emojiInfo = record.emoji;
|
||||||
const emojiPath = outputPath + '/' + record.fileName;
|
const emojiPath = outputPath + '/' + record.fileName;
|
||||||
await Emojis.delete({
|
await Emojis.delete({
|
||||||
|
|
|
@ -34,21 +34,38 @@ export function getApIds(value: ApObject | undefined): string[] {
|
||||||
return array.map(x => getApId(x));
|
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
|
* Get ActivityStreams Object id
|
||||||
*/
|
*/
|
||||||
export function getApId(value: string | Object): string {
|
export function getApId(value: string | Object): string {
|
||||||
if (typeof value === 'string') return value;
|
let url = null;
|
||||||
if (typeof value.id === 'string') return value.id;
|
if (typeof value === 'string') url = value;
|
||||||
throw new Error('cannot detemine id');
|
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');
|
throw new Error('cannot detect type');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
|
||||||
const firstOne = Array.isArray(value) ? value[0] : value;
|
let url = null;
|
||||||
return getApHrefNullable(firstOne);
|
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 {
|
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||||
if (typeof value === 'string') return value;
|
if (!value) {
|
||||||
if (typeof value?.href === 'string') return value.href;
|
return;
|
||||||
return undefined;
|
} 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 {
|
export interface IActivity extends IObject {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { MINUTE, HOUR } from '@/const.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { Note } from '@/models/entities/note.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 { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
|
@ -122,7 +121,7 @@ export default define(meta, paramDef, async () => {
|
||||||
for (let i = 0; i < range; i++) {
|
for (let i = 0; i < range; i++) {
|
||||||
countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
|
countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
|
||||||
.select('count(distinct note.userId)')
|
.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 < :lt', { lt: new Date(now.getTime() - (interval * i)) })
|
||||||
.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) })
|
.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) })
|
||||||
.cache(60000) // 1 min
|
.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')
|
const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
|
||||||
.select('count(distinct note.userId)')
|
.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) })
|
.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) })
|
||||||
.cache(60000 * 60) // 60 min
|
.cache(60000 * 60) // 60 min
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { safeForSql } from '@/misc/safe-for-sql.js';
|
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
|
@ -86,15 +85,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (ps.tag) {
|
if (ps.tag) {
|
||||||
if (!safeForSql(ps.tag)) throw new Error('Injection');
|
query.andWhere(':tag = ANY(note.tags)', { tag: normalizeForSearch(ps.tag) });
|
||||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
|
||||||
} else {
|
} else {
|
||||||
|
let i = 0;
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
for (const tags of ps.query!) {
|
for (const tags of ps.query!) {
|
||||||
qb.orWhere(new Brackets(qb => {
|
qb.orWhere(new Brackets(qb => {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if (!safeForSql(tag)) throw new Error('Injection');
|
qb.andWhere(`:tag${++i} = ANY(note.tags)`, { ['tag' + i]: normalizeForSearch(tag) });
|
||||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,14 @@ export const urlPreviewHandler = async (ctx: Koa.Context): Promise<void> => {
|
||||||
|
|
||||||
logger.succ(`Got preview of ${url}: ${summary.title}`);
|
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.icon = wrap(summary.icon);
|
||||||
summary.thumbnail = wrap(summary.thumbnail);
|
summary.thumbnail = wrap(summary.thumbnail);
|
||||||
|
|
||||||
|
@ -54,12 +62,10 @@ export const urlPreviewHandler = async (ctx: Koa.Context): Promise<void> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function wrap(url?: string): string | null {
|
function wrap(url?: string): string | null {
|
||||||
return url != null
|
if (url == null) return null;
|
||||||
? url.match(/^https?:\/\//)
|
if (!['http:', 'https:'].includes(new URL(url).protocol)) return null;
|
||||||
? `${config.url}/proxy/preview.webp?${query({
|
return config.url + '/proxy/preview.webp?' + query({
|
||||||
url,
|
url,
|
||||||
preview: '1',
|
preview: '1',
|
||||||
})}`
|
});
|
||||||
: url
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const self = props.url.startsWith(local);
|
const self = props.url.startsWith(local);
|
||||||
const uri = new URL(props.url);
|
const uri = new URL(props.url);
|
||||||
|
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
|
||||||
let el: HTMLElement | null = $ref(null);
|
let el: HTMLElement | null = $ref(null);
|
||||||
|
|
||||||
let schema = $ref(uri.protocol);
|
let schema = $ref(uri.protocol);
|
||||||
|
|
|
@ -54,6 +54,7 @@ let player = $ref({
|
||||||
let playerEnabled = $ref(false);
|
let playerEnabled = $ref(false);
|
||||||
|
|
||||||
const requestUrl = new URL(props.url);
|
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)')) {
|
if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
|
||||||
requestUrl.hostname = 'www.youtube.com';
|
requestUrl.hostname = 'www.youtube.com';
|
||||||
|
@ -72,7 +73,9 @@ fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).the
|
||||||
icon = info.icon;
|
icon = info.icon;
|
||||||
sitename = info.sitename;
|
sitename = info.sitename;
|
||||||
fetching = false;
|
fetching = false;
|
||||||
player = info.player;
|
if (['http:', 'https:'].includes(new URL(info.player.url).protocol)) {
|
||||||
|
player = info.player;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -59,6 +59,7 @@ async function run() {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: q,
|
title: q,
|
||||||
}).then(({ canceled, result: a }) => {
|
}).then(({ canceled, result: a }) => {
|
||||||
|
if (canceled) return;
|
||||||
ok(a);
|
ok(a);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,8 @@ export function install(plugin) {
|
||||||
return new Promise(ok => {
|
return new Promise(ok => {
|
||||||
inputText({
|
inputText({
|
||||||
title: q,
|
title: q,
|
||||||
}).then(({ result: a }) => {
|
}).then(({ canceled, result: a }) => {
|
||||||
|
if (canceled) return;
|
||||||
ok(a);
|
ok(a);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,11 @@ export function createAiScriptEnv(opts) {
|
||||||
return confirm.canceled ? values.FALSE : values.TRUE;
|
return confirm.canceled ? values.FALSE : values.TRUE;
|
||||||
}),
|
}),
|
||||||
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
|
'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++;
|
apiRequests++;
|
||||||
if (apiRequests > 16) return values.NULL;
|
if (apiRequests > 16) return values.NULL;
|
||||||
const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || 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 => {
|
return new Promise(ok => {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: q,
|
title: q,
|
||||||
}).then(({ result: a }) => {
|
}).then(({ canceled, result: a }) => {
|
||||||
|
if (canceled) return;
|
||||||
ok(a);
|
ok(a);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,7 +60,8 @@ const run = async (): Promise<void> => {
|
||||||
return new Promise(ok => {
|
return new Promise(ok => {
|
||||||
os.inputText({
|
os.inputText({
|
||||||
title: q,
|
title: q,
|
||||||
}).then(({ result: a }) => {
|
}).then(({ canceled, result: a }) => {
|
||||||
|
if (canceled) return;
|
||||||
ok(a);
|
ok(a);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue