diff options
Diffstat (limited to 'packages/backend/src/misc')
20 files changed, 233 insertions, 60 deletions
diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts index 367a8eb560..27c67cb5df 100644 --- a/packages/backend/src/misc/FileWriterStream.ts +++ b/packages/backend/src/misc/FileWriterStream.ts @@ -4,6 +4,7 @@ */ import * as fs from 'node:fs/promises'; +import { WritableStream } from 'node:stream/web'; import type { PathLike } from 'node:fs'; /** diff --git a/packages/backend/src/misc/append-content-warning.ts b/packages/backend/src/misc/append-content-warning.ts index 152cd6760e..9f61776b1d 100644 --- a/packages/backend/src/misc/append-content-warning.ts +++ b/packages/backend/src/misc/append-content-warning.ts @@ -14,10 +14,13 @@ * @param additional Content warning to append * @param reverse If true, then the additional CW will be prepended instead of appended. */ -export function appendContentWarning(original: string | null | undefined, additional: string, reverse = false): string { +export function appendContentWarning(original: string | null | undefined, additional: string, reverse?: boolean): string; +export function appendContentWarning(original: string, additional: string | null | undefined, reverse?: boolean): string; +export function appendContentWarning(original: string | null | undefined, additional: string | null | undefined, reverse?: boolean): string | null; +export function appendContentWarning(original: string | null | undefined, additional: string | null | undefined, reverse = false): string | null { // Easy case - if original is empty, then additional replaces it. if (!original) { - return additional; + return additional ?? null; } // Easy case - if the additional CW is empty, then don't append it. diff --git a/packages/backend/src/misc/bigint.ts b/packages/backend/src/misc/bigint.ts new file mode 100644 index 0000000000..efa1527ec9 --- /dev/null +++ b/packages/backend/src/misc/bigint.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +function parseBigIntChunked(str: string, base: number, chunkSize: number, powerOfChunkSize: bigint): bigint { + const chunks = []; + while (str.length > 0) { + chunks.unshift(str.slice(-chunkSize)); + str = str.slice(0, -chunkSize); + } + let result = 0n; + for (const chunk of chunks) { + result *= powerOfChunkSize; + const int = parseInt(chunk, base); + if (Number.isNaN(int)) { + throw new Error('Invalid base36 string'); + } + result += BigInt(int); + } + return result; +} + +export function parseBigInt36(str: string): bigint { + // log_36(Number.MAX_SAFE_INTEGER) => 10.251599391715352 + // so we process 10 chars at once + return parseBigIntChunked(str, 36, 10, 36n ** 10n); +} + +export function parseBigInt16(str: string): bigint { + // log_16(Number.MAX_SAFE_INTEGER) => 13.25 + // so we process 13 chars at once + return parseBigIntChunked(str, 16, 13, 16n ** 13n); +} + +export function parseBigInt32(str: string): bigint { + // log_32(Number.MAX_SAFE_INTEGER) => 10.6 + // so we process 10 chars at once + return parseBigIntChunked(str, 32, 10, 32n ** 10n); +} diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index f9692ce5d5..48b8f43678 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -19,16 +19,16 @@ export class RedisKVCache<T> { opts: { lifetime: RedisKVCache<T>['lifetime']; memoryCacheLifetime: number; - fetcher: RedisKVCache<T>['fetcher']; - toRedisConverter: RedisKVCache<T>['toRedisConverter']; - fromRedisConverter: RedisKVCache<T>['fromRedisConverter']; + fetcher?: RedisKVCache<T>['fetcher']; + toRedisConverter?: RedisKVCache<T>['toRedisConverter']; + fromRedisConverter?: RedisKVCache<T>['fromRedisConverter']; }, ) { this.lifetime = opts.lifetime; this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime); - this.fetcher = opts.fetcher; - this.toRedisConverter = opts.toRedisConverter; - this.fromRedisConverter = opts.fromRedisConverter; + this.fetcher = opts.fetcher ?? (() => { throw new Error('fetch not supported - use get/set directly'); }); + this.toRedisConverter = opts.toRedisConverter ?? ((value) => JSON.stringify(value)); + this.fromRedisConverter = opts.fromRedisConverter ?? ((value) => JSON.parse(value)); } @bindThis diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 6cc896046f..9ba95cff42 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { pipeline } from 'node:stream/promises'; +import fs from 'node:fs'; import * as tmp from 'tmp'; export function createTemp(): Promise<[string, () => void]> { @@ -27,3 +29,21 @@ export function createTempDir(): Promise<[string, () => void]> { ); }); } + +export async function saveToTempFile(stream: NodeJS.ReadableStream & { truncated?: boolean }): Promise<[string, () => void]> { + const [filepath, cleanup] = await createTemp(); + + try { + await pipeline(stream, fs.createWriteStream(filepath)); + } catch (e) { + cleanup(); + throw e; + } + + if (stream.truncated) { + cleanup(); + throw new Error('Read failed: input stream truncated'); + } + + return [filepath, cleanup]; +} diff --git a/packages/backend/src/misc/from-tuple.ts b/packages/backend/src/misc/from-tuple.ts index 366b1e310f..034bae584b 100644 --- a/packages/backend/src/misc/from-tuple.ts +++ b/packages/backend/src/misc/from-tuple.ts @@ -1,4 +1,11 @@ -export function fromTuple<T>(value: T | [T]): T { +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function fromTuple<T>(value: T | [T]): T; +export function fromTuple<T>(value: T | [T] | T[]): T | undefined; +export function fromTuple<T>(value: T | [T] | T[]): T | undefined { if (Array.isArray(value)) { return value[0]; } diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts index f3c08cc76e..ac7db82f2e 100644 --- a/packages/backend/src/misc/gen-identicon.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -44,7 +44,7 @@ const sideN = Math.floor(n / 2); /** * Generate buffer of an identicon by seed */ -export async function genIdenticon(seed: string): Promise<Buffer> { +export function genIdenticon(seed: string): Buffer { const rand = gen.create(seed); const canvas = createCanvas(size, size); const ctx = canvas.getContext('2d'); @@ -100,5 +100,5 @@ export async function genIdenticon(seed: string): Promise<Buffer> { } } - return await canvas.toBuffer('image/png'); + return canvas.toBuffer('image/png'); } diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index 60ba788e44..c0e8478db5 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -7,6 +7,7 @@ // 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列] import * as crypto from 'node:crypto'; +import { parseBigInt36 } from '@/misc/bigint.js'; export const aidRegExp = /^[0-9a-z]{10}$/; @@ -35,6 +36,12 @@ export function parseAid(id: string): { date: Date; } { return { date: new Date(time) }; } +export function parseAidFull(id: string): { date: number; additional: bigint; } { + const date = parseInt(id.slice(0, 8), 36) + TIME2000; + const additional = parseBigInt36(id.slice(8, 10)); + return { date, additional }; +} + export function isSafeAidT(t: number): boolean { return t > TIME2000; } diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts index 1b087e70af..006673a6d0 100644 --- a/packages/backend/src/misc/id/aidx.ts +++ b/packages/backend/src/misc/id/aidx.ts @@ -9,6 +9,7 @@ // https://misskey.m544.net/notes/71899acdcc9859ec5708ac24 import { customAlphabet } from 'nanoid'; +import { parseBigInt36 } from '@/misc/bigint.js'; export const aidxRegExp = /^[0-9a-z]{16}$/; @@ -16,6 +17,7 @@ const TIME2000 = 946684800000; const TIME_LENGTH = 8; const NODE_LENGTH = 4; const NOISE_LENGTH = 4; +const AIDX_LENGTH = TIME_LENGTH + NODE_LENGTH + NOISE_LENGTH; const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)(); let counter = 0; @@ -42,6 +44,12 @@ export function parseAidx(id: string): { date: Date; } { return { date: new Date(time) }; } +export function parseAidxFull(id: string): { date: number; additional: bigint; } { + const date = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000; + const additional = parseBigInt36(id.slice(TIME_LENGTH, AIDX_LENGTH)); + return { date, additional }; +} + export function isSafeAidxT(t: number): boolean { return t > TIME2000; } diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index dfab48a369..563e07ed8f 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { parseBigInt16 } from '@/misc/bigint.js'; + const CHARS = '0123456789abcdef'; // same as object-id @@ -39,6 +41,13 @@ export function parseMeid(id: string): { date: Date; } { }; } +export function parseMeidFull(id: string): { date: number; additional: bigint; } { + return { + date: parseInt(id.slice(0, 12), 16) - 0x800000000000, + additional: parseBigInt16(id.slice(12, 24)), + }; +} + export function isSafeMeidT(t: number): boolean { return t > 0; } diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index b9c0cc3dda..b825807114 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { parseBigInt16 } from '@/misc/bigint.js'; + const CHARS = '0123456789abcdef'; // 4bit Fixed hex value 'g' @@ -39,6 +41,13 @@ export function parseMeidg(id: string): { date: Date; } { }; } +export function parseMeidgFull(id: string): { date: number; additional: bigint; } { + return { + date: parseInt(id.slice(1, 12), 16), + additional: parseBigInt16(id.slice(12, 24)), + }; +} + export function isSafeMeidgT(t: number): boolean { return t > 0; } diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index 243f92bbac..68409c7a61 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { parseBigInt16 } from '@/misc/bigint.js'; + const CHARS = '0123456789abcdef'; // same as meid @@ -39,6 +41,13 @@ export function parseObjectId(id: string): { date: Date; } { }; } +export function parseObjectIdFull(id: string): { date: number; additional: bigint; } { + return { + date: parseInt(id.slice(0, 8), 16) * 1000, + additional: parseBigInt16(id.slice(8, 24)), + }; +} + export function isSafeObjectIdT(t: number): boolean { return t > 0; } diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index fc3654d6d2..8b81702d19 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -5,15 +5,27 @@ // Crockford's Base32 // https://github.com/ulid/spec#encoding +import { parseBigInt32 } from '@/misc/bigint.js'; + const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; -export function parseUlid(id: string): { date: Date; } { - const timestamp = id.slice(0, 10); +function parseBase32(timestamp: string) { let time = 0; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < timestamp.length; i++) { time = time * 32 + CHARS.indexOf(timestamp[i]); } - return { date: new Date(time) }; + return time; +} + +export function parseUlid(id: string): { date: Date; } { + return { date: new Date(parseBase32(id.slice(0, 10))) }; +} + +export function parseUlidFull(id: string): { date: number; additional: bigint; } { + return { + date: parseBase32(id.slice(0, 10)), + additional: parseBigInt32(id.slice(10, 26)), + }; } diff --git a/packages/backend/src/misc/is-native-token.ts b/packages/backend/src/misc/is-native-token.ts deleted file mode 100644 index 300c4c05b3..0000000000 --- a/packages/backend/src/misc/is-native-token.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line import/no-default-export -export default (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index d6872de46a..fcaafaf95a 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -77,7 +77,7 @@ type PackedPureRenote = PackedRenote & { replyId: NonNullable<Packed<'Note'>['replyId']>; poll: NonNullable<Packed<'Note'>['poll']>; fileIds: NonNullable<Packed<'Note'>['fileIds']>; -} +}; export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { return note.renoteId != null; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index f612591eda..27aa3d89de 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -63,6 +63,10 @@ import { } from '@/models/json-schema/meta.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; +import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js'; +import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js'; +import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js'; +import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -120,6 +124,13 @@ export const refs = { MetaDetailed: packedMetaDetailedSchema, SystemWebhook: packedSystemWebhookSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, + ChatMessage: packedChatMessageSchema, + ChatMessageLite: packedChatMessageLiteSchema, + ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema, + ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema, + ChatRoom: packedChatRoomSchema, + ChatRoomInvitation: packedChatRoomInvitationSchema, + ChatRoomMembership: packedChatRoomMembershipSchema, }; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; @@ -143,7 +154,7 @@ type OfSchema = { readonly anyOf?: ReadonlyArray<Schema>; readonly oneOf?: ReadonlyArray<Schema>; readonly allOf?: ReadonlyArray<Schema>; -} +}; export interface Schema extends OfSchema { readonly type?: TypeStringef; @@ -166,15 +177,16 @@ export interface Schema extends OfSchema { readonly maximum?: number; readonly minimum?: number; readonly pattern?: string; + readonly additionalProperties?: Schema | boolean; } type RequiredPropertyNames<s extends Obj> = { [K in keyof s]: - // K is not optional - s[K]['optional'] extends false ? K : - // K has default value - s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K : - never + // K is not optional + s[K]['optional'] extends false ? K : + // K has default value + s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K : + never }[keyof s]; export type Obj = Record<string, Schema>; @@ -213,11 +225,18 @@ type ObjectSchemaTypeDef<p extends Schema> = p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ? UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>> : never - : ObjType<p['properties'], NonNullable<p['required']>> - : - p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md - p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : - any + : ObjType<p['properties'], NonNullable<p['required']>> + : + p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md + p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : + p['additionalProperties'] extends true ? Record<string, any> : + p['additionalProperties'] extends Schema ? + p['additionalProperties'] extends infer AdditionalProperties ? + AdditionalProperties extends Schema ? + Record<string, SchemaType<AdditionalProperties>> : + never : + never : + any; type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>; @@ -227,30 +246,30 @@ export type SchemaTypeDef<p extends Schema> = p['type'] extends 'number' ? number : p['type'] extends 'string' ? ( p['enum'] extends readonly (string | null)[] ? - p['enum'][number] : - p['format'] extends 'date-time' ? string : // Dateにする?? - string + p['enum'][number] : + p['format'] extends 'date-time' ? string : // Dateにする?? + string ) : - p['type'] extends 'boolean' ? boolean : - p['type'] extends 'object' ? ObjectSchemaTypeDef<p> : - p['type'] extends 'array' ? ( - p['items'] extends OfSchema ? ( - p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : - p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> : - p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] : - never - ) : - p['prefixItems'] extends ReadonlyArray<Schema> ? ( - p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] : - p['items'] extends false ? ArrayToTuple<p['prefixItems']> : - p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> : - [...ArrayToTuple<p['prefixItems']>, ...unknown[]] + p['type'] extends 'boolean' ? boolean : + p['type'] extends 'object' ? ObjectSchemaTypeDef<p> : + p['type'] extends 'array' ? ( + p['items'] extends OfSchema ? ( + p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : + p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> : + p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] : + never + ) : + p['prefixItems'] extends ReadonlyArray<Schema> ? ( + p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] : + p['items'] extends false ? ArrayToTuple<p['prefixItems']> : + p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> : + [...ArrayToTuple<p['prefixItems']>, ...unknown[]] + ) : + p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] : + any[] ) : - p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] : - any[] - ) : - p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> : - p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> : - any; + p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> : + p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> : + any; export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>; diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts index bd7fe12058..195f7c4d47 100644 --- a/packages/backend/src/misc/json-value.ts +++ b/packages/backend/src/misc/json-value.ts @@ -4,7 +4,7 @@ */ export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; -export type JsonObject = {[K in string]?: JsonValue}; +export type JsonObject = { [K in string]?: JsonValue }; export type JsonArray = JsonValue[]; export function isJsonObject(value: JsonValue | undefined): value is JsonObject { diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts index 8a52ca703e..76b4dd810c 100644 --- a/packages/backend/src/misc/promise-tracker.ts +++ b/packages/backend/src/misc/promise-tracker.ts @@ -5,6 +5,10 @@ const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set(); +export function trackTask(task: () => Promise<unknown>): void { + trackPromise(task()); +} + /** * This tracks promises that other modules decided not to wait for, * and makes sure they are all settled before fully closing down the server. diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/token.ts index 85fb383ba2..5d37cba26d 100644 --- a/packages/backend/src/misc/generate-native-user-token.ts +++ b/packages/backend/src/misc/token.ts @@ -5,5 +5,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js'; -// eslint-disable-next-line import/no-default-export -export default () => secureRndstr(16); +export const generateNativeUserToken = () => secureRndstr(16); + +export const isNativeUserToken = (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/verify-field-link.ts b/packages/backend/src/misc/verify-field-link.ts new file mode 100644 index 0000000000..62542eaaa0 --- /dev/null +++ b/packages/backend/src/misc/verify-field-link.ts @@ -0,0 +1,31 @@ +/* +* SPDX-FileCopyrightText: piuvas and other Sharkey contributors +* SPDX-License-Identifier: AGPL-3.0-only +*/ + +import { load as cheerio } from 'cheerio'; +import type { HttpRequestService } from '@/core/HttpRequestService.js'; + +type Field = { name: string, value: string }; + +export async function verifyFieldLinks(fields: Field[], profile_url: string, httpRequestService: HttpRequestService): Promise<string[]> { + const verified_links = []; + for (const field_url of fields.filter(x => URL.canParse(x.value) && ['http:', 'https:'].includes((new URL(x.value).protocol)))) { + try { + const html = await httpRequestService.getHtml(field_url.value); + + const doc = cheerio(html); + + const links = doc('a[rel~="me"][href], link[rel~="me"][href]').toArray(); + + const includesProfileLinks = links.some(link => link.attribs.href === profile_url); + if (includesProfileLinks) { + verified_links.push(field_url.value); + } + } catch { + // don't do anything. + } + } + + return verified_links; +} |