From ab85360d2ff5de1e3aa42e4a35aa4b640cccaf31 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 3 Oct 2024 11:17:21 -0300 Subject: [PATCH 01/64] refactor: move getConfigs() function and frontendConfig logic to 'src/utils/frontendConfig.ts' --- src/controllers/api/pleroma.ts | 32 ++++----------------------- src/utils/frontendConfig.ts | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 src/utils/frontendConfig.ts diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 31d8545f..2c025b8e 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -1,26 +1,20 @@ -import { NSchema as n, NStore } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; +import { configSchema } from '@/schemas/pleroma-api.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts'; +import { getConfigs, getPleromaConfig } from '@/utils/frontendConfig.ts'; import { lookupPubkey } from '@/utils/lookup.ts'; const frontendConfigController: AppController = async (c) => { const store = await Storages.db(); - const configs = await getConfigs(store, c.req.raw.signal); - const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); + const frontendConfig = await getPleromaConfig(store, c.req.raw.signal); if (frontendConfig) { - const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); - const data = schema.parse(frontendConfig.value).reduce>((result, [name, data]) => { - result[name.replace(/^:/, '')] = data; - return result; - }, {}); - return c.json(data); + return c.json(frontendConfig); } else { return c.json({}); } @@ -70,24 +64,6 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => { return c.json({}); }; -async function getConfigs(store: NStore, signal: AbortSignal): Promise { - const { pubkey } = Conf; - - const [event] = await store.query([{ - kinds: [30078], - authors: [pubkey], - '#d': ['pub.ditto.pleroma.config'], - limit: 1, - }], { signal }); - - try { - const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); - return n.json().pipe(configSchema.array()).catch([]).parse(decrypted); - } catch (_e) { - return []; - } -} - const pleromaAdminTagSchema = z.object({ nicknames: z.string().array(), tags: z.string().array(), diff --git a/src/utils/frontendConfig.ts b/src/utils/frontendConfig.ts new file mode 100644 index 00000000..a4f3f592 --- /dev/null +++ b/src/utils/frontendConfig.ts @@ -0,0 +1,40 @@ +import { NSchema as n, NStore } from '@nostrify/nostrify'; + +import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { Conf } from '@/config.ts'; +import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; + +export async function getPleromaConfig( + store: NStore, + signal?: AbortSignal, +): Promise> { + const configs = await getConfigs(store, signal ?? AbortSignal.timeout(1000)); + const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); + if (frontendConfig) { + const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); + const data = schema.parse(frontendConfig.value).reduce>((result, [name, data]) => { + result[name.replace(/^:/, '')] = data; + return result; + }, {}); + return data; + } + return undefined; +} + +export async function getConfigs(store: NStore, signal: AbortSignal): Promise { + const { pubkey } = Conf; + + const [event] = await store.query([{ + kinds: [30078], + authors: [pubkey], + '#d': ['pub.ditto.pleroma.config'], + limit: 1, + }], { signal }); + + try { + const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); + return n.json().pipe(configSchema.array()).catch([]).parse(decrypted); + } catch (_e) { + return []; + } +} From f3b7f91a07707adff13c2941a646bd5c022675d7 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 4 Oct 2024 23:37:01 -0300 Subject: [PATCH 02/64] feat: languageSchema converts value to lowercase and returns type LanguageCode --- src/schema.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 5efa7769..edaba0b4 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,4 @@ -import ISO6391 from 'iso-639-1'; +import ISO6391, { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; /** Validates individual items in an array, dropping any that aren't valid. */ @@ -42,6 +42,7 @@ const fileSchema = z.custom((value) => value instanceof File); const percentageSchema = z.coerce.number().int().gte(1).lte(100); const languageSchema = z.string().transform((val, ctx) => { + val = val.toLowerCase(); if (!ISO6391.validate(val)) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -49,7 +50,7 @@ const languageSchema = z.string().transform((val, ctx) => { }); return z.NEVER; } - return val; + return val as LanguageCode; }); export { From 0d126ad3b7157c0dc2dfd45dc03dd1b54edcad40 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:36:26 -0300 Subject: [PATCH 03/64] feat(languageSchema): split value to extract only language and not country code pt-BR becomes pt en-US becomes en --- src/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index edaba0b4..f21128d3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -42,7 +42,7 @@ const fileSchema = z.custom((value) => value instanceof File); const percentageSchema = z.coerce.number().int().gte(1).lte(100); const languageSchema = z.string().transform((val, ctx) => { - val = val.toLowerCase(); + val = (val.toLowerCase()).split('-')[0]; // pt-BR -> pt if (!ISO6391.validate(val)) { ctx.addIssue({ code: z.ZodIssueCode.custom, From de8eba40790b3c985c01ace87aa966da161d84d2 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:37:53 -0300 Subject: [PATCH 04/64] feat: create getLanguage() function, used for testing purposes --- src/test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test.ts b/src/test.ts index f4e720e1..c8fcfe6b 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,3 +1,5 @@ +import ISO6391, { LanguageCode } from 'iso-639-1'; +import lande from 'lande'; import { NostrEvent } from '@nostrify/nostrify'; import { finalizeEvent, generateSecretKey } from 'nostr-tools'; @@ -65,3 +67,15 @@ export async function createTestDB() { export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function getLanguage(text: string): LanguageCode | undefined { + const [topResult] = lande(text); + if (topResult) { + const [iso6393] = topResult; + const locale = new Intl.Locale(iso6393); + if (ISO6391.validate(locale.language)) { + return locale.language as LanguageCode; + } + } + return; +} From c6626313bc544baded47f7ea1e878d9726bd0b62 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:38:55 -0300 Subject: [PATCH 05/64] feat: get TRANSLATION_PROVIDER, TRANSLATION_PROVIDER_ENDPOINT & TRANSLATION_PROVIDER_API_KEY enviornment variables --- src/config.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/config.ts b/src/config.ts index ae841997..733ecc44 100644 --- a/src/config.ts +++ b/src/config.ts @@ -252,6 +252,18 @@ class Conf { static get preferredLanguages(): LanguageCode[] | undefined { return Deno.env.get('DITTO_LANGUAGES')?.split(',')?.filter(ISO6391.validate) as LanguageCode[]; } + /** Translation provider used to translate posts. */ + static get translationProvider(): string | undefined { + return Deno.env.get('TRANSLATION_PROVIDER')?.toLowerCase(); + } + /** Translation provider URL endpoint. */ + static get translationProviderEndpoint(): string | undefined { + return Deno.env.get('TRANSLATION_PROVIDER_ENDPOINT'); + } + /** Translation provider API KEY. */ + static get translationProviderApiKey(): string | undefined { + return Deno.env.get('TRANSLATION_PROVIDER_API_KEY'); + } /** Cache settings. */ static caches = { /** NIP-05 cache settings. */ From f434f875844504096497a4f64f2a33400ca65a31 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:39:42 -0300 Subject: [PATCH 06/64] feat(instanceV2Controller): enable translation --- src/controllers/api/instance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index b350963d..09e50632 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -128,7 +128,7 @@ const instanceV2Controller: AppController = async (c) => { max_expiration: 2629746, }, translation: { - enabled: false, + enabled: true, }, }, registrations: { From ea4d0f002afae725ceff0c49349aadb2348bd2f2 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:42:10 -0300 Subject: [PATCH 07/64] feat: create dittoTranslations LRUCache, create DittoTranslator interface, create MastodonTranslation type, create DittoTranslation type, create Provider type and some other minor ones --- src/translators/translator.ts | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/translators/translator.ts diff --git a/src/translators/translator.ts b/src/translators/translator.ts new file mode 100644 index 00000000..515b335f --- /dev/null +++ b/src/translators/translator.ts @@ -0,0 +1,65 @@ +import { LanguageCode } from 'iso-639-1'; +import { LRUCache } from 'lru-cache'; + +import { Time } from '@/utils/time.ts'; + +/** Supported providers. */ +export type Provider = 'DeepL.com' | 'libretranslate.com'; + +/** Original language of the post */ +export type SourceLanguage = LanguageCode; + +/** Content will be translated to this language */ +export type TargetLanguage = LanguageCode; + +/** Entity returned by DittoTranslator and LRUCache */ +type DittoTranslation = { + data: MastodonTranslation; +}; + +export type MastodonTranslation = { + /** HTML-encoded translated content of the status. */ + content: string; + /** The translated spoiler warning of the status. */ + spoiler_text: string; + /** The translated media descriptions of the status. */ + media_attachments: { id: string; description: string }[]; + /** The translated poll of the status. */ + poll: { id: string; options: { title: string }[] } | null; + //** The language of the source text, as auto-detected by the machine translation provider. */ + detected_source_language: SourceLanguage; + /** The service that provided the machine translation. */ + provider: Provider; +}; + +/** DittoTranslator class, used for status translation. */ +export interface DittoTranslator { + /** Translate the 'content' into 'targetLanguage'. */ + translate( + /** HTML-encoded content of the status. */ + content: string, + /** Spoiler warning of the status. */ + spoilerText: string, + /** Media descriptions of the status. */ + mediaAttachments: { id: string; description: string }[], + /** Poll of the status. */ + poll: { id: string; options: { title: string }[] } | null, + /** The language of the source text/status. */ + sourceLanguage: SourceLanguage | undefined, + /** The status content will be translated into this language. */ + targetLanguage: TargetLanguage, + /** Custom options. */ + opts?: { signal?: AbortSignal }, + ): Promise; +} + +/** Includes the TARGET language and the status id. + * Example: en-390f5b01b49a8ee6e13fe917420c023d889b3da8e983a14c9e84587e43d12c15 + * The example above means: + * I want the status 390f5b01b49a8ee6e13fe917420c023d889b3da8e983a14c9e84587e43d12c15 translated to english (if it exists in the LRUCache). */ +export type dittoTranslationsKey = `${TargetLanguage}-${string}`; + +export const dittoTranslations = new LRUCache({ + max: 1000, + ttl: Time.hours(6), +}); From 27f435a93cd68ea2e5974f610cb875b664a329b7 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:54:10 -0300 Subject: [PATCH 08/64] feat: create DeepLTranslator class that implements DittoTranslator --- src/translators/DeepLTranslator.ts | 142 +++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/translators/DeepLTranslator.ts diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts new file mode 100644 index 00000000..fd8788dd --- /dev/null +++ b/src/translators/DeepLTranslator.ts @@ -0,0 +1,142 @@ +import { z } from 'zod'; + +import { + DittoTranslator, + MastodonTranslation, + Provider, + SourceLanguage, + TargetLanguage, +} from '@/translators/translator.ts'; +import { languageSchema } from '@/schema.ts'; + +interface DeepLTranslatorOpts { + /** DeepL endpoint to use. Default: 'https://api.deepl.com '*/ + endpoint?: string; + /** DeepL API key. */ + apiKey: string; + /** Custom fetch implementation. */ + fetch?: typeof fetch; +} + +export class DeepLTranslator implements DittoTranslator { + private readonly endpoint: string; + private readonly apiKey: string; + private readonly fetch: typeof fetch; + private readonly provider: Provider; + + constructor(opts: DeepLTranslatorOpts) { + this.endpoint = opts.endpoint ?? 'https://api.deepl.com'; + this.fetch = opts.fetch ?? globalThis.fetch; + this.provider = 'DeepL.com'; + this.apiKey = opts.apiKey; + } + + async translate( + contentHTMLencoded: string, + spoilerText: string, + mediaAttachments: { id: string; description: string }[], + poll: { id: string; options: { title: string }[] } | null, + sourceLanguage: SourceLanguage | undefined, + targetLanguage: TargetLanguage, + opts?: { signal?: AbortSignal }, + ) { + // --------------------- START explanation + // Order of texts: + // 1 - contentHTMLencoded + // 2 - spoilerText + // 3 - mediaAttachments descriptions + // 4 - poll title options + const medias = mediaAttachments.map((value) => value.description); + + const polls = poll?.options.map((value) => value.title) ?? []; + + const text = [contentHTMLencoded, spoilerText].concat(medias, polls); + // --------------------- END explanation + + const body: any = { + text, + target_lang: targetLanguage.toUpperCase(), + tag_handling: 'html', + split_sentences: '1', + }; + if (sourceLanguage) { + body.source_lang = sourceLanguage.toUpperCase(); + } + + const headers = new Headers(); + headers.append('Authorization', 'DeepL-Auth-Key' + ' ' + this.apiKey); + headers.append('Content-Type', 'application/json'); + + const request = new Request(this.endpoint + '/v2/translate', { + method: 'POST', + body: JSON.stringify(body), + headers, + signal: opts?.signal, + }); + + const response = await this.fetch(request); + const json = await response.json(); + const data = DeepLTranslator.schema().parse(json).translations; + + const mastodonTranslation: MastodonTranslation = { + content: '', + spoiler_text: '', + media_attachments: [], + poll: null, + detected_source_language: 'en', + provider: this.provider, + }; + + /** Used to keep track of the offset. When slicing, should be used as the start value. */ + let startIndex = 0; + mastodonTranslation.content = data[0].text; + startIndex++; + + mastodonTranslation.spoiler_text = data[1].text; + startIndex++; + + if (medias.length) { + const mediasTranslated = data.slice(startIndex, startIndex + medias.length); + for (let i = 0; i < mediasTranslated.length; i++) { + mastodonTranslation.media_attachments.push({ + id: mediaAttachments[i].id, + description: mediasTranslated[i].text, + }); + } + startIndex += mediasTranslated.length; + } + + if (polls.length && poll) { + const pollsTranslated = data.slice(startIndex); + mastodonTranslation.poll = { + id: poll.id, + options: [], + }; + for (let i = 0; i < pollsTranslated.length; i++) { + mastodonTranslation.poll.options.push({ + title: pollsTranslated[i].text, + }); + } + startIndex += pollsTranslated.length; + } + + mastodonTranslation.detected_source_language = data[0].detected_source_language; + + return { + data: mastodonTranslation, + }; + } + + /** DeepL response schema. + * https://developers.deepl.com/docs/api-reference/translate/openapi-spec-for-text-translation */ + private static schema() { + return z.object({ + translations: z.array( + z.object({ + detected_source_language: languageSchema, + text: z.string(), + }), + ), + }); + } +} From 321d26b47093df75fd6b9ee0bbb5dd8194b2f2a1 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:55:12 -0300 Subject: [PATCH 09/64] test(DeepLTranslator): create multiple tests --- src/translators/DeepLTranslator.test.ts | 139 ++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/translators/DeepLTranslator.test.ts diff --git a/src/translators/DeepLTranslator.test.ts b/src/translators/DeepLTranslator.test.ts new file mode 100644 index 00000000..e261a9b6 --- /dev/null +++ b/src/translators/DeepLTranslator.test.ts @@ -0,0 +1,139 @@ +import { assertEquals } from '@std/assert'; + +import { Conf } from '@/config.ts'; +import { DeepLTranslator } from '@/translators/DeepLTranslator.ts'; +import { getLanguage } from '@/test.ts'; + +const endpoint = Conf.translationProviderEndpoint; +const apiKey = Conf.translationProviderApiKey; +const translationProvider = Conf.translationProvider; +const deepL = 'DeepL'.toLowerCase(); + +Deno.test('Translate status with EMPTY media_attachments and WITHOUT poll', { + ignore: !(translationProvider === deepL && apiKey), +}, async () => { + const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const mastodonTranslation = await translator.translate( + 'Bom dia amigos do Element, meu nome รฉ Patrick', + '', + [], + null, + 'pt', + 'en', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments, []); + assertEquals(mastodonTranslation.data.poll, null); + assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); +}); + +Deno.test('Translate status WITH auto detect and with EMPTY media_attachments and WITHOUT poll', { + ignore: !(translationProvider === deepL && apiKey), +}, async () => { + const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const mastodonTranslation = await translator.translate( + 'Bom dia amigos do Element, meu nome รฉ Patrick', + '', + [], + null, + undefined, + 'en', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments, []); + assertEquals(mastodonTranslation.data.poll, null); + assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); +}); + +Deno.test('Translate status WITH media_attachments and WITHOUT poll', { + ignore: !(translationProvider === deepL && apiKey), +}, async () => { + const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const mastodonTranslation = await translator.translate( + 'Hello my friends, my name is Alex and I am american.', + "That is spoiler isn't it", + [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], + null, + 'en', + 'pt', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); + assertEquals(getLanguage(mastodonTranslation.data.spoiler_text), 'pt'); + assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); + assertEquals(mastodonTranslation.data.poll, null); + assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); +}); + +Deno.test('Translate status WITHOUT media_attachments and WITH poll', { + ignore: !(translationProvider === deepL && apiKey), +}, async () => { + const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const poll = { + 'id': '34858', + 'options': [ + { + 'title': 'Kill him right now', + }, + { + 'title': 'Save him right now', + }, + ], + }; + + const mastodonTranslation = await translator.translate( + 'Hello my friends, my name is Alex and I am american.', + '', + [], + poll, + 'en', + 'pt', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments, []); + assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); + assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); +}); + +Deno.test('Translate status WITH media_attachments and WITH poll', { + ignore: !(translationProvider === deepL && apiKey), +}, async () => { + const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const poll = { + 'id': '34858', + 'options': [ + { + 'title': 'Kill him right now', + }, + { + 'title': 'Save him right now', + }, + ], + }; + + const mastodonTranslation = await translator.translate( + 'Hello my friends, my name is Alex and I am american.', + '', + [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], + poll, + 'en', + 'pt', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); + assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); + assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); +}); From c23ddb7d843388eb9f7f6cbb85f2d97ffd4ad436 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:58:33 -0300 Subject: [PATCH 10/64] feat: create LibreTranslateTranslator class that implements DittoTranslator --- src/translators/LibreTranslateTranslator.ts | 147 ++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/translators/LibreTranslateTranslator.ts diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts new file mode 100644 index 00000000..80f44479 --- /dev/null +++ b/src/translators/LibreTranslateTranslator.ts @@ -0,0 +1,147 @@ +import { z } from 'zod'; + +import { + DittoTranslator, + MastodonTranslation, + Provider, + SourceLanguage, + TargetLanguage, +} from '@/translators/translator.ts'; + +interface LibreTranslateTranslatorOpts { + /** Libretranslate endpoint to use. Default: 'https://libretranslate.com' */ + endpoint?: string; + /** Libretranslate API key. */ + apiKey: string; + /** Custom fetch implementation. */ + fetch?: typeof fetch; +} + +export class LibreTranslateTranslator implements DittoTranslator { + private readonly endpoint: string; + private readonly apiKey: string; + private readonly fetch: typeof fetch; + private readonly provider: Provider; + + constructor(opts: LibreTranslateTranslatorOpts) { + this.endpoint = opts.endpoint ?? 'https://libretranslate.com'; + this.fetch = opts.fetch ?? globalThis.fetch; + this.provider = 'libretranslate.com'; + this.apiKey = opts.apiKey; + } + + async translate( + contentHTMLencoded: string, + spoilerText: string, + mediaAttachments: { id: string; description: string }[], + poll: { id: string; options: { title: string }[] } | null, + sourceLanguage: SourceLanguage | undefined, + targetLanguage: TargetLanguage, + opts?: { signal?: AbortSignal }, + ) { + const mastodonTranslation: MastodonTranslation = { + content: '', + spoiler_text: '', + media_attachments: [], + poll: null, + detected_source_language: 'en', + provider: this.provider, + }; + + const translatedContent = await this.makeRequest(contentHTMLencoded, sourceLanguage, targetLanguage, 'html', { + signal: opts?.signal, + }); + mastodonTranslation.content = translatedContent; + + if (spoilerText.length) { + const translatedSpoilerText = await this.makeRequest(spoilerText, sourceLanguage, targetLanguage, 'text', { + signal: opts?.signal, + }); + mastodonTranslation.spoiler_text = translatedSpoilerText; + } + + if (mediaAttachments) { + for (const media of mediaAttachments) { + const translatedDescription = await this.makeRequest( + media.description, + sourceLanguage, + targetLanguage, + 'text', + { + signal: opts?.signal, + }, + ); + mastodonTranslation.media_attachments.push({ + id: media.id, + description: translatedDescription, + }); + } + } + + if (poll) { + mastodonTranslation.poll = { + id: poll.id, + options: [], + }; + + for (const option of poll.options) { + const translatedTitle = await this.makeRequest( + option.title, + sourceLanguage, + targetLanguage, + 'text', + { + signal: opts?.signal, + }, + ); + mastodonTranslation.poll.options.push({ + title: translatedTitle, + }); + } + } + + return { + data: mastodonTranslation, + }; + } + + private async makeRequest( + q: string, + sourceLanguage: string | undefined, + targetLanguage: string, + format: 'html' | 'text', + opts?: { signal?: AbortSignal }, + ): Promise { + const body = { + q, + source: sourceLanguage?.toLowerCase() ?? 'auto', + target: targetLanguage.toLowerCase(), + format, + api_key: this.apiKey, + }; + + const headers = new Headers(); + headers.append('Content-Type', 'application/json'); + + const request = new Request(this.endpoint + '/translate', { + method: 'POST', + body: JSON.stringify(body), + headers, + signal: opts?.signal, + }); + + const response = await this.fetch(request); + const json = await response.json(); + const data = LibreTranslateTranslator.schema().parse(json).translatedText; + + return data; + } + + /** Libretranslate response schema. + * https://libretranslate.com/docs/#/translate/post_translate */ + private static schema() { + return z.object({ + translatedText: z.string(), + }); + } +} From a2d8478e1a4f0c20463c6c23f1dd73c1488b01a3 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 14:59:05 -0300 Subject: [PATCH 11/64] test(LibreTranslateTranslator): create multiple tests --- .../LibreTranslateTranslator.test.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/translators/LibreTranslateTranslator.test.ts diff --git a/src/translators/LibreTranslateTranslator.test.ts b/src/translators/LibreTranslateTranslator.test.ts new file mode 100644 index 00000000..11f57d09 --- /dev/null +++ b/src/translators/LibreTranslateTranslator.test.ts @@ -0,0 +1,139 @@ +import { assertEquals } from '@std/assert'; + +import { Conf } from '@/config.ts'; +import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator.ts'; +import { getLanguage } from '@/test.ts'; + +const endpoint = Conf.translationProviderEndpoint; +const apiKey = Conf.translationProviderApiKey; +const translationProvider = Conf.translationProvider; +const libreTranslate = 'Libretranslate'.toLowerCase(); + +Deno.test('Translate status with EMPTY media_attachments and WITHOUT poll', { + ignore: !(translationProvider === libreTranslate && apiKey), +}, async () => { + const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const mastodonTranslation = await translator.translate( + 'Bom dia amigos do Element, meu nome รฉ Patrick', + '', + [], + null, + 'pt', + 'en', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments, []); + assertEquals(mastodonTranslation.data.poll, null); + assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); +}); + +Deno.test('Translate status WITH auto detect and with EMPTY media_attachments and WITHOUT poll', { + ignore: !(translationProvider === libreTranslate && apiKey), +}, async () => { + const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const mastodonTranslation = await translator.translate( + 'Bom dia amigos do Element, meu nome รฉ Patrick', + '', + [], + null, + undefined, + 'en', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments, []); + assertEquals(mastodonTranslation.data.poll, null); + assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); +}); + +Deno.test('Translate status WITH media_attachments and WITHOUT poll', { + ignore: !(translationProvider === libreTranslate && apiKey), +}, async () => { + const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const mastodonTranslation = await translator.translate( + 'Hello my friends, my name is Alex and I am american.', + "That is spoiler isn't it", + [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], + null, + 'en', + 'pt', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); + assertEquals(getLanguage(mastodonTranslation.data.spoiler_text), 'pt'); + assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); + assertEquals(mastodonTranslation.data.poll, null); + assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); +}); + +Deno.test('Translate status WITHOUT media_attachments and WITH poll', { + ignore: !(translationProvider === libreTranslate && apiKey), +}, async () => { + const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const poll = { + 'id': '34858', + 'options': [ + { + 'title': 'Kill him right now', + }, + { + 'title': 'Save him right now', + }, + ], + }; + + const mastodonTranslation = await translator.translate( + 'Hello my friends, my name is Alex and I am american.', + '', + [], + poll, + 'en', + 'pt', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments, []); + assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); + assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); +}); + +Deno.test('Translate status WITH media_attachments and WITH poll', { + ignore: !(translationProvider === libreTranslate && apiKey), +}, async () => { + const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + + const poll = { + 'id': '34858', + 'options': [ + { + 'title': 'Kill him right now', + }, + { + 'title': 'Save him right now', + }, + ], + }; + + const mastodonTranslation = await translator.translate( + 'Hello my friends, my name is Alex and I am american.', + '', + [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], + poll, + 'en', + 'pt', + ); + + assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); + assertEquals(mastodonTranslation.data.spoiler_text, ''); + assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); + assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); + assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); +}); From 8e58b1a7d46d1ed8f65da5c3a8a7d9af605dfd9f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 15:00:46 -0300 Subject: [PATCH 12/64] feat: create translatorMiddleware --- src/middleware/translatorMiddleware.ts | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/middleware/translatorMiddleware.ts diff --git a/src/middleware/translatorMiddleware.ts b/src/middleware/translatorMiddleware.ts new file mode 100644 index 00000000..6e336ad5 --- /dev/null +++ b/src/middleware/translatorMiddleware.ts @@ -0,0 +1,33 @@ +import { AppMiddleware } from '@/app.ts'; +import { Conf } from '@/config.ts'; +import { fetchWorker } from '@/workers/fetch.ts'; +import { DeepLTranslator } from '@/translators/DeepLTranslator.ts'; +import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator.ts'; + +/** Set the translator used for translating posts. */ +export const translatorMiddleware: AppMiddleware = async (c, next) => { + const endpoint = Conf.translationProviderEndpoint; + const apiKey = Conf.translationProviderApiKey; + const translationProvider = Conf.translationProvider; + + switch (translationProvider) { + case 'DeepL'.toLowerCase(): + if (apiKey) { + c.set( + 'translator', + new DeepLTranslator({ endpoint, apiKey, fetch: fetchWorker }), + ); + } + break; + case 'Libretranslate'.toLowerCase(): + if (apiKey) { + c.set( + 'translator', + new LibreTranslateTranslator({ endpoint, apiKey, fetch: fetchWorker }), + ); + } + break; + } + + await next(); +}; From b369b2171d38fe16b4d6277030595ba71bf5af7f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 15:02:04 -0300 Subject: [PATCH 13/64] feat: create translateController - /api/v1/statuses/:id/translate --- src/app.ts | 6 +++ src/controllers/api/translate.ts | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/controllers/api/translate.ts diff --git a/src/app.ts b/src/app.ts index 9fb67ced..8831f188 100644 --- a/src/app.ts +++ b/src/app.ts @@ -109,6 +109,7 @@ import { trendingStatusesController, trendingTagsController, } from '@/controllers/api/trends.ts'; +import { translateController } from '@/controllers/api/translate.ts'; import { errorHandler } from '@/controllers/error.ts'; import { frontendController } from '@/controllers/frontend.ts'; import { metricsController } from '@/controllers/metrics.ts'; @@ -126,6 +127,8 @@ import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts'; +import { DittoTranslator } from '@/translators/translator.ts'; +import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts'; interface AppEnv extends HonoEnv { Variables: { @@ -141,6 +144,8 @@ interface AppEnv extends HonoEnv { pagination: { since?: number; until?: number; limit: number }; /** Normalized list pagination params. */ listPagination: { offset: number; limit: number }; + /** Translation service. */ + translator?: DittoTranslator; }; } @@ -220,6 +225,7 @@ app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requireSigner, bookmarkC app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requireSigner, unbookmarkController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requireSigner, pinController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requireSigner, unpinController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/translate', requireSigner, translatorMiddleware, translateController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requireSigner, reblogStatusController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requireSigner, unreblogStatusController); app.post('/api/v1/statuses', requireSigner, createStatusController); diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts new file mode 100644 index 00000000..86aaa1f4 --- /dev/null +++ b/src/controllers/api/translate.ts @@ -0,0 +1,89 @@ +import { LanguageCode } from 'iso-639-1'; +import { z } from 'zod'; + +import { AppController } from '@/app.ts'; +import { languageSchema } from '@/schema.ts'; +import { Storages } from '@/storages.ts'; +import { dittoTranslations, dittoTranslationsKey } from '@/translators/translator.ts'; +import { parseBody } from '@/utils/api.ts'; +import { getEvent } from '@/queries.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; + +const translateSchema = z.object({ + //lang: languageSchema, // Correct property name, as stated by Mastodon docs + target_language: languageSchema, // Property name soapbox sends +}); + +const translateController: AppController = async (c) => { + const result = translateSchema.safeParse(await parseBody(c.req.raw)); + const { signal } = c.req.raw; + + if (!result.success) { + return c.json({ error: 'Bad request.', schema: result.error }, 422); + } + + const translator = c.get('translator'); + if (!translator) { + return c.json({ error: 'No translator configured.' }, 500); + } + + const { target_language } = result.data; + const targetLang = target_language; + const id = c.req.param('id'); + + const event = await getEvent(id, { signal }); + if (!event) { + return c.json({ error: 'Record not found' }, 400); + } + + const viewerPubkey = await c.get('signer')?.getPublicKey(); + + const kysely = await Storages.kysely(); + + let sourceLang = (await kysely + .selectFrom('nostr_events') + .select('language').where('id', '=', id) + .limit(1) + .execute())[0]?.language as LanguageCode | undefined; + if (!sourceLang) { + sourceLang = undefined; + } + + if (targetLang.toLowerCase() === sourceLang?.toLowerCase()) { + return c.json({ error: 'Source and target languages are the same. No translation needed.' }, 400); + } + + const status = await renderStatus(event, { viewerPubkey }); + + const translatedId = `${target_language}-${id}` as dittoTranslationsKey; + const translationCache = dittoTranslations.get(translatedId); + + if (translationCache) { + return c.json(translationCache.data, 200); + } + + const mediaAttachments = status?.media_attachments.map((value) => { + return { + id: value.id, + description: value.description ?? '', + }; + }) ?? []; + + try { + const translation = await translator.translate( + status?.content ?? '', + status?.spoiler_text ?? '', + mediaAttachments, + null, + sourceLang, + targetLang, + { signal }, + ); + dittoTranslations.set(translatedId, translation); + return c.json(translation.data, 200); + } catch (_) { + return c.json({ error: 'Service Unavailable' }, 503); + } +}; + +export { translateController }; From bbbce958d9201c6af408cf5c5083160699608885 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 16:04:59 -0300 Subject: [PATCH 14/64] chore: update nostrify:db --- deno.json | 2 +- deno.lock | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/deno.json b/deno.json index f97d4fa7..06952b75 100644 --- a/deno.json +++ b/deno.json @@ -41,7 +41,7 @@ "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/db": "jsr:@nostrify/db@^0.35.0", + "@nostrify/db": "jsr:@nostrify/db@^0.36.1", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.0", "@nostrify/policies": "jsr:@nostrify/policies@^0.35.0", "@scure/base": "npm:@scure/base@^1.1.6", diff --git a/deno.lock b/deno.lock index 84969bb2..37507e0d 100644 --- a/deno.lock +++ b/deno.lock @@ -20,9 +20,9 @@ "jsr:@gleasonator/policy@0.6.4": "jsr:@gleasonator/policy@0.6.4", "jsr:@gleasonator/policy@0.7.0": "jsr:@gleasonator/policy@0.7.0", "jsr:@gleasonator/policy@0.7.1": "jsr:@gleasonator/policy@0.7.1", - "jsr:@hono/hono@^4.4.6": "jsr:@hono/hono@4.6.2", + "jsr:@hono/hono@^4.4.6": "jsr:@hono/hono@4.6.3", "jsr:@lambdalisue/async@^2.1.1": "jsr:@lambdalisue/async@2.1.1", - "jsr:@nostrify/db@^0.35.0": "jsr:@nostrify/db@0.35.0", + "jsr:@nostrify/db@^0.36.1": "jsr:@nostrify/db@0.36.1", "jsr:@nostrify/nostrify@^0.22.1": "jsr:@nostrify/nostrify@0.22.5", "jsr:@nostrify/nostrify@^0.22.4": "jsr:@nostrify/nostrify@0.22.4", "jsr:@nostrify/nostrify@^0.22.5": "jsr:@nostrify/nostrify@0.22.5", @@ -62,7 +62,7 @@ "jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3", "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.4", "jsr:@std/io@^0.223.0": "jsr:@std/io@0.223.0", - "jsr:@std/io@^0.224": "jsr:@std/io@0.224.8", + "jsr:@std/io@^0.224": "jsr:@std/io@0.224.9", "jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0", "jsr:@std/media-types@^0.224.1": "jsr:@std/media-types@0.224.1", "jsr:@std/path@0.213.1": "jsr:@std/path@0.213.1", @@ -260,13 +260,16 @@ "@hono/hono@4.6.2": { "integrity": "35fcf3be4687825080b01bed7bbe2ac66f8d8b8939f0bad459661bf3b46d916f" }, + "@hono/hono@4.6.3": { + "integrity": "a1f5a18cd12a0db54755b0461dd5a4e2d93a6f85403073eb710103eacc42daf3" + }, "@lambdalisue/async@2.1.1": { "integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4" }, - "@nostrify/db@0.35.0": { - "integrity": "637191c41812544e361b7997dc44ea098f8bd7efebb28f37a8a7142a0ecada8d", + "@nostrify/db@0.36.1": { + "integrity": "b65b89ca6fe98d9dbcc0402b5c9c07b8430c2c91f84ba4128ff2eeed70c3d49f", "dependencies": [ - "jsr:@nostrify/nostrify@^0.35.0", + "jsr:@nostrify/nostrify@^0.36.0", "jsr:@nostrify/types@^0.35.0", "npm:kysely@^0.27.3", "npm:nostr-tools@^2.7.0" @@ -558,6 +561,12 @@ "jsr:@std/bytes@^1.0.2" ] }, + "@std/io@0.224.9": { + "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3", + "dependencies": [ + "jsr:@std/bytes@^1.0.2" + ] + }, "@std/json@0.223.0": { "integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f", "dependencies": [ @@ -2149,7 +2158,7 @@ "jsr:@gfx/canvas-wasm@^0.4.2", "jsr:@hono/hono@^4.4.6", "jsr:@lambdalisue/async@^2.1.1", - "jsr:@nostrify/db@^0.35.0", + "jsr:@nostrify/db@^0.36.1", "jsr:@nostrify/nostrify@^0.36.0", "jsr:@nostrify/policies@^0.35.0", "jsr:@soapbox/kysely-pglite@^1.0.0", From 4712cb1d80aa2bd82bf6e08103e2697d84da1069 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 16:05:22 -0300 Subject: [PATCH 15/64] fix: fix language property in the Mastodon API --- src/interfaces/DittoEvent.ts | 3 +++ src/storages/EventsDB.ts | 18 +++++++++++++++++- src/views/mastodon/statuses.ts | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index dcaec6ae..cca7c0ca 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -1,4 +1,5 @@ import { NostrEvent } from '@nostrify/nostrify'; +import { LanguageCode } from 'iso-639-1'; /** Ditto internal stats for the event's author. */ export interface AuthorStats { @@ -43,4 +44,6 @@ export interface DittoEvent extends NostrEvent { zap_sender?: DittoEvent | string; zap_amount?: number; zap_message?: string; + /** Language of the event (kind 1s are more accurate). */ + language?: LanguageCode; } diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 1bf3cd86..dfff6394 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -1,5 +1,6 @@ // deno-lint-ignore-file require-await +import { LanguageCode } from 'iso-639-1'; import { NPostgres, NPostgresSchema } from '@nostrify/db'; import { NIP50, NKinds, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { Stickynotes } from '@soapbox/stickynotes'; @@ -12,6 +13,7 @@ import { RelayError } from '@/RelayError.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; import { purifyEvent } from '@/utils/purify.ts'; +import { DittoEvent } from '@/interfaces/DittoEvent.ts'; /** Function to decide whether or not to index a tag. */ type TagCondition = ({ event, count, value }: { @@ -175,7 +177,7 @@ class EventsDB extends NPostgres { override async query( filters: NostrFilter[], opts: { signal?: AbortSignal; timeout?: number; limit?: number } = {}, - ): Promise { + ): Promise { filters = await this.expandFilters(filters); for (const filter of filters) { @@ -199,6 +201,20 @@ class EventsDB extends NPostgres { return super.query(filters, { ...opts, timeout: opts.timeout ?? this.opts.timeout }); } + /** Parse an event row from the database. */ + protected override parseEventRow(row: Pick): DittoEvent { + return { + id: row.id, + kind: row.kind, + pubkey: row.pubkey, + content: row.content, + created_at: Number(row.created_at), + tags: row.tags, + sig: row.sig, + language: (row.language || undefined) as LanguageCode, + }; + } + /** Delete events based on filters from the database. */ override async remove(filters: NostrFilter[], opts: { signal?: AbortSignal; timeout?: number } = {}): Promise { this.console.debug('DELETE', JSON.stringify(filters)); diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index e21c9e1c..48d8e099 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -113,7 +113,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< sensitive: !!cw, spoiler_text: (cw ? cw[1] : subject?.[1]) || '', visibility: 'public', - language: event.tags.find((tag) => tag[0] === 'l' && tag[2] === 'ISO-639-1')?.[1] || null, + language: event.language ?? null, replies_count: event.event_stats?.replies_count ?? 0, reblogs_count: event.event_stats?.reposts_count ?? 0, favourites_count: event.event_stats?.reactions['+'] ?? 0, From bfab84d9376e61258617b126e67281e5247a2003 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 17:54:03 -0300 Subject: [PATCH 16/64] refactor: make provider lowercase because supporting case insensitive is allegedly protocol bloat --- src/config.ts | 2 +- src/middleware/translatorMiddleware.ts | 4 ++-- src/translators/DeepLTranslator.test.ts | 2 +- src/translators/LibreTranslateTranslator.test.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config.ts b/src/config.ts index 8148ab3b..848f9836 100644 --- a/src/config.ts +++ b/src/config.ts @@ -273,7 +273,7 @@ class Conf { } /** Translation provider used to translate posts. */ static get translationProvider(): string | undefined { - return Deno.env.get('TRANSLATION_PROVIDER')?.toLowerCase(); + return Deno.env.get('TRANSLATION_PROVIDER'); } /** Translation provider URL endpoint. */ static get translationProviderEndpoint(): string | undefined { diff --git a/src/middleware/translatorMiddleware.ts b/src/middleware/translatorMiddleware.ts index 6e336ad5..2d3971dd 100644 --- a/src/middleware/translatorMiddleware.ts +++ b/src/middleware/translatorMiddleware.ts @@ -11,7 +11,7 @@ export const translatorMiddleware: AppMiddleware = async (c, next) => { const translationProvider = Conf.translationProvider; switch (translationProvider) { - case 'DeepL'.toLowerCase(): + case 'deepl': if (apiKey) { c.set( 'translator', @@ -19,7 +19,7 @@ export const translatorMiddleware: AppMiddleware = async (c, next) => { ); } break; - case 'Libretranslate'.toLowerCase(): + case 'libretranslate': if (apiKey) { c.set( 'translator', diff --git a/src/translators/DeepLTranslator.test.ts b/src/translators/DeepLTranslator.test.ts index e261a9b6..8335670e 100644 --- a/src/translators/DeepLTranslator.test.ts +++ b/src/translators/DeepLTranslator.test.ts @@ -7,7 +7,7 @@ import { getLanguage } from '@/test.ts'; const endpoint = Conf.translationProviderEndpoint; const apiKey = Conf.translationProviderApiKey; const translationProvider = Conf.translationProvider; -const deepL = 'DeepL'.toLowerCase(); +const deepL = 'deepl'; Deno.test('Translate status with EMPTY media_attachments and WITHOUT poll', { ignore: !(translationProvider === deepL && apiKey), diff --git a/src/translators/LibreTranslateTranslator.test.ts b/src/translators/LibreTranslateTranslator.test.ts index 11f57d09..8d1fc24d 100644 --- a/src/translators/LibreTranslateTranslator.test.ts +++ b/src/translators/LibreTranslateTranslator.test.ts @@ -7,7 +7,7 @@ import { getLanguage } from '@/test.ts'; const endpoint = Conf.translationProviderEndpoint; const apiKey = Conf.translationProviderApiKey; const translationProvider = Conf.translationProvider; -const libreTranslate = 'Libretranslate'.toLowerCase(); +const libreTranslate = 'libretranslate'; Deno.test('Translate status with EMPTY media_attachments and WITHOUT poll', { ignore: !(translationProvider === libreTranslate && apiKey), From acbdae29ae71ee3f569eba7da9cd29d35f0244c4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 17:54:41 -0300 Subject: [PATCH 17/64] fix(EventsDB): type is correct, ignore type complaint --- src/storages/EventsDB.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index dfff6394..957e996c 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -146,10 +146,12 @@ class EventsDB extends NPostgres { } } + // @ts-ignore The type is correct, but NPostgres doesn't realize it. I don't think it's solvable without modifying NPostgres again, which I don't think is worth it for this. protected override getFilterQuery(trx: Kysely, filter: NostrFilter) { if (filter.search) { const tokens = NIP50.parseInput(filter.search); + // @ts-ignore The type is correct, but NPostgres doesn't realize it. I don't think it's solvable without modifying NPostgres again, which I don't think is worth it for this. let query = super.getFilterQuery(trx, { ...filter, search: tokens.filter((t) => typeof t === 'string').join(' '), From 6c931531176de804c8391759b59b8d3a4e399d8b Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 7 Oct 2024 17:55:50 -0300 Subject: [PATCH 18/64] refactor: get language from event itself --- src/controllers/api/translate.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 86aaa1f4..3e8bf13c 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -38,18 +38,7 @@ const translateController: AppController = async (c) => { const viewerPubkey = await c.get('signer')?.getPublicKey(); - const kysely = await Storages.kysely(); - - let sourceLang = (await kysely - .selectFrom('nostr_events') - .select('language').where('id', '=', id) - .limit(1) - .execute())[0]?.language as LanguageCode | undefined; - if (!sourceLang) { - sourceLang = undefined; - } - - if (targetLang.toLowerCase() === sourceLang?.toLowerCase()) { + if (targetLang.toLowerCase() === event?.language?.toLowerCase()) { return c.json({ error: 'Source and target languages are the same. No translation needed.' }, 400); } @@ -75,7 +64,7 @@ const translateController: AppController = async (c) => { status?.spoiler_text ?? '', mediaAttachments, null, - sourceLang, + event.language, targetLang, { signal }, ); From 3f00f255a5e817a3149e1157fc215f33ffc48987 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 11:01:34 -0300 Subject: [PATCH 19/64] fix: type assertions in EventsDB --- src/db/DittoTables.ts | 4 +--- src/storages/EventsDB.ts | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index b6fa93f4..46eeeab9 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -1,5 +1,3 @@ -import { Nullable } from 'kysely'; - import { NPostgresSchema } from '@nostrify/db'; export interface DittoTables extends NPostgresSchema { @@ -12,7 +10,7 @@ export interface DittoTables extends NPostgresSchema { } type NostrEventsRow = NPostgresSchema['nostr_events'] & { - language: Nullable; + language: string | null; }; interface AuthorStatsRow { diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 957e996c..905883b7 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -146,16 +146,14 @@ class EventsDB extends NPostgres { } } - // @ts-ignore The type is correct, but NPostgres doesn't realize it. I don't think it's solvable without modifying NPostgres again, which I don't think is worth it for this. protected override getFilterQuery(trx: Kysely, filter: NostrFilter) { if (filter.search) { const tokens = NIP50.parseInput(filter.search); - // @ts-ignore The type is correct, but NPostgres doesn't realize it. I don't think it's solvable without modifying NPostgres again, which I don't think is worth it for this. let query = super.getFilterQuery(trx, { ...filter, search: tokens.filter((t) => typeof t === 'string').join(' '), - }) as SelectQueryBuilder>; + }) as SelectQueryBuilder; const languages = new Set(); @@ -204,7 +202,7 @@ class EventsDB extends NPostgres { } /** Parse an event row from the database. */ - protected override parseEventRow(row: Pick): DittoEvent { + protected override parseEventRow(row: DittoTables['nostr_events']): DittoEvent { return { id: row.id, kind: row.kind, From 17be4ab48fde243190fabdf6234bbd88d0f2ac28 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 11:11:29 -0300 Subject: [PATCH 20/64] fix(instanceV1Controller): add translation field --- src/controllers/api/instance.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 09e50632..ae31bdc2 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -36,6 +36,9 @@ const instanceV1Controller: AppController = async (c) => { max_characters: Conf.postCharLimit, max_media_attachments: 20, }, + translation: { + enabled: true, + }, }, pleroma: { metadata: { From df27959d35a6c045acefb0593be60bc9d9c4c008 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 14:15:48 -0300 Subject: [PATCH 21/64] fix(relay.ts): purify event --- src/controllers/nostr/relay.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 2a38e751..f62ad76b 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -18,6 +18,7 @@ import * as pipeline from '@/pipeline.ts'; import { RelayError } from '@/RelayError.ts'; import { Storages } from '@/storages.ts'; import { Time } from '@/utils/time.ts'; +import { purifyEvent } from '@/utils/purify.ts'; /** Limit of initial events returned for a subscription. */ const FILTER_LIMIT = 100; @@ -105,7 +106,7 @@ function connectStream(socket: WebSocket, ip: string | undefined) { try { for (const event of await store.query(filters, { limit: FILTER_LIMIT, timeout: Conf.db.timeouts.relay })) { - send(['EVENT', subId, event]); + send(['EVENT', subId, purifyEvent(event)]); } } catch (e: any) { if (e instanceof RelayError) { @@ -137,7 +138,7 @@ function connectStream(socket: WebSocket, ip: string | undefined) { relayEventsCounter.inc({ kind: event.kind.toString() }); try { // This will store it (if eligible) and run other side-effects. - await pipeline.handleEvent(event, AbortSignal.timeout(1000)); + await pipeline.handleEvent(purifyEvent(event), AbortSignal.timeout(1000)); send(['OK', event.id, true, '']); } catch (e) { if (e instanceof RelayError) { From d4a8ec21fef73bdf600ee56cd17f8bd904381e65 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 14:17:27 -0300 Subject: [PATCH 22/64] fix: add 'pure' option in EventsDB if pure is true, EventsDB will return a Nostr event, otherwise it will return a Ditto event --- src/storages/EventsDB.test.ts | 26 +++++++++++++------------- src/storages/EventsDB.ts | 15 +++++++++++++-- src/test.ts | 3 ++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/storages/EventsDB.test.ts b/src/storages/EventsDB.test.ts index b24032aa..e19fe775 100644 --- a/src/storages/EventsDB.test.ts +++ b/src/storages/EventsDB.test.ts @@ -7,7 +7,7 @@ import { Conf } from '@/config.ts'; import { createTestDB } from '@/test.ts'; Deno.test('count filters', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const event1 = await eventFixture('event-1'); @@ -18,7 +18,7 @@ Deno.test('count filters', async () => { }); Deno.test('insert and filter events', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const event1 = await eventFixture('event-1'); @@ -35,7 +35,7 @@ Deno.test('insert and filter events', async () => { }); Deno.test('query events with domain search filter', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store, kysely } = db; const event1 = await eventFixture('event-1'); @@ -55,7 +55,7 @@ Deno.test('query events with domain search filter', async () => { }); Deno.test('query events with language search filter', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store, kysely } = db; const en = genEvent({ kind: 1, content: 'hello world!' }); @@ -72,7 +72,7 @@ Deno.test('query events with language search filter', async () => { }); Deno.test('delete events', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const sk = generateSecretKey(); @@ -96,7 +96,7 @@ Deno.test('delete events', async () => { }); Deno.test("user cannot delete another user's event", async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const event = genEvent({ kind: 1, content: 'hello world', created_at: 1 }); @@ -113,7 +113,7 @@ Deno.test("user cannot delete another user's event", async () => { }); Deno.test('admin can delete any event', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const sk = generateSecretKey(); @@ -137,7 +137,7 @@ Deno.test('admin can delete any event', async () => { }); Deno.test('throws a RelayError when inserting an event deleted by the admin', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const event = genEvent(); @@ -154,7 +154,7 @@ Deno.test('throws a RelayError when inserting an event deleted by the admin', as }); Deno.test('throws a RelayError when inserting an event deleted by a user', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const sk = generateSecretKey(); @@ -173,7 +173,7 @@ Deno.test('throws a RelayError when inserting an event deleted by a user', async }); Deno.test('inserting replaceable events', async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; const sk = generateSecretKey(); @@ -190,7 +190,7 @@ Deno.test('inserting replaceable events', async () => { }); Deno.test("throws a RelayError when querying an event with a large 'since'", async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; await assertRejects( @@ -201,7 +201,7 @@ Deno.test("throws a RelayError when querying an event with a large 'since'", asy }); Deno.test("throws a RelayError when querying an event with a large 'until'", async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; await assertRejects( @@ -212,7 +212,7 @@ Deno.test("throws a RelayError when querying an event with a large 'until'", asy }); Deno.test("throws a RelayError when querying an event with a large 'kind'", async () => { - await using db = await createTestDB(); + await using db = await createTestDB({ pure: true }); const { store } = db; await assertRejects( diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 905883b7..b303dad0 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -30,6 +30,8 @@ interface EventsDBOpts { pubkey: string; /** Timeout in milliseconds for database queries. */ timeout: number; + /** Whether the event returned should be a Nostr event or a Ditto event. Defaults to false. */ + pure?: boolean; } /** SQL database storage adapter for Nostr events. */ @@ -203,7 +205,7 @@ class EventsDB extends NPostgres { /** Parse an event row from the database. */ protected override parseEventRow(row: DittoTables['nostr_events']): DittoEvent { - return { + const event: DittoEvent = { id: row.id, kind: row.kind, pubkey: row.pubkey, @@ -211,8 +213,17 @@ class EventsDB extends NPostgres { created_at: Number(row.created_at), tags: row.tags, sig: row.sig, - language: (row.language || undefined) as LanguageCode, }; + + if (this.opts.pure) { + return event; + } + + if (row.language) { + event.language = row.language as LanguageCode; + } + + return event; } /** Delete events based on filters from the database. */ diff --git a/src/test.ts b/src/test.ts index c8fcfe6b..3f2d1c38 100644 --- a/src/test.ts +++ b/src/test.ts @@ -35,7 +35,7 @@ export function genEvent(t: Partial = {}, sk: Uint8Array = generateS } /** Create a database for testing. It uses `TEST_DATABASE_URL`, or creates an in-memory database by default. */ -export async function createTestDB() { +export async function createTestDB(opts?: { pure?: boolean }) { const { testDatabaseUrl } = Conf; const { kysely } = DittoDB.create(testDatabaseUrl, { poolSize: 1 }); @@ -45,6 +45,7 @@ export async function createTestDB() { kysely, timeout: Conf.db.timeouts.default, pubkey: Conf.pubkey, + pure: opts?.pure ?? false, }); return { From ba2393172706a90b767d146c86111e9b0ed7cd68 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 14:25:25 -0300 Subject: [PATCH 23/64] refactor: remove unused imports --- src/controllers/api/translate.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 3e8bf13c..629571ff 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -1,9 +1,7 @@ -import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; import { AppController } from '@/app.ts'; import { languageSchema } from '@/schema.ts'; -import { Storages } from '@/storages.ts'; import { dittoTranslations, dittoTranslationsKey } from '@/translators/translator.ts'; import { parseBody } from '@/utils/api.ts'; import { getEvent } from '@/queries.ts'; From a3bc5ec5c3bd9e4baeda4dd3f9cba9160641b5ae Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 14:27:00 -0300 Subject: [PATCH 24/64] refactor: remove translation enabled in instanceV1Controller --- src/controllers/api/instance.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index ae31bdc2..09e50632 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -36,9 +36,6 @@ const instanceV1Controller: AppController = async (c) => { max_characters: Conf.postCharLimit, max_media_attachments: 20, }, - translation: { - enabled: true, - }, }, pleroma: { metadata: { From f76ee000b098abb00acbbfe912ac43bdb1890a58 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 14:35:51 -0300 Subject: [PATCH 25/64] refactor: use 'lang' instead of 'target_language' --- src/controllers/api/translate.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 629571ff..37b0bcea 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -8,8 +8,7 @@ import { getEvent } from '@/queries.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; const translateSchema = z.object({ - //lang: languageSchema, // Correct property name, as stated by Mastodon docs - target_language: languageSchema, // Property name soapbox sends + lang: languageSchema, }); const translateController: AppController = async (c) => { @@ -25,8 +24,7 @@ const translateController: AppController = async (c) => { return c.json({ error: 'No translator configured.' }, 500); } - const { target_language } = result.data; - const targetLang = target_language; + const { lang } = result.data; const id = c.req.param('id'); const event = await getEvent(id, { signal }); @@ -36,13 +34,13 @@ const translateController: AppController = async (c) => { const viewerPubkey = await c.get('signer')?.getPublicKey(); - if (targetLang.toLowerCase() === event?.language?.toLowerCase()) { + if (lang.toLowerCase() === event?.language?.toLowerCase()) { return c.json({ error: 'Source and target languages are the same. No translation needed.' }, 400); } const status = await renderStatus(event, { viewerPubkey }); - const translatedId = `${target_language}-${id}` as dittoTranslationsKey; + const translatedId = `${lang}-${id}` as dittoTranslationsKey; const translationCache = dittoTranslations.get(translatedId); if (translationCache) { @@ -63,7 +61,7 @@ const translateController: AppController = async (c) => { mediaAttachments, null, event.language, - targetLang, + lang, { signal }, ); dittoTranslations.set(translatedId, translation); From dbd590228d7128353ae026bcb2cddba3e00adb46 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 14:39:59 -0300 Subject: [PATCH 26/64] fix: typo --- src/translators/DeepLTranslator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index fd8788dd..b340a715 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -10,7 +10,7 @@ import { import { languageSchema } from '@/schema.ts'; interface DeepLTranslatorOpts { - /** DeepL endpoint to use. Default: 'https://api.deepl.com '*/ + /** DeepL endpoint to use. Default: 'https://api.deepl.com' */ endpoint?: string; /** DeepL API key. */ apiKey: string; From 20caaa9ebdc6b1ac412349313384f91256fc4080 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 16:53:30 -0300 Subject: [PATCH 27/64] refactor: LibreTranslate and DeepL with separate environment variables for their configuration --- src/config.ts | 20 +++++++++++++------ src/middleware/translatorMiddleware.ts | 18 +++++++++++------ src/translators/DeepLTranslator.test.ts | 4 ++-- .../LibreTranslateTranslator.test.ts | 4 ++-- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/config.ts b/src/config.ts index 848f9836..0e4fc816 100644 --- a/src/config.ts +++ b/src/config.ts @@ -275,13 +275,21 @@ class Conf { static get translationProvider(): string | undefined { return Deno.env.get('TRANSLATION_PROVIDER'); } - /** Translation provider URL endpoint. */ - static get translationProviderEndpoint(): string | undefined { - return Deno.env.get('TRANSLATION_PROVIDER_ENDPOINT'); + /** DeepL URL endpoint. */ + static get deepLendpoint(): string | undefined { + return Deno.env.get('DEEPL_ENDPOINT'); } - /** Translation provider API KEY. */ - static get translationProviderApiKey(): string | undefined { - return Deno.env.get('TRANSLATION_PROVIDER_API_KEY'); + /** DeepL API KEY. */ + static get deepLapiKey(): string | undefined { + return Deno.env.get('DEEPL_API_KEY'); + } + /** LibreTranslate URL endpoint. */ + static get libreTranslateEndpoint(): string | undefined { + return Deno.env.get('LIBRETRANSLATE_ENDPOINT'); + } + /** LibreTranslate API KEY. */ + static get libreTranslateApiKey(): string | undefined { + return Deno.env.get('LIBRETRANSLATE_API_KEY'); } /** Cache settings. */ static caches = { diff --git a/src/middleware/translatorMiddleware.ts b/src/middleware/translatorMiddleware.ts index 2d3971dd..b8a07686 100644 --- a/src/middleware/translatorMiddleware.ts +++ b/src/middleware/translatorMiddleware.ts @@ -6,24 +6,30 @@ import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator /** Set the translator used for translating posts. */ export const translatorMiddleware: AppMiddleware = async (c, next) => { - const endpoint = Conf.translationProviderEndpoint; - const apiKey = Conf.translationProviderApiKey; + const deepLendpoint = Conf.deepLendpoint; + const deepLapiKey = Conf.deepLapiKey; + const libreTranslateEndpoint = Conf.libreTranslateEndpoint; + const libreTranslateApiKey = Conf.libreTranslateApiKey; const translationProvider = Conf.translationProvider; switch (translationProvider) { case 'deepl': - if (apiKey) { + if (deepLapiKey) { c.set( 'translator', - new DeepLTranslator({ endpoint, apiKey, fetch: fetchWorker }), + new DeepLTranslator({ endpoint: deepLendpoint, apiKey: deepLapiKey, fetch: fetchWorker }), ); } break; case 'libretranslate': - if (apiKey) { + if (libreTranslateApiKey) { c.set( 'translator', - new LibreTranslateTranslator({ endpoint, apiKey, fetch: fetchWorker }), + new LibreTranslateTranslator({ + endpoint: libreTranslateEndpoint, + apiKey: libreTranslateApiKey, + fetch: fetchWorker, + }), ); } break; diff --git a/src/translators/DeepLTranslator.test.ts b/src/translators/DeepLTranslator.test.ts index 8335670e..f8a12ede 100644 --- a/src/translators/DeepLTranslator.test.ts +++ b/src/translators/DeepLTranslator.test.ts @@ -4,8 +4,8 @@ import { Conf } from '@/config.ts'; import { DeepLTranslator } from '@/translators/DeepLTranslator.ts'; import { getLanguage } from '@/test.ts'; -const endpoint = Conf.translationProviderEndpoint; -const apiKey = Conf.translationProviderApiKey; +const endpoint = Conf.deepLendpoint; +const apiKey = Conf.deepLapiKey; const translationProvider = Conf.translationProvider; const deepL = 'deepl'; diff --git a/src/translators/LibreTranslateTranslator.test.ts b/src/translators/LibreTranslateTranslator.test.ts index 8d1fc24d..f7e89b30 100644 --- a/src/translators/LibreTranslateTranslator.test.ts +++ b/src/translators/LibreTranslateTranslator.test.ts @@ -4,8 +4,8 @@ import { Conf } from '@/config.ts'; import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator.ts'; import { getLanguage } from '@/test.ts'; -const endpoint = Conf.translationProviderEndpoint; -const apiKey = Conf.translationProviderApiKey; +const endpoint = Conf.libreTranslateEndpoint; +const apiKey = Conf.libreTranslateApiKey; const translationProvider = Conf.translationProvider; const libreTranslate = 'libretranslate'; From fc5e9b29902efdb4d188673b671e734af6046107 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 16:58:42 -0300 Subject: [PATCH 28/64] Revert "refactor: move getConfigs() function and frontendConfig logic to 'src/utils/frontendConfig.ts'" This reverts commit ab85360d2ff5de1e3aa42e4a35aa4b640cccaf31. The discussion started publicly in Gitlab: https://gitlab.com/soapbox-pub/ditto/-/merge_requests/537#note_2148430111 Then it kept going in Element, basically the purpose of the commit is correct, but the way Patrick did it is not good. --- src/controllers/api/pleroma.ts | 32 +++++++++++++++++++++++---- src/utils/frontendConfig.ts | 40 ---------------------------------- 2 files changed, 28 insertions(+), 44 deletions(-) delete mode 100644 src/utils/frontendConfig.ts diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 2c025b8e..31d8545f 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -1,20 +1,26 @@ +import { NSchema as n, NStore } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { configSchema } from '@/schemas/pleroma-api.ts'; +import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts'; -import { getConfigs, getPleromaConfig } from '@/utils/frontendConfig.ts'; import { lookupPubkey } from '@/utils/lookup.ts'; const frontendConfigController: AppController = async (c) => { const store = await Storages.db(); - const frontendConfig = await getPleromaConfig(store, c.req.raw.signal); + const configs = await getConfigs(store, c.req.raw.signal); + const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); if (frontendConfig) { - return c.json(frontendConfig); + const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); + const data = schema.parse(frontendConfig.value).reduce>((result, [name, data]) => { + result[name.replace(/^:/, '')] = data; + return result; + }, {}); + return c.json(data); } else { return c.json({}); } @@ -64,6 +70,24 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => { return c.json({}); }; +async function getConfigs(store: NStore, signal: AbortSignal): Promise { + const { pubkey } = Conf; + + const [event] = await store.query([{ + kinds: [30078], + authors: [pubkey], + '#d': ['pub.ditto.pleroma.config'], + limit: 1, + }], { signal }); + + try { + const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); + return n.json().pipe(configSchema.array()).catch([]).parse(decrypted); + } catch (_e) { + return []; + } +} + const pleromaAdminTagSchema = z.object({ nicknames: z.string().array(), tags: z.string().array(), diff --git a/src/utils/frontendConfig.ts b/src/utils/frontendConfig.ts deleted file mode 100644 index a4f3f592..00000000 --- a/src/utils/frontendConfig.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NSchema as n, NStore } from '@nostrify/nostrify'; - -import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { Conf } from '@/config.ts'; -import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; - -export async function getPleromaConfig( - store: NStore, - signal?: AbortSignal, -): Promise> { - const configs = await getConfigs(store, signal ?? AbortSignal.timeout(1000)); - const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); - if (frontendConfig) { - const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); - const data = schema.parse(frontendConfig.value).reduce>((result, [name, data]) => { - result[name.replace(/^:/, '')] = data; - return result; - }, {}); - return data; - } - return undefined; -} - -export async function getConfigs(store: NStore, signal: AbortSignal): Promise { - const { pubkey } = Conf; - - const [event] = await store.query([{ - kinds: [30078], - authors: [pubkey], - '#d': ['pub.ditto.pleroma.config'], - limit: 1, - }], { signal }); - - try { - const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); - return n.json().pipe(configSchema.array()).catch([]).parse(decrypted); - } catch (_e) { - return []; - } -} From 49d815826cc3b1bb7b6b1852fd19ad9e991879b0 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 8 Oct 2024 17:07:05 -0300 Subject: [PATCH 29/64] refactor(languageSchema): enforce return type --- src/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index f21128d3..9adcffdd 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -41,7 +41,7 @@ const fileSchema = z.custom((value) => value instanceof File); const percentageSchema = z.coerce.number().int().gte(1).lte(100); -const languageSchema = z.string().transform((val, ctx) => { +const languageSchema = z.string().transform((val, ctx) => { val = (val.toLowerCase()).split('-')[0]; // pt-BR -> pt if (!ISO6391.validate(val)) { ctx.addIssue({ From 90e0de2cac4b021f5a3a736cbd1c28c57729320d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Oct 2024 23:23:52 -0500 Subject: [PATCH 30/64] Upgrade dompurify --- deno.json | 2 +- deno.lock | 241 +----------------------------------------------------- 2 files changed, 2 insertions(+), 241 deletions(-) diff --git a/deno.json b/deno.json index f97d4fa7..dc107e8f 100644 --- a/deno.json +++ b/deno.json @@ -68,7 +68,7 @@ "formdata-helper": "npm:formdata-helper@^0.3.0", "hono-rate-limiter": "npm:hono-rate-limiter@^0.3.0", "iso-639-1": "npm:iso-639-1@2.1.15", - "isomorphic-dompurify": "npm:isomorphic-dompurify@^2.11.0", + "isomorphic-dompurify": "npm:isomorphic-dompurify@^2.16.0", "kysely": "npm:kysely@^0.27.4", "kysely-postgres-js": "npm:kysely-postgres-js@2.0.0", "lande": "npm:lande@^1.0.10", diff --git a/deno.lock b/deno.lock index 3921afc3..a438899d 100644 --- a/deno.lock +++ b/deno.lock @@ -88,7 +88,6 @@ "npm:formdata-helper@^0.3.0": "npm:formdata-helper@0.3.0", "npm:hono-rate-limiter@^0.3.0": "npm:hono-rate-limiter@0.3.0_hono@4.2.5", "npm:iso-639-1@2.1.15": "npm:iso-639-1@2.1.15", - "npm:isomorphic-dompurify@^2.11.0": "npm:isomorphic-dompurify@2.11.0", "npm:kysely-postgres-js@2.0.0": "npm:kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4", "npm:kysely@^0.27.2": "npm:kysely@0.27.4", "npm:kysely@^0.27.3": "npm:kysely@0.27.4", @@ -704,12 +703,6 @@ "@scure/base": "@scure/base@1.1.6" } }, - "@types/dompurify@3.0.5": { - "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", - "dependencies": { - "@types/trusted-types": "@types/trusted-types@2.0.7" - } - }, "@types/node@17.0.45": { "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "dependencies": {} @@ -718,16 +711,6 @@ "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", "dependencies": {} }, - "@types/trusted-types@2.0.7": { - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dependencies": {} - }, - "agent-base@7.1.1": { - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "debug@4.3.4" - } - }, "ansi-escapes@6.2.0": { "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", "dependencies": { @@ -742,10 +725,6 @@ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dependencies": {} }, - "asynckit@0.4.0": { - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dependencies": {} - }, "bintrees@1.0.2": { "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "dependencies": {} @@ -777,12 +756,6 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dependencies": {} }, - "combined-stream@1.0.8": { - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "delayed-stream@1.0.0" - } - }, "comlink-async-generator@0.0.1": { "integrity": "sha512-RjOPv6Tb7cL9FiIgwanUJuFG9aW4myAFyyzxZoEkEegeDQrZqr92d1Njv2WIgi7nbGpTiyy5GdNTUubDaNgZ6A==", "dependencies": { @@ -809,19 +782,6 @@ "which": "which@2.0.2" } }, - "cssstyle@4.0.1": { - "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", - "dependencies": { - "rrweb-cssom": "rrweb-cssom@0.6.0" - } - }, - "data-urls@5.0.0": { - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dependencies": { - "whatwg-mimetype": "whatwg-mimetype@4.0.0", - "whatwg-url": "whatwg-url@14.0.0" - } - }, "debug@3.2.7": { "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dependencies": { @@ -834,14 +794,6 @@ "ms": "ms@2.1.2" } }, - "decimal.js@10.4.3": { - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dependencies": {} - }, - "delayed-stream@1.0.0": { - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dependencies": {} - }, "dom-serializer@2.0.0": { "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": { @@ -860,10 +812,6 @@ "domelementtype": "domelementtype@2.3.0" } }, - "dompurify@3.1.4": { - "integrity": "sha512-2gnshi6OshmuKil8rMZuQCGiUF3cUxHY3NGDzUAdUx/NPEe5DVnO8BDoAQouvgwnx0R/+a6jUn36Z0FSdq8vww==", - "dependencies": {} - }, "domutils@3.1.0": { "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dependencies": { @@ -908,14 +856,6 @@ "to-regex-range": "to-regex-range@5.0.1" } }, - "form-data@4.0.0": { - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "asynckit@0.4.0", - "combined-stream": "combined-stream@1.0.8", - "mime-types": "mime-types@2.1.35" - } - }, "formdata-helper@0.3.0": { "integrity": "sha512-QkRUFbNgWSu9lkc5TKLWri0ilTFowo950w13I5pRhj4cUxzMLuz0MIhGbE/gIRyfsZQoFeMNN0h06OCSOgfhUg==", "dependencies": {} @@ -942,12 +882,6 @@ "integrity": "sha512-uonJD3i/yy005kQ7bPZRVfG3rejYJwyPqBmPoUGijS4UB/qM+YlrZ7xzSWy+ByDu9buGHUG+f+SKzz03Y6V1Kw==", "dependencies": {} }, - "html-encoding-sniffer@4.0.0": { - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dependencies": { - "whatwg-encoding": "whatwg-encoding@3.1.1" - } - }, "htmlparser2@8.0.2": { "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dependencies": { @@ -957,20 +891,6 @@ "entities": "entities@4.5.0" } }, - "http-proxy-agent@7.0.2": { - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dependencies": { - "agent-base": "agent-base@7.1.1", - "debug": "debug@4.3.4" - } - }, - "https-proxy-agent@7.0.4": { - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dependencies": { - "agent-base": "agent-base@7.1.1", - "debug": "debug@4.3.4" - } - }, "human-signals@5.0.0": { "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dependencies": {} @@ -981,12 +901,6 @@ "safer-buffer": "safer-buffer@2.1.2" } }, - "iconv-lite@0.6.3": { - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": "safer-buffer@2.1.2" - } - }, "is-fullwidth-code-point@4.0.0": { "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dependencies": {} @@ -1001,10 +915,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dependencies": {} }, - "is-potential-custom-element-name@1.0.1": { - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dependencies": {} - }, "is-stream@3.0.0": { "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dependencies": {} @@ -1017,40 +927,6 @@ "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==", "dependencies": {} }, - "isomorphic-dompurify@2.11.0": { - "integrity": "sha512-PNGGCbbSH7+zF45UKu4Kh+yI8hm1bWA8kIZQow4KMImnjYQtrqJA0ZmwHamYUU7+M5tQ84z7xXMWmZF/v5t5eA==", - "dependencies": { - "@types/dompurify": "@types/dompurify@3.0.5", - "dompurify": "dompurify@3.1.4", - "jsdom": "jsdom@24.0.0" - } - }, - "jsdom@24.0.0": { - "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", - "dependencies": { - "cssstyle": "cssstyle@4.0.1", - "data-urls": "data-urls@5.0.0", - "decimal.js": "decimal.js@10.4.3", - "form-data": "form-data@4.0.0", - "html-encoding-sniffer": "html-encoding-sniffer@4.0.0", - "http-proxy-agent": "http-proxy-agent@7.0.2", - "https-proxy-agent": "https-proxy-agent@7.0.4", - "is-potential-custom-element-name": "is-potential-custom-element-name@1.0.1", - "nwsapi": "nwsapi@2.2.10", - "parse5": "parse5@7.1.2", - "rrweb-cssom": "rrweb-cssom@0.6.0", - "saxes": "saxes@6.0.0", - "symbol-tree": "symbol-tree@3.2.4", - "tough-cookie": "tough-cookie@4.1.4", - "w3c-xmlserializer": "w3c-xmlserializer@5.0.0", - "webidl-conversions": "webidl-conversions@7.0.0", - "whatwg-encoding": "whatwg-encoding@3.1.1", - "whatwg-mimetype": "whatwg-mimetype@4.0.0", - "whatwg-url": "whatwg-url@14.0.0", - "ws": "ws@8.17.0", - "xml-name-validator": "xml-name-validator@5.0.0" - } - }, "kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4": { "integrity": "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ==", "dependencies": { @@ -1149,16 +1025,6 @@ "picomatch": "picomatch@2.3.1" } }, - "mime-db@1.52.0": { - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dependencies": {} - }, - "mime-types@2.1.35": { - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "mime-db@1.52.0" - } - }, "mimic-fn@2.1.0": { "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dependencies": {} @@ -1219,10 +1085,6 @@ "path-key": "path-key@4.0.0" } }, - "nwsapi@2.2.10": { - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", - "dependencies": {} - }, "onetime@5.1.2": { "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dependencies": { @@ -1235,12 +1097,6 @@ "mimic-fn": "mimic-fn@4.0.0" } }, - "parse5@7.1.2": { - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "entities@4.5.0" - } - }, "path-key@3.1.1": { "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dependencies": {} @@ -1284,22 +1140,6 @@ "tdigest": "tdigest@0.1.2" } }, - "psl@1.9.0": { - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dependencies": {} - }, - "punycode@2.3.1": { - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dependencies": {} - }, - "querystringify@2.2.0": { - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dependencies": {} - }, - "requires-port@1.0.0": { - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dependencies": {} - }, "restore-cursor@4.0.0": { "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dependencies": { @@ -1311,20 +1151,10 @@ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dependencies": {} }, - "rrweb-cssom@0.6.0": { - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", - "dependencies": {} - }, "safer-buffer@2.1.2": { "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dependencies": {} }, - "saxes@6.0.0": { - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dependencies": { - "xmlchars": "xmlchars@2.2.0" - } - }, "shebang-command@2.0.0": { "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dependencies": { @@ -1379,10 +1209,6 @@ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dependencies": {} }, - "symbol-tree@3.2.4": { - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dependencies": {} - }, "tdigest@0.1.2": { "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", "dependencies": { @@ -1405,15 +1231,6 @@ "is-number": "is-number@7.0.0" } }, - "tough-cookie@4.1.4": { - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dependencies": { - "psl": "psl@1.9.0", - "punycode": "punycode@2.3.1", - "universalify": "universalify@0.2.0", - "url-parse": "url-parse@1.5.10" - } - }, "toygrad@2.6.0": { "integrity": "sha512-g4zBmlSbvzOE5FOILxYkAybTSxijKLkj1WoNqVGnbMcWDyj4wWQ+eYSr3ik7XOpIgMq/7eBcPRTJX3DM2E0YMg==", "dependencies": {} @@ -1422,12 +1239,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dependencies": {} }, - "tr46@5.0.0": { - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dependencies": { - "punycode": "punycode@2.3.1" - } - }, "tseep@1.2.1": { "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==", "dependencies": {} @@ -1450,52 +1261,14 @@ "node-fetch": "node-fetch@2.7.0" } }, - "universalify@0.2.0": { - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dependencies": {} - }, - "url-parse@1.5.10": { - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "querystringify@2.2.0", - "requires-port": "requires-port@1.0.0" - } - }, - "w3c-xmlserializer@5.0.0": { - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dependencies": { - "xml-name-validator": "xml-name-validator@5.0.0" - } - }, "webidl-conversions@3.0.1": { "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dependencies": {} }, - "webidl-conversions@7.0.0": { - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dependencies": {} - }, "websocket-ts@2.1.5": { "integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA==", "dependencies": {} }, - "whatwg-encoding@3.1.1": { - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dependencies": { - "iconv-lite": "iconv-lite@0.6.3" - } - }, - "whatwg-mimetype@4.0.0": { - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dependencies": {} - }, - "whatwg-url@14.0.0": { - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dependencies": { - "tr46": "tr46@5.0.0", - "webidl-conversions": "webidl-conversions@7.0.0" - } - }, "whatwg-url@5.0.0": { "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": { @@ -1517,18 +1290,6 @@ "strip-ansi": "strip-ansi@7.1.0" } }, - "ws@8.17.0": { - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dependencies": {} - }, - "xml-name-validator@5.0.0": { - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dependencies": {} - }, - "xmlchars@2.2.0": { - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dependencies": {} - }, "yaml@2.3.4": { "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dependencies": {} @@ -2184,7 +1945,7 @@ "npm:formdata-helper@^0.3.0", "npm:hono-rate-limiter@^0.3.0", "npm:iso-639-1@2.1.15", - "npm:isomorphic-dompurify@^2.11.0", + "npm:isomorphic-dompurify@2.16.0", "npm:kysely-postgres-js@2.0.0", "npm:kysely@^0.27.4", "npm:lande@^1.0.10", From d6b65245eae8860e1f6a77fc4ed803a82a28a2c6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 04:01:08 -0500 Subject: [PATCH 31/64] Fix dompurify in deno.lock --- deno.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.lock b/deno.lock index a438899d..fb1dfc96 100644 --- a/deno.lock +++ b/deno.lock @@ -1945,7 +1945,7 @@ "npm:formdata-helper@^0.3.0", "npm:hono-rate-limiter@^0.3.0", "npm:iso-639-1@2.1.15", - "npm:isomorphic-dompurify@2.16.0", + "npm:isomorphic-dompurify@^2.16.0", "npm:kysely-postgres-js@2.0.0", "npm:kysely@^0.27.4", "npm:lande@^1.0.10", From bf2e20f2ce83fc711b17b59a3884ad4161b1c335 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 04:02:36 -0500 Subject: [PATCH 32/64] Upgrade to Deno v2.0 --- .gitlab-ci.yml | 2 +- .tool-versions | 2 +- deno.lock | 2774 +++++++++++++++++++++-------------------- src/workers/policy.ts | 18 +- 4 files changed, 1464 insertions(+), 1332 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2ec0892f..457e62e3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: denoland/deno:2.0.0-rc.3 +image: denoland/deno:2.0.0 default: interruptible: true diff --git a/.tool-versions b/.tool-versions index 900b9e20..18a2eb04 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -deno 1.46.3 \ No newline at end of file +deno 2.0.0 \ No newline at end of file diff --git a/deno.lock b/deno.lock index fb1dfc96..0a0d9e31 100644 --- a/deno.lock +++ b/deno.lock @@ -1,1303 +1,1433 @@ { - "version": "3", - "packages": { - "specifiers": { - "jsr:@b-fuze/deno-dom@^0.1.47": "jsr:@b-fuze/deno-dom@0.1.48", - "jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.6", - "jsr:@denosaurs/plug@1.0.3": "jsr:@denosaurs/plug@1.0.3", - "jsr:@gfx/canvas-wasm@^0.4.2": "jsr:@gfx/canvas-wasm@0.4.2", - "jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0", - "jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0", - "jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0", - "jsr:@gleasonator/policy@0.4.1": "jsr:@gleasonator/policy@0.4.1", - "jsr:@gleasonator/policy@0.4.2": "jsr:@gleasonator/policy@0.4.2", - "jsr:@gleasonator/policy@0.5.0": "jsr:@gleasonator/policy@0.5.0", - "jsr:@gleasonator/policy@0.5.1": "jsr:@gleasonator/policy@0.5.1", - "jsr:@gleasonator/policy@0.5.2": "jsr:@gleasonator/policy@0.5.2", - "jsr:@gleasonator/policy@0.6.0": "jsr:@gleasonator/policy@0.6.0", - "jsr:@gleasonator/policy@0.6.1": "jsr:@gleasonator/policy@0.6.1", - "jsr:@gleasonator/policy@0.6.3": "jsr:@gleasonator/policy@0.6.3", - "jsr:@gleasonator/policy@0.6.4": "jsr:@gleasonator/policy@0.6.4", - "jsr:@gleasonator/policy@0.7.0": "jsr:@gleasonator/policy@0.7.0", - "jsr:@gleasonator/policy@0.7.1": "jsr:@gleasonator/policy@0.7.1", - "jsr:@gleasonator/policy@0.8.0": "jsr:@gleasonator/policy@0.8.0", - "jsr:@hono/hono@^4.4.6": "jsr:@hono/hono@4.6.2", - "jsr:@lambdalisue/async@^2.1.1": "jsr:@lambdalisue/async@2.1.1", - "jsr:@nostrify/db@^0.35.0": "jsr:@nostrify/db@0.35.0", - "jsr:@nostrify/nostrify@^0.22.1": "jsr:@nostrify/nostrify@0.22.5", - "jsr:@nostrify/nostrify@^0.22.4": "jsr:@nostrify/nostrify@0.22.4", - "jsr:@nostrify/nostrify@^0.22.5": "jsr:@nostrify/nostrify@0.22.5", - "jsr:@nostrify/nostrify@^0.31.0": "jsr:@nostrify/nostrify@0.31.0", - "jsr:@nostrify/nostrify@^0.32.0": "jsr:@nostrify/nostrify@0.32.0", - "jsr:@nostrify/nostrify@^0.35.0": "jsr:@nostrify/nostrify@0.35.0", - "jsr:@nostrify/nostrify@^0.36.0": "jsr:@nostrify/nostrify@0.36.0", - "jsr:@nostrify/policies@^0.33.0": "jsr:@nostrify/policies@0.33.0", - "jsr:@nostrify/policies@^0.33.1": "jsr:@nostrify/policies@0.33.1", - "jsr:@nostrify/policies@^0.34.0": "jsr:@nostrify/policies@0.34.0", - "jsr:@nostrify/policies@^0.35.0": "jsr:@nostrify/policies@0.35.0", - "jsr:@nostrify/policies@^0.36.0": "jsr:@nostrify/policies@0.36.0", - "jsr:@nostrify/types@^0.30.0": "jsr:@nostrify/types@0.30.1", - "jsr:@nostrify/types@^0.30.1": "jsr:@nostrify/types@0.30.1", - "jsr:@nostrify/types@^0.35.0": "jsr:@nostrify/types@0.35.0", - "jsr:@soapbox/kysely-pglite@^1.0.0": "jsr:@soapbox/kysely-pglite@1.0.0", - "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", - "jsr:@std/assert@^0.213.1": "jsr:@std/assert@0.213.1", - "jsr:@std/assert@^0.223.0": "jsr:@std/assert@0.223.0", - "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", - "jsr:@std/assert@^0.225.1": "jsr:@std/assert@0.225.3", - "jsr:@std/bytes@^0.223.0": "jsr:@std/bytes@0.223.0", - "jsr:@std/bytes@^0.224.0": "jsr:@std/bytes@0.224.0", - "jsr:@std/bytes@^1.0.0-rc.3": "jsr:@std/bytes@1.0.0", - "jsr:@std/bytes@^1.0.1-rc.3": "jsr:@std/bytes@1.0.2", - "jsr:@std/bytes@^1.0.2": "jsr:@std/bytes@1.0.2", - "jsr:@std/bytes@^1.0.2-rc.3": "jsr:@std/bytes@1.0.2", - "jsr:@std/cli@^0.223.0": "jsr:@std/cli@0.223.0", - "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", - "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.2", - "jsr:@std/encoding@0.213.1": "jsr:@std/encoding@0.213.1", - "jsr:@std/encoding@1.0.5": "jsr:@std/encoding@1.0.5", - "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3", - "jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3", - "jsr:@std/fmt@0.213.1": "jsr:@std/fmt@0.213.1", - "jsr:@std/fs@0.213.1": "jsr:@std/fs@0.213.1", - "jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3", - "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.4", - "jsr:@std/io@^0.223.0": "jsr:@std/io@0.223.0", - "jsr:@std/io@^0.224": "jsr:@std/io@0.224.8", - "jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0", - "jsr:@std/media-types@^0.224.1": "jsr:@std/media-types@0.224.1", - "jsr:@std/path@0.213.1": "jsr:@std/path@0.213.1", - "jsr:@std/path@1.0.0-rc.1": "jsr:@std/path@1.0.0-rc.1", - "jsr:@std/path@^0.213.1": "jsr:@std/path@0.213.1", - "jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0", - "npm:@electric-sql/pglite@^0.2.8": "npm:@electric-sql/pglite@0.2.8", - "npm:@isaacs/ttlcache@^1.4.1": "npm:@isaacs/ttlcache@1.4.1", - "npm:@noble/hashes@^1.4.0": "npm:@noble/hashes@1.4.0", - "npm:@noble/secp256k1@^2.0.0": "npm:@noble/secp256k1@2.1.0", - "npm:@scure/base@^1.1.6": "npm:@scure/base@1.1.6", - "npm:@scure/bip32@^1.4.0": "npm:@scure/bip32@1.4.0", - "npm:@scure/bip32@^1.5.0": "npm:@scure/bip32@1.5.0", - "npm:@scure/bip39@^1.3.0": "npm:@scure/bip39@1.3.0", - "npm:@types/node": "npm:@types/node@18.16.19", - "npm:comlink-async-generator": "npm:comlink-async-generator@0.0.1", - "npm:comlink-async-generator@^0.0.1": "npm:comlink-async-generator@0.0.1", - "npm:comlink@^4.4.1": "npm:comlink@4.4.1", - "npm:commander@12.1.0": "npm:commander@12.1.0", - "npm:entities@^4.5.0": "npm:entities@4.5.0", - "npm:fast-stable-stringify@^1.0.0": "npm:fast-stable-stringify@1.0.0", - "npm:formdata-helper@^0.3.0": "npm:formdata-helper@0.3.0", - "npm:hono-rate-limiter@^0.3.0": "npm:hono-rate-limiter@0.3.0_hono@4.2.5", - "npm:iso-639-1@2.1.15": "npm:iso-639-1@2.1.15", - "npm:kysely-postgres-js@2.0.0": "npm:kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4", - "npm:kysely@^0.27.2": "npm:kysely@0.27.4", - "npm:kysely@^0.27.3": "npm:kysely@0.27.4", - "npm:kysely@^0.27.4": "npm:kysely@0.27.4", - "npm:lande@^1.0.10": "npm:lande@1.0.10", - "npm:light-bolt11-decoder": "npm:light-bolt11-decoder@3.1.1", - "npm:linkify-plugin-hashtag@^4.1.1": "npm:linkify-plugin-hashtag@4.1.3_linkifyjs@4.1.3", - "npm:linkify-string@^4.1.1": "npm:linkify-string@4.1.3_linkifyjs@4.1.3", - "npm:linkifyjs@^4.1.1": "npm:linkifyjs@4.1.3", - "npm:lint-staged": "npm:lint-staged@15.2.2", - "npm:lru-cache@^10.2.0": "npm:lru-cache@10.2.2", - "npm:lru-cache@^10.2.2": "npm:lru-cache@10.2.2", - "npm:nostr-tools@2.5.1": "npm:nostr-tools@2.5.1", - "npm:nostr-tools@^2.5.0": "npm:nostr-tools@2.5.1", - "npm:nostr-tools@^2.7.0": "npm:nostr-tools@2.7.0", - "npm:nostr-wasm@^0.1.0": "npm:nostr-wasm@0.1.0", - "npm:path-to-regexp@^7.1.0": "npm:path-to-regexp@7.1.0", - "npm:png-to-ico@^2.1.8": "npm:png-to-ico@2.1.8", - "npm:postgres@3.4.4": "npm:postgres@3.4.4", - "npm:prom-client@^15.1.2": "npm:prom-client@15.1.2", - "npm:tldts@^6.0.14": "npm:tldts@6.1.18", - "npm:tseep@^1.2.1": "npm:tseep@1.2.1", - "npm:type-fest@^4.3.0": "npm:type-fest@4.18.2", - "npm:unfurl.js@^6.4.0": "npm:unfurl.js@6.4.0", - "npm:websocket-ts@^2.1.5": "npm:websocket-ts@2.1.5", - "npm:zod@^3.23.8": "npm:zod@3.23.8" + "version": "4", + "specifiers": { + "jsr:@b-fuze/deno-dom@~0.1.47": "0.1.48", + "jsr:@bradenmacdonald/s3-lite-client@~0.7.4": "0.7.6", + "jsr:@denosaurs/plug@1.0.3": "1.0.3", + "jsr:@gfx/canvas-wasm@~0.4.2": "0.4.2", + "jsr:@gleasonator/policy@*": "0.2.0", + "jsr:@gleasonator/policy@0.2.0": "0.2.0", + "jsr:@gleasonator/policy@0.4.0": "0.4.0", + "jsr:@gleasonator/policy@0.4.1": "0.4.1", + "jsr:@gleasonator/policy@0.4.2": "0.4.2", + "jsr:@gleasonator/policy@0.5.0": "0.5.0", + "jsr:@gleasonator/policy@0.5.1": "0.5.1", + "jsr:@gleasonator/policy@0.5.2": "0.5.2", + "jsr:@gleasonator/policy@0.6.0": "0.6.0", + "jsr:@gleasonator/policy@0.6.1": "0.6.1", + "jsr:@gleasonator/policy@0.6.3": "0.6.3", + "jsr:@gleasonator/policy@0.6.4": "0.6.4", + "jsr:@gleasonator/policy@0.7.0": "0.7.0", + "jsr:@gleasonator/policy@0.7.1": "0.7.1", + "jsr:@gleasonator/policy@0.8.0": "0.8.0", + "jsr:@hono/hono@^4.4.6": "4.6.2", + "jsr:@lambdalisue/async@^2.1.1": "2.1.1", + "jsr:@nostrify/db@0.35": "0.35.0", + "jsr:@nostrify/nostrify@0.31": "0.31.0", + "jsr:@nostrify/nostrify@0.32": "0.32.0", + "jsr:@nostrify/nostrify@0.35": "0.35.0", + "jsr:@nostrify/nostrify@0.36": "0.36.0", + "jsr:@nostrify/nostrify@~0.22.1": "0.22.5", + "jsr:@nostrify/nostrify@~0.22.4": "0.22.4", + "jsr:@nostrify/nostrify@~0.22.5": "0.22.5", + "jsr:@nostrify/policies@0.33": "0.33.0", + "jsr:@nostrify/policies@0.34": "0.34.0", + "jsr:@nostrify/policies@0.35": "0.35.0", + "jsr:@nostrify/policies@0.36": "0.36.0", + "jsr:@nostrify/policies@~0.33.1": "0.33.1", + "jsr:@nostrify/types@0.30": "0.30.1", + "jsr:@nostrify/types@0.35": "0.35.0", + "jsr:@nostrify/types@~0.30.1": "0.30.1", + "jsr:@soapbox/kysely-pglite@1": "1.0.0", + "jsr:@soapbox/stickynotes@0.4": "0.4.0", + "jsr:@std/assert@0.223": "0.223.0", + "jsr:@std/assert@0.224": "0.224.0", + "jsr:@std/assert@~0.213.1": "0.213.1", + "jsr:@std/assert@~0.225.1": "0.225.3", + "jsr:@std/bytes@0.223": "0.223.0", + "jsr:@std/bytes@0.224": "0.224.0", + "jsr:@std/bytes@^1.0.0-rc.3": "1.0.0", + "jsr:@std/bytes@^1.0.1-rc.3": "1.0.2", + "jsr:@std/bytes@^1.0.2": "1.0.2", + "jsr:@std/bytes@^1.0.2-rc.3": "1.0.2", + "jsr:@std/cli@0.223": "0.223.0", + "jsr:@std/crypto@0.224": "0.224.0", + "jsr:@std/dotenv@0.224": "0.224.2", + "jsr:@std/encoding@0.213.1": "0.213.1", + "jsr:@std/encoding@0.224": "0.224.3", + "jsr:@std/encoding@1.0.5": "1.0.5", + "jsr:@std/encoding@~0.224.1": "0.224.3", + "jsr:@std/fmt@0.213.1": "0.213.1", + "jsr:@std/fs@0.213.1": "0.213.1", + "jsr:@std/fs@~0.229.3": "0.229.3", + "jsr:@std/internal@1": "1.0.4", + "jsr:@std/io@0.223": "0.223.0", + "jsr:@std/io@0.224": "0.224.8", + "jsr:@std/json@0.223": "0.223.0", + "jsr:@std/media-types@~0.224.1": "0.224.1", + "jsr:@std/path@0.213.1": "0.213.1", + "jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1", + "jsr:@std/path@~0.213.1": "0.213.1", + "jsr:@std/streams@0.223": "0.223.0", + "npm:@electric-sql/pglite@~0.2.8": "0.2.8", + "npm:@isaacs/ttlcache@^1.4.1": "1.4.1", + "npm:@noble/hashes@^1.4.0": "1.4.0", + "npm:@noble/secp256k1@2": "2.1.0", + "npm:@scure/base@^1.1.6": "1.1.6", + "npm:@scure/bip32@^1.4.0": "1.4.0", + "npm:@scure/bip32@^1.5.0": "1.5.0", + "npm:@scure/bip39@^1.3.0": "1.3.0", + "npm:@types/node@*": "18.16.19", + "npm:comlink-async-generator@*": "0.0.1", + "npm:comlink-async-generator@^0.0.1": "0.0.1", + "npm:comlink@^4.4.1": "4.4.1", + "npm:commander@12.1.0": "12.1.0", + "npm:entities@^4.5.0": "4.5.0", + "npm:fast-stable-stringify@1": "1.0.0", + "npm:formdata-helper@0.3": "0.3.0", + "npm:hono-rate-limiter@0.3": "0.3.0_hono@4.2.5", + "npm:iso-639-1@2.1.15": "2.1.15", + "npm:isomorphic-dompurify@^2.16.0": "2.16.0", + "npm:kysely-postgres-js@2.0.0": "2.0.0_kysely@0.27.3_postgres@3.4.4", + "npm:kysely@~0.27.2": "0.27.4", + "npm:kysely@~0.27.3": "0.27.4", + "npm:kysely@~0.27.4": "0.27.4", + "npm:lande@^1.0.10": "1.0.10", + "npm:light-bolt11-decoder@*": "3.1.1", + "npm:linkify-plugin-hashtag@^4.1.1": "4.1.3_linkifyjs@4.1.3", + "npm:linkify-string@^4.1.1": "4.1.3_linkifyjs@4.1.3", + "npm:linkifyjs@^4.1.1": "4.1.3", + "npm:lint-staged@*": "15.2.2", + "npm:lru-cache@^10.2.0": "10.2.2", + "npm:lru-cache@^10.2.2": "10.2.2", + "npm:nostr-tools@2.5.1": "2.5.1", + "npm:nostr-tools@^2.5.0": "2.5.1", + "npm:nostr-tools@^2.7.0": "2.7.0", + "npm:nostr-wasm@0.1": "0.1.0", + "npm:path-to-regexp@^7.1.0": "7.1.0", + "npm:png-to-ico@^2.1.8": "2.1.8", + "npm:postgres@3.4.4": "3.4.4", + "npm:prom-client@^15.1.2": "15.1.2", + "npm:tldts@^6.0.14": "6.1.18", + "npm:tseep@^1.2.1": "1.2.1", + "npm:type-fest@^4.3.0": "4.18.2", + "npm:unfurl.js@^6.4.0": "6.4.0", + "npm:websocket-ts@^2.1.5": "2.1.5", + "npm:zod@^3.23.8": "3.23.8" + }, + "jsr": { + "@b-fuze/deno-dom@0.1.47": { + "integrity": "270a888de91329f8ce3849211ece0ad97ce1e8b9a8a774f2bed2f43c8b0ffe8e", + "dependencies": [ + "jsr:@denosaurs/plug" + ] }, - "jsr": { - "@b-fuze/deno-dom@0.1.47": { - "integrity": "270a888de91329f8ce3849211ece0ad97ce1e8b9a8a774f2bed2f43c8b0ffe8e", - "dependencies": [ - "jsr:@denosaurs/plug@1.0.3" - ] - }, - "@b-fuze/deno-dom@0.1.48": { - "integrity": "bf5b591aef2e9e9c59adfcbb93a9ecd45bab5b7c8263625beafa5c8f1662e7da" - }, - "@bradenmacdonald/s3-lite-client@0.7.6": { - "integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1", - "dependencies": [ - "jsr:@std/io@^0.224" - ] - }, - "@denosaurs/plug@1.0.3": { - "integrity": "b010544e386bea0ff3a1d05e0c88f704ea28cbd4d753439c2f1ee021a85d4640", - "dependencies": [ - "jsr:@std/encoding@0.213.1", - "jsr:@std/fmt@0.213.1", - "jsr:@std/fs@0.213.1", - "jsr:@std/path@0.213.1" - ] - }, - "@gfx/canvas-wasm@0.4.2": { - "integrity": "d653be3bd12cb2fa9bbe5d1b1f041a81b91d80b68502761204aaf60e4592532a", - "dependencies": [ - "jsr:@std/encoding@1.0.5" - ] - }, - "@gleasonator/policy@0.2.0": { - "integrity": "3fe58b853ab203b2b67e65b64391dbcf5c07bc1caaf46e97b2f8ed5b14f30fdf", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.22.1" - ] - }, - "@gleasonator/policy@0.4.0": { - "integrity": "59c2f3ab1dc663e99a3e10b7eb69bf9fe581ce5d428fe56653e38f7f961da5ea", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.22.1" - ] - }, - "@gleasonator/policy@0.4.1": { - "integrity": "4d42d11d2e9b5f183cec1ca73dbb6f5cd50475efb2bef9495857fa8e5e5d8251", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.22.1" - ] - }, - "@gleasonator/policy@0.4.2": { - "integrity": "704527346b35a1ef799c58ba365fea30d1d4bb8e5291937183223d27b24b0f27", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.22.1" - ] - }, - "@gleasonator/policy@0.5.0": { - "integrity": "c2882eb3b4147dfe96b6ec2870b012b5a614f686770d1d4b2f778fdc44e8b1f5", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.31.0", - "jsr:@nostrify/policies@^0.33.0" - ] - }, - "@gleasonator/policy@0.5.1": { - "integrity": "2d687c5166556ce13ac05c4542f61ef8a47d8b96b57f6e43d52035805f895551", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.31.0", - "jsr:@nostrify/policies@^0.33.0" - ] - }, - "@gleasonator/policy@0.5.2": { - "integrity": "cdd3add87be3132eb05736bca640dfb3bbb1aa79928a44d3563cde20bab7c0d3", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.31.0", - "jsr:@nostrify/policies@^0.33.1" - ] - }, - "@gleasonator/policy@0.6.0": { - "integrity": "77f52bb245255a61070a4970c50e2ea8e82345c1de2fef12b9d8887a20b46e6d", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.32.0", - "jsr:@nostrify/policies@^0.34.0" - ] - }, - "@gleasonator/policy@0.6.1": { - "integrity": "ba763d69332a736678b068b4063709874bc64010dfc3f974818218a41deb2291", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.32.0", - "jsr:@nostrify/policies@^0.34.0" - ] - }, - "@gleasonator/policy@0.6.3": { - "integrity": "7126c52edd3de21488714e66ec71f31ba9b14f8afc761ab73ac7c3ecc936625c", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.32.0", - "jsr:@nostrify/policies@^0.34.0" - ] - }, - "@gleasonator/policy@0.6.4": { - "integrity": "fd91c94546edd1de1faa80cb3248699b2f010ef1bdd89818dbc4a03e7606e0bb", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.32.0", - "jsr:@nostrify/policies@^0.34.0" - ] - }, - "@gleasonator/policy@0.7.0": { - "integrity": "22cad69f6c0eaa20ccd45fcbd0a3990c9e395f23181669ebbf397b8c501d14cf", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.36.0", - "jsr:@nostrify/policies@^0.36.0" - ] - }, - "@gleasonator/policy@0.7.1": { - "integrity": "411c106ec8594f6b1c6aa716895600f2483f72367862bd80add91a0bfec94c28", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.36.0", - "jsr:@nostrify/policies@^0.36.0" - ] - }, - "@gleasonator/policy@0.8.0": { - "integrity": "139611066eb60f15ec40686f9c9b8bad13eb631fdd069fd6eaae3ccf27157b0d", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.36.0", - "jsr:@nostrify/policies@^0.36.0" - ] - }, - "@hono/hono@4.4.6": { - "integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453" - }, - "@hono/hono@4.5.0": { - "integrity": "4a410f7773ac4b5b0eb4520b26c7ab7795a271d57a9df7fa1953ded6b90ccaf7" - }, - "@hono/hono@4.5.1": { - "integrity": "459748ed4d4146c6e4bdff0213ff1ac44749904066ae02e7550d6c7f28c9bc4c" - }, - "@hono/hono@4.5.11": { - "integrity": "5bd6b1a3a503efb746fdcf0aae3ac536dd09229d372988bde5db0798ef64ae4f" - }, - "@hono/hono@4.5.3": { - "integrity": "429923b2b3c6586a1450862328d61a1346fee5841e8ae86c494250475057213c" - }, - "@hono/hono@4.5.4": { - "integrity": "3792780b8460d5df0959b07c059db9325e4fa1a49f8b5aff7ab9bc870bdec8e3" - }, - "@hono/hono@4.5.5": { - "integrity": "e5a63b5f535475cd80974b65fed23a138d0cbb91fe1cc9a17a7c7278e835c308" - }, - "@hono/hono@4.5.9": { - "integrity": "47f561e67aedbd6d1e21e3a1ae26c1b80ffdb62a51c161d502e75bee17ca40af" - }, - "@hono/hono@4.6.2": { - "integrity": "35fcf3be4687825080b01bed7bbe2ac66f8d8b8939f0bad459661bf3b46d916f" - }, - "@lambdalisue/async@2.1.1": { - "integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4" - }, - "@nostrify/db@0.35.0": { - "integrity": "637191c41812544e361b7997dc44ea098f8bd7efebb28f37a8a7142a0ecada8d", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.35.0", - "jsr:@nostrify/types@^0.35.0", - "npm:kysely@^0.27.3", - "npm:nostr-tools@^2.7.0" - ] - }, - "@nostrify/nostrify@0.22.4": { - "integrity": "1c8a7847e5773213044b491e85fd7cafae2ad194ce59da4d957d2b27c776b42d", - "dependencies": [ - "jsr:@std/encoding@^0.224.1", - "npm:@noble/hashes@^1.4.0", - "npm:@scure/base@^1.1.6", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:kysely@^0.27.3", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.5.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/nostrify@0.22.5": { - "integrity": "5b9c17325cc02e37c71e14ac0103b40446b0402fe183e5f5362af23e9ea162bf", - "dependencies": [ - "jsr:@std/encoding@^0.224.1", - "npm:@scure/base@^1.1.6", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:kysely@^0.27.3", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.7.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/nostrify@0.30.0": { - "integrity": "7c29e7d8b5a0a81e238170ac1e7ad708bc72dd8f478d8d82c30598fb4eff9b9c", - "dependencies": [ - "jsr:@nostrify/types@^0.30.0", - "jsr:@std/crypto@^0.224.0", - "jsr:@std/encoding@^0.224.1", - "npm:@scure/base@^1.1.6", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.7.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/nostrify@0.31.0": { - "integrity": "1c1b686bb9ca3ad8d19807e3b96ef3793a65d70fd0f433fe6ef8b3fdb9f45557", - "dependencies": [ - "jsr:@nostrify/types@^0.30.1", - "jsr:@std/encoding@^0.224.1", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.7.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/nostrify@0.32.0": { - "integrity": "2d3b7a9cce275c150355f8e566c11f14044afd0b889afcb48e883da9467bdaa9", - "dependencies": [ - "jsr:@nostrify/types@^0.30.1", - "jsr:@std/encoding@^0.224.1", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.7.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/nostrify@0.35.0": { - "integrity": "9bfef4883838b8b4cb2e2b28a60b72de95391ca5b789bc7206a2baea054dea55", - "dependencies": [ - "jsr:@nostrify/types@^0.35.0", - "jsr:@std/encoding@^0.224.1", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.7.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/nostrify@0.36.0": { - "integrity": "f00dbff1f02a2c496c5e85eeeb7a84101b7dd874d87456449dc71b6d037e40fc", - "dependencies": [ - "jsr:@nostrify/types@^0.35.0", - "jsr:@std/crypto@^0.224.0", - "jsr:@std/encoding@^0.224.1", - "npm:@scure/base@^1.1.6", - "npm:@scure/bip32@^1.4.0", - "npm:@scure/bip39@^1.3.0", - "npm:lru-cache@^10.2.0", - "npm:nostr-tools@^2.7.0", - "npm:websocket-ts@^2.1.5", - "npm:zod@^3.23.8" - ] - }, - "@nostrify/policies@0.33.0": { - "integrity": "c946b06d0527298b4d7c9819d142a10f522ba09eee76c37525aa4acfc5d87aee", - "dependencies": [ - "jsr:@nostrify/types@^0.30.1", - "npm:nostr-tools@^2.7.0" - ] - }, - "@nostrify/policies@0.33.1": { - "integrity": "381e1f9406a6da22da03a254e46b1aa07d5491b9761961cda3a4aeb5bf3f5286", - "dependencies": [ - "jsr:@nostrify/types@^0.30.1", - "npm:nostr-tools@^2.7.0" - ] - }, - "@nostrify/policies@0.34.0": { - "integrity": "27eb8fb36106a29e982ec7fc6bbb91bd6989f8ce11113a3ef6c528b4c2deceee", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.32.0", - "jsr:@nostrify/types@^0.30.1", - "npm:nostr-tools@^2.7.0" - ] - }, - "@nostrify/policies@0.35.0": { - "integrity": "b828fac9f253e460a9587c05588b7dae6a0a32c5a9c9083e449219887b9e8e20", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.35.0", - "jsr:@nostrify/types@^0.35.0", - "npm:nostr-tools@^2.7.0" - ] - }, - "@nostrify/policies@0.36.0": { - "integrity": "ad1930de48ce03cdf34da456af1563b487581d1d86683cd416ad760ae40b1fb3", - "dependencies": [ - "jsr:@nostrify/nostrify@^0.36.0", - "jsr:@nostrify/types@^0.35.0", - "npm:nostr-tools@^2.7.0" - ] - }, - "@nostrify/types@0.30.0": { - "integrity": "1f38fa849cff930bd709edbf94ef9ac02f46afb8b851f86c8736517b354616da" - }, - "@nostrify/types@0.30.1": { - "integrity": "245da176f6893a43250697db51ad32bfa29bf9b1cdc1ca218043d9abf6de5ae5" - }, - "@nostrify/types@0.35.0": { - "integrity": "b8d515563d467072694557d5626fa1600f74e83197eef45dd86a9a99c64f7fe6" - }, - "@soapbox/kysely-pglite@1.0.0": { - "integrity": "0954b1bf3deab051c479cba966b1e6ed5a0a966aa21d1f40143ec8f5efcd475d", - "dependencies": [ - "npm:kysely@^0.27.4" - ] - }, - "@soapbox/stickynotes@0.4.0": { - "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" - }, - "@std/assert@0.213.1": { - "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe" - }, - "@std/assert@0.223.0": { - "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" - }, - "@std/assert@0.224.0": { - "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" - }, - "@std/assert@0.225.3": { - "integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f", - "dependencies": [ - "jsr:@std/internal@^1.0.0" - ] - }, - "@std/bytes@0.223.0": { - "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" - }, - "@std/bytes@0.224.0": { - "integrity": "a2250e1d0eb7d1c5a426f21267ab9bdeac2447fa87a3d0d1a467d3f7a6058e49" - }, - "@std/bytes@1.0.0": { - "integrity": "9392e72af80adccaa1197912fa19990ed091cb98d5c9c4344b0c301b22d7c632" - }, - "@std/bytes@1.0.2": { - "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" - }, - "@std/cli@0.223.0": { - "integrity": "2feb7970f2028904c3edc22ea916ce9538113dfc170844f3eae03578c333c356", - "dependencies": [ - "jsr:@std/assert@^0.223.0" - ] - }, - "@std/crypto@0.224.0": { - "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", - "dependencies": [ - "jsr:@std/assert@^0.224.0", - "jsr:@std/encoding@^0.224.0" - ] - }, - "@std/dotenv@0.224.0": { - "integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d" - }, - "@std/dotenv@0.224.2": { - "integrity": "29081695357e4534696c9e986b2560be29c141ccf52daa32b6c20ff5b5c64ab9" - }, - "@std/encoding@0.213.1": { - "integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62" - }, - "@std/encoding@0.224.3": { - "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" - }, - "@std/encoding@1.0.5": { - "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" - }, - "@std/fmt@0.213.1": { - "integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3" - }, - "@std/fs@0.213.1": { - "integrity": "fbcaf099f8a85c27ab0712b666262cda8fe6d02e9937bf9313ecaea39a22c501", - "dependencies": [ - "jsr:@std/assert@^0.213.1", - "jsr:@std/path@^0.213.1" - ] - }, - "@std/fs@0.229.3": { - "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb", - "dependencies": [ - "jsr:@std/path@1.0.0-rc.1" - ] - }, - "@std/internal@1.0.0": { - "integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a" - }, - "@std/internal@1.0.1": { - "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" - }, - "@std/internal@1.0.3": { - "integrity": "208e9b94a3d5649bd880e9ca38b885ab7651ab5b5303a56ed25de4755fb7b11e" - }, - "@std/internal@1.0.4": { - "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" - }, - "@std/io@0.223.0": { - "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", - "dependencies": [ - "jsr:@std/assert@^0.223.0", - "jsr:@std/bytes@^0.223.0" - ] - }, - "@std/io@0.224.0": { - "integrity": "0aff885d21d829c050b8a08b1d71b54aed5841aecf227f8d77e99ec529a11e8e", - "dependencies": [ - "jsr:@std/bytes@^0.224.0" - ] - }, - "@std/io@0.224.1": { - "integrity": "73de242551a5c0965eb33e36b1fc7df4834ffbc836a1a643a410ccd11253d6be", - "dependencies": [ - "jsr:@std/bytes@^1.0.0-rc.3" - ] - }, - "@std/io@0.224.3": { - "integrity": "b402edeb99c6b3778d9ae3e9927bc9085b170b41e5a09bbb7064ab2ee394ae2f", - "dependencies": [ - "jsr:@std/bytes@^1.0.1-rc.3" - ] - }, - "@std/io@0.224.4": { - "integrity": "bce1151765e4e70e376039fd72c71672b4d4aae363878a5ee3e58361b81197ec", - "dependencies": [ - "jsr:@std/bytes@^1.0.2-rc.3" - ] - }, - "@std/io@0.224.6": { - "integrity": "eefe034a370be34daf066c8634dd645635d099bb21eccf110f0bdc28d9040891", - "dependencies": [ - "jsr:@std/bytes@^1.0.2" - ] - }, - "@std/io@0.224.7": { - "integrity": "a70848793c44a7c100926571a8c9be68ba85487bfcd4d0540d86deabe1123dc9", - "dependencies": [ - "jsr:@std/bytes@^1.0.2" - ] - }, - "@std/io@0.224.8": { - "integrity": "f525d05d51fd873de6352b9afcf35cab9ab5dc448bf3c20e0c8b521ded9be392", - "dependencies": [ - "jsr:@std/bytes@^1.0.2" - ] - }, - "@std/json@0.223.0": { - "integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f", - "dependencies": [ - "jsr:@std/streams@^0.223.0" - ] - }, - "@std/media-types@0.224.1": { - "integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1" - }, - "@std/path@0.213.1": { - "integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673", - "dependencies": [ - "jsr:@std/assert@^0.213.1" - ] - }, - "@std/path@1.0.0-rc.1": { - "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" - }, - "@std/streams@0.223.0": { - "integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99", - "dependencies": [ - "jsr:@std/assert@^0.223.0", - "jsr:@std/bytes@^0.223.0", - "jsr:@std/io@^0.223.0" - ] - } + "@b-fuze/deno-dom@0.1.48": { + "integrity": "bf5b591aef2e9e9c59adfcbb93a9ecd45bab5b7c8263625beafa5c8f1662e7da" }, - "npm": { - "@electric-sql/pglite@0.2.8": { - "integrity": "sha512-0wSmQu22euBRzR5ghqyIHnBH4MfwlkL5WstOrrA3KOsjEWEglvoL/gH92JajEUA6Ufei/+qbkB2hVloC/K/RxQ==", - "dependencies": {} - }, - "@isaacs/ttlcache@1.4.1": { - "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", - "dependencies": {} - }, - "@noble/ciphers@0.5.3": { - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", - "dependencies": {} - }, - "@noble/curves@1.1.0": { - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", - "dependencies": { - "@noble/hashes": "@noble/hashes@1.3.1" - } - }, - "@noble/curves@1.2.0": { - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "@noble/hashes@1.3.2" - } - }, - "@noble/curves@1.4.0": { - "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", - "dependencies": { - "@noble/hashes": "@noble/hashes@1.4.0" - } - }, - "@noble/curves@1.6.0": { - "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", - "dependencies": { - "@noble/hashes": "@noble/hashes@1.5.0" - } - }, - "@noble/hashes@1.3.1": { - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", - "dependencies": {} - }, - "@noble/hashes@1.3.2": { - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "dependencies": {} - }, - "@noble/hashes@1.4.0": { - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "dependencies": {} - }, - "@noble/hashes@1.5.0": { - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", - "dependencies": {} - }, - "@noble/secp256k1@2.1.0": { - "integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==", - "dependencies": {} - }, - "@opentelemetry/api@1.9.0": { - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "dependencies": {} - }, - "@scure/base@1.1.1": { - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "dependencies": {} - }, - "@scure/base@1.1.6": { - "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", - "dependencies": {} - }, - "@scure/base@1.1.9": { - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "dependencies": {} - }, - "@scure/bip32@1.3.1": { - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", - "dependencies": { - "@noble/curves": "@noble/curves@1.1.0", - "@noble/hashes": "@noble/hashes@1.3.2", - "@scure/base": "@scure/base@1.1.6" - } - }, - "@scure/bip32@1.4.0": { - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", - "dependencies": { - "@noble/curves": "@noble/curves@1.4.0", - "@noble/hashes": "@noble/hashes@1.4.0", - "@scure/base": "@scure/base@1.1.6" - } - }, - "@scure/bip32@1.5.0": { - "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", - "dependencies": { - "@noble/curves": "@noble/curves@1.6.0", - "@noble/hashes": "@noble/hashes@1.5.0", - "@scure/base": "@scure/base@1.1.9" - } - }, - "@scure/bip39@1.2.1": { - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "dependencies": { - "@noble/hashes": "@noble/hashes@1.3.2", - "@scure/base": "@scure/base@1.1.6" - } - }, - "@scure/bip39@1.3.0": { - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", - "dependencies": { - "@noble/hashes": "@noble/hashes@1.4.0", - "@scure/base": "@scure/base@1.1.6" - } - }, - "@types/node@17.0.45": { - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", - "dependencies": {} - }, - "@types/node@18.16.19": { - "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", - "dependencies": {} - }, - "ansi-escapes@6.2.0": { - "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", - "dependencies": { - "type-fest": "type-fest@3.13.1" - } - }, - "ansi-regex@6.0.1": { - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dependencies": {} - }, - "ansi-styles@6.2.1": { - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dependencies": {} - }, - "bintrees@1.0.2": { - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "dependencies": {} - }, - "braces@3.0.2": { - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "fill-range@7.0.1" - } - }, - "chalk@5.3.0": { - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dependencies": {} - }, - "cli-cursor@4.0.0": { - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dependencies": { - "restore-cursor": "restore-cursor@4.0.0" - } - }, - "cli-truncate@4.0.0": { - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dependencies": { - "slice-ansi": "slice-ansi@5.0.0", - "string-width": "string-width@7.1.0" - } - }, - "colorette@2.0.20": { - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dependencies": {} - }, - "comlink-async-generator@0.0.1": { - "integrity": "sha512-RjOPv6Tb7cL9FiIgwanUJuFG9aW4myAFyyzxZoEkEegeDQrZqr92d1Njv2WIgi7nbGpTiyy5GdNTUubDaNgZ6A==", - "dependencies": { - "comlink": "comlink@4.4.1" - } - }, - "comlink@4.4.1": { - "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==", - "dependencies": {} - }, - "commander@11.1.0": { - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dependencies": {} - }, - "commander@12.1.0": { - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dependencies": {} - }, - "cross-spawn@7.0.3": { - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "path-key@3.1.1", - "shebang-command": "shebang-command@2.0.0", - "which": "which@2.0.2" - } - }, - "debug@3.2.7": { - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "ms@2.1.3" - } - }, - "debug@4.3.4": { - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "ms@2.1.2" - } - }, - "dom-serializer@2.0.0": { - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "domelementtype@2.3.0", - "domhandler": "domhandler@5.0.3", - "entities": "entities@4.5.0" - } - }, - "domelementtype@2.3.0": { - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dependencies": {} - }, - "domhandler@5.0.3": { - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "domelementtype@2.3.0" - } - }, - "domutils@3.1.0": { - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "dom-serializer@2.0.0", - "domelementtype": "domelementtype@2.3.0", - "domhandler": "domhandler@5.0.3" - } - }, - "emoji-regex@10.3.0": { - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dependencies": {} - }, - "entities@4.5.0": { - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dependencies": {} - }, - "eventemitter3@5.0.1": { - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dependencies": {} - }, - "execa@8.0.1": { - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dependencies": { - "cross-spawn": "cross-spawn@7.0.3", - "get-stream": "get-stream@8.0.1", - "human-signals": "human-signals@5.0.0", - "is-stream": "is-stream@3.0.0", - "merge-stream": "merge-stream@2.0.0", - "npm-run-path": "npm-run-path@5.3.0", - "onetime": "onetime@6.0.0", - "signal-exit": "signal-exit@4.1.0", - "strip-final-newline": "strip-final-newline@3.0.0" - } - }, - "fast-stable-stringify@1.0.0": { - "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", - "dependencies": {} - }, - "fill-range@7.0.1": { - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "to-regex-range@5.0.1" - } - }, - "formdata-helper@0.3.0": { - "integrity": "sha512-QkRUFbNgWSu9lkc5TKLWri0ilTFowo950w13I5pRhj4cUxzMLuz0MIhGbE/gIRyfsZQoFeMNN0h06OCSOgfhUg==", - "dependencies": {} - }, - "get-east-asian-width@1.2.0": { - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", - "dependencies": {} - }, - "get-stream@8.0.1": { - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dependencies": {} - }, - "he@1.2.0": { - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dependencies": {} - }, - "hono-rate-limiter@0.3.0_hono@4.2.5": { - "integrity": "sha512-QUS1N+DCZs8CKjpoHugvEvAp/+e+LUllPPnaIlATK9GyT26niQxn4H4V+O5kHwcsXjD3P3JOc0jMb7fnW6gpFQ==", - "dependencies": { - "hono": "hono@4.2.5" - } - }, - "hono@4.2.5": { - "integrity": "sha512-uonJD3i/yy005kQ7bPZRVfG3rejYJwyPqBmPoUGijS4UB/qM+YlrZ7xzSWy+ByDu9buGHUG+f+SKzz03Y6V1Kw==", - "dependencies": {} - }, - "htmlparser2@8.0.2": { - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dependencies": { - "domelementtype": "domelementtype@2.3.0", - "domhandler": "domhandler@5.0.3", - "domutils": "domutils@3.1.0", - "entities": "entities@4.5.0" - } - }, - "human-signals@5.0.0": { - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dependencies": {} - }, - "iconv-lite@0.4.24": { - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": "safer-buffer@2.1.2" - } - }, - "is-fullwidth-code-point@4.0.0": { - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dependencies": {} - }, - "is-fullwidth-code-point@5.0.0": { - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dependencies": { - "get-east-asian-width": "get-east-asian-width@1.2.0" - } - }, - "is-number@7.0.0": { - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dependencies": {} - }, - "is-stream@3.0.0": { - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dependencies": {} - }, - "isexe@2.0.0": { - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dependencies": {} - }, - "iso-639-1@2.1.15": { - "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==", - "dependencies": {} - }, - "kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4": { - "integrity": "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ==", - "dependencies": { - "kysely": "kysely@0.27.3", - "postgres": "postgres@3.4.4" - } - }, - "kysely@0.27.3": { - "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", - "dependencies": {} - }, - "kysely@0.27.4": { - "integrity": "sha512-dyNKv2KRvYOQPLCAOCjjQuCk4YFd33BvGdf/o5bC7FiW+BB6snA81Zt+2wT9QDFzKqxKa5rrOmvlK/anehCcgA==", - "dependencies": {} - }, - "lande@1.0.10": { - "integrity": "sha512-yT52DQh+UV2pEp08jOYrA4drDv0DbjpiRyZYgl25ak9G2cVR2AimzrqkYQWrD9a7Ud+qkAcaiDDoNH9DXfHPmw==", - "dependencies": { - "toygrad": "toygrad@2.6.0" - } - }, - "light-bolt11-decoder@3.1.1": { - "integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==", - "dependencies": { - "@scure/base": "@scure/base@1.1.1" - } - }, - "lilconfig@3.0.0": { - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", - "dependencies": {} - }, - "linkify-plugin-hashtag@4.1.3_linkifyjs@4.1.3": { - "integrity": "sha512-sq627UTrmmDhVnYoUbj/EFfSrhGBvAZYIUdUCjtLeW/AWBV7g9NX9JXEglAuJ7DIyJ84Ged0EHOe+xCXRe2Gmw==", - "dependencies": { - "linkifyjs": "linkifyjs@4.1.3" - } - }, - "linkify-string@4.1.3_linkifyjs@4.1.3": { - "integrity": "sha512-6dAgx4MiTcvEX87OS5aNpAioO7cSELUXp61k7azOvMYOLSmREx0w4yM1Uf0+O3JLC08YdkUyZhAX+YkasRt/mw==", - "dependencies": { - "linkifyjs": "linkifyjs@4.1.3" - } - }, - "linkifyjs@4.1.3": { - "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==", - "dependencies": {} - }, - "lint-staged@15.2.2": { - "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", - "dependencies": { - "chalk": "chalk@5.3.0", - "commander": "commander@11.1.0", - "debug": "debug@4.3.4", - "execa": "execa@8.0.1", - "lilconfig": "lilconfig@3.0.0", - "listr2": "listr2@8.0.1", - "micromatch": "micromatch@4.0.5", - "pidtree": "pidtree@0.6.0", - "string-argv": "string-argv@0.3.2", - "yaml": "yaml@2.3.4" - } - }, - "listr2@8.0.1": { - "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", - "dependencies": { - "cli-truncate": "cli-truncate@4.0.0", - "colorette": "colorette@2.0.20", - "eventemitter3": "eventemitter3@5.0.1", - "log-update": "log-update@6.0.0", - "rfdc": "rfdc@1.3.1", - "wrap-ansi": "wrap-ansi@9.0.0" - } - }, - "log-update@6.0.0": { - "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", - "dependencies": { - "ansi-escapes": "ansi-escapes@6.2.0", - "cli-cursor": "cli-cursor@4.0.0", - "slice-ansi": "slice-ansi@7.1.0", - "strip-ansi": "strip-ansi@7.1.0", - "wrap-ansi": "wrap-ansi@9.0.0" - } - }, - "lru-cache@10.2.2": { - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dependencies": {} - }, - "merge-stream@2.0.0": { - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dependencies": {} - }, - "micromatch@4.0.5": { - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dependencies": { - "braces": "braces@3.0.2", - "picomatch": "picomatch@2.3.1" - } - }, - "mimic-fn@2.1.0": { - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dependencies": {} - }, - "mimic-fn@4.0.0": { - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dependencies": {} - }, - "minimist@1.2.8": { - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dependencies": {} - }, - "ms@2.1.2": { - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dependencies": {} - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dependencies": {} - }, - "node-fetch@2.7.0": { - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "whatwg-url@5.0.0" - } - }, - "nostr-tools@2.5.1": { - "integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==", - "dependencies": { - "@noble/ciphers": "@noble/ciphers@0.5.3", - "@noble/curves": "@noble/curves@1.2.0", - "@noble/hashes": "@noble/hashes@1.3.1", - "@scure/base": "@scure/base@1.1.1", - "@scure/bip32": "@scure/bip32@1.3.1", - "@scure/bip39": "@scure/bip39@1.2.1", - "nostr-wasm": "nostr-wasm@0.1.0" - } - }, - "nostr-tools@2.7.0": { - "integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==", - "dependencies": { - "@noble/ciphers": "@noble/ciphers@0.5.3", - "@noble/curves": "@noble/curves@1.2.0", - "@noble/hashes": "@noble/hashes@1.3.1", - "@scure/base": "@scure/base@1.1.1", - "@scure/bip32": "@scure/bip32@1.3.1", - "@scure/bip39": "@scure/bip39@1.2.1", - "nostr-wasm": "nostr-wasm@0.1.0" - } - }, - "nostr-wasm@0.1.0": { - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "dependencies": {} - }, - "npm-run-path@5.3.0": { - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dependencies": { - "path-key": "path-key@4.0.0" - } - }, - "onetime@5.1.2": { - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "mimic-fn@2.1.0" - } - }, - "onetime@6.0.0": { - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dependencies": { - "mimic-fn": "mimic-fn@4.0.0" - } - }, - "path-key@3.1.1": { - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dependencies": {} - }, - "path-key@4.0.0": { - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dependencies": {} - }, - "path-to-regexp@7.1.0": { - "integrity": "sha512-ZToe+MbUF4lBqk6dV8GKot4DKfzrxXsplOddH8zN3YK+qw9/McvP7+4ICjZvOne0jQhN4eJwHsX6tT0Ns19fvw==", - "dependencies": {} - }, - "picomatch@2.3.1": { - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dependencies": {} - }, - "pidtree@0.6.0": { - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dependencies": {} - }, - "png-to-ico@2.1.8": { - "integrity": "sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==", - "dependencies": { - "@types/node": "@types/node@17.0.45", - "minimist": "minimist@1.2.8", - "pngjs": "pngjs@6.0.0" - } - }, - "pngjs@6.0.0": { - "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", - "dependencies": {} - }, - "postgres@3.4.4": { - "integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==", - "dependencies": {} - }, - "prom-client@15.1.2": { - "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==", - "dependencies": { - "@opentelemetry/api": "@opentelemetry/api@1.9.0", - "tdigest": "tdigest@0.1.2" - } - }, - "restore-cursor@4.0.0": { - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dependencies": { - "onetime": "onetime@5.1.2", - "signal-exit": "signal-exit@3.0.7" - } - }, - "rfdc@1.3.1": { - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dependencies": {} - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dependencies": {} - }, - "shebang-command@2.0.0": { - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "shebang-regex@3.0.0" - } - }, - "shebang-regex@3.0.0": { - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dependencies": {} - }, - "signal-exit@3.0.7": { - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dependencies": {} - }, - "signal-exit@4.1.0": { - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dependencies": {} - }, - "slice-ansi@5.0.0": { - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dependencies": { - "ansi-styles": "ansi-styles@6.2.1", - "is-fullwidth-code-point": "is-fullwidth-code-point@4.0.0" - } - }, - "slice-ansi@7.1.0": { - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dependencies": { - "ansi-styles": "ansi-styles@6.2.1", - "is-fullwidth-code-point": "is-fullwidth-code-point@5.0.0" - } - }, - "string-argv@0.3.2": { - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dependencies": {} - }, - "string-width@7.1.0": { - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", - "dependencies": { - "emoji-regex": "emoji-regex@10.3.0", - "get-east-asian-width": "get-east-asian-width@1.2.0", - "strip-ansi": "strip-ansi@7.1.0" - } - }, - "strip-ansi@7.1.0": { - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "ansi-regex@6.0.1" - } - }, - "strip-final-newline@3.0.0": { - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dependencies": {} - }, - "tdigest@0.1.2": { - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dependencies": { - "bintrees": "bintrees@1.0.2" - } - }, - "tldts-core@6.1.18": { - "integrity": "sha512-e4wx32F/7dMBSZyKAx825Yte3U0PQtZZ0bkWxYQiwLteRVnQ5zM40fEbi0IyNtwQssgJAk3GCr7Q+w39hX0VKA==", - "dependencies": {} - }, - "tldts@6.1.18": { - "integrity": "sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==", - "dependencies": { - "tldts-core": "tldts-core@6.1.18" - } - }, - "to-regex-range@5.0.1": { - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "is-number@7.0.0" - } - }, - "toygrad@2.6.0": { - "integrity": "sha512-g4zBmlSbvzOE5FOILxYkAybTSxijKLkj1WoNqVGnbMcWDyj4wWQ+eYSr3ik7XOpIgMq/7eBcPRTJX3DM2E0YMg==", - "dependencies": {} - }, - "tr46@0.0.3": { - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dependencies": {} - }, - "tseep@1.2.1": { - "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==", - "dependencies": {} - }, - "type-fest@3.13.1": { - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dependencies": {} - }, - "type-fest@4.18.2": { - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", - "dependencies": {} - }, - "unfurl.js@6.4.0": { - "integrity": "sha512-DogJFWPkOWMcu2xPdpmbcsL+diOOJInD3/jXOv6saX1upnWmMK8ndAtDWUfJkuInqNI9yzADud4ID9T+9UeWCw==", - "dependencies": { - "debug": "debug@3.2.7", - "he": "he@1.2.0", - "htmlparser2": "htmlparser2@8.0.2", - "iconv-lite": "iconv-lite@0.4.24", - "node-fetch": "node-fetch@2.7.0" - } - }, - "webidl-conversions@3.0.1": { - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dependencies": {} - }, - "websocket-ts@2.1.5": { - "integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA==", - "dependencies": {} - }, - "whatwg-url@5.0.0": { - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "tr46@0.0.3", - "webidl-conversions": "webidl-conversions@3.0.1" - } - }, - "which@2.0.2": { - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "isexe@2.0.0" - } - }, - "wrap-ansi@9.0.0": { - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dependencies": { - "ansi-styles": "ansi-styles@6.2.1", - "string-width": "string-width@7.1.0", - "strip-ansi": "strip-ansi@7.1.0" - } - }, - "yaml@2.3.4": { - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dependencies": {} - }, - "zod@3.23.8": { - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "dependencies": {} - } + "@bradenmacdonald/s3-lite-client@0.7.6": { + "integrity": "2b5976dca95d207dc88e23f9807e3eecbc441b0cf547dcda5784afe6668404d1", + "dependencies": [ + "jsr:@std/io@0.224" + ] + }, + "@denosaurs/plug@1.0.3": { + "integrity": "b010544e386bea0ff3a1d05e0c88f704ea28cbd4d753439c2f1ee021a85d4640", + "dependencies": [ + "jsr:@std/encoding@0.213.1", + "jsr:@std/fmt", + "jsr:@std/fs@0.213.1", + "jsr:@std/path@0.213.1" + ] + }, + "@gfx/canvas-wasm@0.4.2": { + "integrity": "d653be3bd12cb2fa9bbe5d1b1f041a81b91d80b68502761204aaf60e4592532a", + "dependencies": [ + "jsr:@std/encoding@1.0.5" + ] + }, + "@gleasonator/policy@0.2.0": { + "integrity": "3fe58b853ab203b2b67e65b64391dbcf5c07bc1caaf46e97b2f8ed5b14f30fdf", + "dependencies": [ + "jsr:@nostrify/nostrify@~0.22.1" + ] + }, + "@gleasonator/policy@0.4.0": { + "integrity": "59c2f3ab1dc663e99a3e10b7eb69bf9fe581ce5d428fe56653e38f7f961da5ea", + "dependencies": [ + "jsr:@nostrify/nostrify@~0.22.1" + ] + }, + "@gleasonator/policy@0.4.1": { + "integrity": "4d42d11d2e9b5f183cec1ca73dbb6f5cd50475efb2bef9495857fa8e5e5d8251", + "dependencies": [ + "jsr:@nostrify/nostrify@~0.22.1" + ] + }, + "@gleasonator/policy@0.4.2": { + "integrity": "704527346b35a1ef799c58ba365fea30d1d4bb8e5291937183223d27b24b0f27", + "dependencies": [ + "jsr:@nostrify/nostrify@~0.22.1" + ] + }, + "@gleasonator/policy@0.5.0": { + "integrity": "c2882eb3b4147dfe96b6ec2870b012b5a614f686770d1d4b2f778fdc44e8b1f5", + "dependencies": [ + "jsr:@nostrify/nostrify@0.31", + "jsr:@nostrify/policies@0.33" + ] + }, + "@gleasonator/policy@0.5.1": { + "integrity": "2d687c5166556ce13ac05c4542f61ef8a47d8b96b57f6e43d52035805f895551", + "dependencies": [ + "jsr:@nostrify/nostrify@0.31", + "jsr:@nostrify/policies@0.33" + ] + }, + "@gleasonator/policy@0.5.2": { + "integrity": "cdd3add87be3132eb05736bca640dfb3bbb1aa79928a44d3563cde20bab7c0d3", + "dependencies": [ + "jsr:@nostrify/nostrify@0.31", + "jsr:@nostrify/policies@~0.33.1" + ] + }, + "@gleasonator/policy@0.6.0": { + "integrity": "77f52bb245255a61070a4970c50e2ea8e82345c1de2fef12b9d8887a20b46e6d", + "dependencies": [ + "jsr:@nostrify/nostrify@0.32", + "jsr:@nostrify/policies@0.34" + ] + }, + "@gleasonator/policy@0.6.1": { + "integrity": "ba763d69332a736678b068b4063709874bc64010dfc3f974818218a41deb2291", + "dependencies": [ + "jsr:@nostrify/nostrify@0.32", + "jsr:@nostrify/policies@0.34" + ] + }, + "@gleasonator/policy@0.6.3": { + "integrity": "7126c52edd3de21488714e66ec71f31ba9b14f8afc761ab73ac7c3ecc936625c", + "dependencies": [ + "jsr:@nostrify/nostrify@0.32", + "jsr:@nostrify/policies@0.34" + ] + }, + "@gleasonator/policy@0.6.4": { + "integrity": "fd91c94546edd1de1faa80cb3248699b2f010ef1bdd89818dbc4a03e7606e0bb", + "dependencies": [ + "jsr:@nostrify/nostrify@0.32", + "jsr:@nostrify/policies@0.34" + ] + }, + "@gleasonator/policy@0.7.0": { + "integrity": "22cad69f6c0eaa20ccd45fcbd0a3990c9e395f23181669ebbf397b8c501d14cf", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/policies@0.36" + ] + }, + "@gleasonator/policy@0.7.1": { + "integrity": "411c106ec8594f6b1c6aa716895600f2483f72367862bd80add91a0bfec94c28", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/policies@0.36" + ] + }, + "@gleasonator/policy@0.8.0": { + "integrity": "139611066eb60f15ec40686f9c9b8bad13eb631fdd069fd6eaae3ccf27157b0d", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/policies@0.36" + ] + }, + "@hono/hono@4.4.6": { + "integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453" + }, + "@hono/hono@4.5.0": { + "integrity": "4a410f7773ac4b5b0eb4520b26c7ab7795a271d57a9df7fa1953ded6b90ccaf7" + }, + "@hono/hono@4.5.1": { + "integrity": "459748ed4d4146c6e4bdff0213ff1ac44749904066ae02e7550d6c7f28c9bc4c" + }, + "@hono/hono@4.5.3": { + "integrity": "429923b2b3c6586a1450862328d61a1346fee5841e8ae86c494250475057213c" + }, + "@hono/hono@4.5.4": { + "integrity": "3792780b8460d5df0959b07c059db9325e4fa1a49f8b5aff7ab9bc870bdec8e3" + }, + "@hono/hono@4.5.5": { + "integrity": "e5a63b5f535475cd80974b65fed23a138d0cbb91fe1cc9a17a7c7278e835c308" + }, + "@hono/hono@4.5.9": { + "integrity": "47f561e67aedbd6d1e21e3a1ae26c1b80ffdb62a51c161d502e75bee17ca40af" + }, + "@hono/hono@4.5.11": { + "integrity": "5bd6b1a3a503efb746fdcf0aae3ac536dd09229d372988bde5db0798ef64ae4f" + }, + "@hono/hono@4.6.2": { + "integrity": "35fcf3be4687825080b01bed7bbe2ac66f8d8b8939f0bad459661bf3b46d916f" + }, + "@lambdalisue/async@2.1.1": { + "integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4" + }, + "@nostrify/db@0.35.0": { + "integrity": "637191c41812544e361b7997dc44ea098f8bd7efebb28f37a8a7142a0ecada8d", + "dependencies": [ + "jsr:@nostrify/nostrify@0.35", + "jsr:@nostrify/types@0.35", + "npm:kysely@~0.27.3", + "npm:nostr-tools@^2.7.0" + ] + }, + "@nostrify/nostrify@0.22.4": { + "integrity": "1c8a7847e5773213044b491e85fd7cafae2ad194ce59da4d957d2b27c776b42d", + "dependencies": [ + "jsr:@std/encoding@~0.224.1", + "npm:@noble/hashes", + "npm:@scure/base", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:kysely@~0.27.3", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.5.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/nostrify@0.22.5": { + "integrity": "5b9c17325cc02e37c71e14ac0103b40446b0402fe183e5f5362af23e9ea162bf", + "dependencies": [ + "jsr:@std/encoding@~0.224.1", + "npm:@scure/base", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:kysely@~0.27.3", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.7.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/nostrify@0.30.0": { + "integrity": "7c29e7d8b5a0a81e238170ac1e7ad708bc72dd8f478d8d82c30598fb4eff9b9c", + "dependencies": [ + "jsr:@nostrify/types@0.30", + "jsr:@std/crypto", + "jsr:@std/encoding@~0.224.1", + "npm:@scure/base", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.7.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/nostrify@0.31.0": { + "integrity": "1c1b686bb9ca3ad8d19807e3b96ef3793a65d70fd0f433fe6ef8b3fdb9f45557", + "dependencies": [ + "jsr:@nostrify/types@~0.30.1", + "jsr:@std/encoding@~0.224.1", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.7.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/nostrify@0.32.0": { + "integrity": "2d3b7a9cce275c150355f8e566c11f14044afd0b889afcb48e883da9467bdaa9", + "dependencies": [ + "jsr:@nostrify/types@~0.30.1", + "jsr:@std/encoding@~0.224.1", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.7.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/nostrify@0.35.0": { + "integrity": "9bfef4883838b8b4cb2e2b28a60b72de95391ca5b789bc7206a2baea054dea55", + "dependencies": [ + "jsr:@nostrify/types@0.35", + "jsr:@std/encoding@~0.224.1", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.7.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/nostrify@0.36.0": { + "integrity": "f00dbff1f02a2c496c5e85eeeb7a84101b7dd874d87456449dc71b6d037e40fc", + "dependencies": [ + "jsr:@nostrify/types@0.35", + "jsr:@std/crypto", + "jsr:@std/encoding@~0.224.1", + "npm:@scure/base", + "npm:@scure/bip32@^1.4.0", + "npm:@scure/bip39", + "npm:lru-cache@^10.2.0", + "npm:nostr-tools@^2.7.0", + "npm:websocket-ts", + "npm:zod" + ] + }, + "@nostrify/policies@0.33.0": { + "integrity": "c946b06d0527298b4d7c9819d142a10f522ba09eee76c37525aa4acfc5d87aee", + "dependencies": [ + "jsr:@nostrify/types@~0.30.1", + "npm:nostr-tools@^2.7.0" + ] + }, + "@nostrify/policies@0.33.1": { + "integrity": "381e1f9406a6da22da03a254e46b1aa07d5491b9761961cda3a4aeb5bf3f5286", + "dependencies": [ + "jsr:@nostrify/types@~0.30.1", + "npm:nostr-tools@^2.7.0" + ] + }, + "@nostrify/policies@0.34.0": { + "integrity": "27eb8fb36106a29e982ec7fc6bbb91bd6989f8ce11113a3ef6c528b4c2deceee", + "dependencies": [ + "jsr:@nostrify/nostrify@0.32", + "jsr:@nostrify/types@~0.30.1", + "npm:nostr-tools@^2.7.0" + ] + }, + "@nostrify/policies@0.35.0": { + "integrity": "b828fac9f253e460a9587c05588b7dae6a0a32c5a9c9083e449219887b9e8e20", + "dependencies": [ + "jsr:@nostrify/nostrify@0.35", + "jsr:@nostrify/types@0.35", + "npm:nostr-tools@^2.7.0" + ] + }, + "@nostrify/policies@0.36.0": { + "integrity": "ad1930de48ce03cdf34da456af1563b487581d1d86683cd416ad760ae40b1fb3", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/types@0.35", + "npm:nostr-tools@^2.7.0" + ] + }, + "@nostrify/types@0.30.0": { + "integrity": "1f38fa849cff930bd709edbf94ef9ac02f46afb8b851f86c8736517b354616da" + }, + "@nostrify/types@0.30.1": { + "integrity": "245da176f6893a43250697db51ad32bfa29bf9b1cdc1ca218043d9abf6de5ae5" + }, + "@nostrify/types@0.35.0": { + "integrity": "b8d515563d467072694557d5626fa1600f74e83197eef45dd86a9a99c64f7fe6" + }, + "@soapbox/kysely-pglite@1.0.0": { + "integrity": "0954b1bf3deab051c479cba966b1e6ed5a0a966aa21d1f40143ec8f5efcd475d", + "dependencies": [ + "npm:kysely@~0.27.4" + ] + }, + "@soapbox/stickynotes@0.4.0": { + "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" + }, + "@std/assert@0.213.1": { + "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe" + }, + "@std/assert@0.223.0": { + "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" + }, + "@std/assert@0.224.0": { + "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" + }, + "@std/assert@0.225.3": { + "integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/bytes@0.223.0": { + "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" + }, + "@std/bytes@0.224.0": { + "integrity": "a2250e1d0eb7d1c5a426f21267ab9bdeac2447fa87a3d0d1a467d3f7a6058e49" + }, + "@std/bytes@1.0.0": { + "integrity": "9392e72af80adccaa1197912fa19990ed091cb98d5c9c4344b0c301b22d7c632" + }, + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, + "@std/cli@0.223.0": { + "integrity": "2feb7970f2028904c3edc22ea916ce9538113dfc170844f3eae03578c333c356", + "dependencies": [ + "jsr:@std/assert@0.223" + ] + }, + "@std/crypto@0.224.0": { + "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", + "dependencies": [ + "jsr:@std/assert@0.224", + "jsr:@std/encoding@0.224" + ] + }, + "@std/dotenv@0.224.0": { + "integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d" + }, + "@std/dotenv@0.224.2": { + "integrity": "29081695357e4534696c9e986b2560be29c141ccf52daa32b6c20ff5b5c64ab9" + }, + "@std/encoding@0.213.1": { + "integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62" + }, + "@std/encoding@0.224.3": { + "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/fmt@0.213.1": { + "integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3" + }, + "@std/fs@0.213.1": { + "integrity": "fbcaf099f8a85c27ab0712b666262cda8fe6d02e9937bf9313ecaea39a22c501", + "dependencies": [ + "jsr:@std/assert@~0.213.1", + "jsr:@std/path@~0.213.1" + ] + }, + "@std/fs@0.229.3": { + "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb", + "dependencies": [ + "jsr:@std/path@1.0.0-rc.1" + ] + }, + "@std/internal@1.0.0": { + "integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a" + }, + "@std/internal@1.0.1": { + "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" + }, + "@std/internal@1.0.3": { + "integrity": "208e9b94a3d5649bd880e9ca38b885ab7651ab5b5303a56ed25de4755fb7b11e" + }, + "@std/internal@1.0.4": { + "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" + }, + "@std/io@0.223.0": { + "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", + "dependencies": [ + "jsr:@std/assert@0.223", + "jsr:@std/bytes@0.223" + ] + }, + "@std/io@0.224.0": { + "integrity": "0aff885d21d829c050b8a08b1d71b54aed5841aecf227f8d77e99ec529a11e8e", + "dependencies": [ + "jsr:@std/bytes@0.224" + ] + }, + "@std/io@0.224.1": { + "integrity": "73de242551a5c0965eb33e36b1fc7df4834ffbc836a1a643a410ccd11253d6be", + "dependencies": [ + "jsr:@std/bytes@^1.0.0-rc.3" + ] + }, + "@std/io@0.224.3": { + "integrity": "b402edeb99c6b3778d9ae3e9927bc9085b170b41e5a09bbb7064ab2ee394ae2f", + "dependencies": [ + "jsr:@std/bytes@^1.0.1-rc.3" + ] + }, + "@std/io@0.224.4": { + "integrity": "bce1151765e4e70e376039fd72c71672b4d4aae363878a5ee3e58361b81197ec", + "dependencies": [ + "jsr:@std/bytes@^1.0.2-rc.3" + ] + }, + "@std/io@0.224.6": { + "integrity": "eefe034a370be34daf066c8634dd645635d099bb21eccf110f0bdc28d9040891", + "dependencies": [ + "jsr:@std/bytes@^1.0.2" + ] + }, + "@std/io@0.224.7": { + "integrity": "a70848793c44a7c100926571a8c9be68ba85487bfcd4d0540d86deabe1123dc9", + "dependencies": [ + "jsr:@std/bytes@^1.0.2" + ] + }, + "@std/io@0.224.8": { + "integrity": "f525d05d51fd873de6352b9afcf35cab9ab5dc448bf3c20e0c8b521ded9be392", + "dependencies": [ + "jsr:@std/bytes@^1.0.2" + ] + }, + "@std/json@0.223.0": { + "integrity": "9a4a255931dd0397924c6b10bb6a72fe3e28ddd876b981ada2e3b8dd0764163f", + "dependencies": [ + "jsr:@std/streams" + ] + }, + "@std/media-types@0.224.1": { + "integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1" + }, + "@std/path@0.213.1": { + "integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673", + "dependencies": [ + "jsr:@std/assert@~0.213.1" + ] + }, + "@std/path@1.0.0-rc.1": { + "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" + }, + "@std/streams@0.223.0": { + "integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99", + "dependencies": [ + "jsr:@std/assert@0.223", + "jsr:@std/bytes@0.223", + "jsr:@std/io@0.223" + ] + } + }, + "npm": { + "@electric-sql/pglite@0.2.8": { + "integrity": "sha512-0wSmQu22euBRzR5ghqyIHnBH4MfwlkL5WstOrrA3KOsjEWEglvoL/gH92JajEUA6Ufei/+qbkB2hVloC/K/RxQ==" + }, + "@isaacs/ttlcache@1.4.1": { + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==" + }, + "@noble/ciphers@0.5.3": { + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==" + }, + "@noble/curves@1.1.0": { + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": [ + "@noble/hashes@1.3.1" + ] + }, + "@noble/curves@1.2.0": { + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": [ + "@noble/hashes@1.3.2" + ] + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": [ + "@noble/hashes@1.4.0" + ] + }, + "@noble/curves@1.6.0": { + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "dependencies": [ + "@noble/hashes@1.5.0" + ] + }, + "@noble/hashes@1.3.1": { + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + }, + "@noble/hashes@1.3.2": { + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@noble/hashes@1.5.0": { + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" + }, + "@noble/secp256k1@2.1.0": { + "integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==" + }, + "@opentelemetry/api@1.9.0": { + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" + }, + "@scure/base@1.1.1": { + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, + "@scure/base@1.1.6": { + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==" + }, + "@scure/base@1.1.9": { + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==" + }, + "@scure/bip32@1.3.1": { + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": [ + "@noble/curves@1.1.0", + "@noble/hashes@1.3.2", + "@scure/base@1.1.6" + ] + }, + "@scure/bip32@1.4.0": { + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": [ + "@noble/curves@1.4.0", + "@noble/hashes@1.4.0", + "@scure/base@1.1.6" + ] + }, + "@scure/bip32@1.5.0": { + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", + "dependencies": [ + "@noble/curves@1.6.0", + "@noble/hashes@1.5.0", + "@scure/base@1.1.9" + ] + }, + "@scure/bip39@1.2.1": { + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": [ + "@noble/hashes@1.3.2", + "@scure/base@1.1.6" + ] + }, + "@scure/bip39@1.3.0": { + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dependencies": [ + "@noble/hashes@1.4.0", + "@scure/base@1.1.6" + ] + }, + "@types/dompurify@3.0.5": { + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dependencies": [ + "@types/trusted-types" + ] + }, + "@types/node@17.0.45": { + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==" + }, + "@types/trusted-types@2.0.7": { + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "agent-base@7.1.1": { + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": [ + "debug@4.3.4" + ] + }, + "ansi-escapes@6.2.0": { + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dependencies": [ + "type-fest@3.13.1" + ] + }, + "ansi-regex@6.0.1": { + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles@6.2.1": { + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "asynckit@0.4.0": { + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "bintrees@1.0.2": { + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, + "braces@3.0.2": { + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": [ + "fill-range" + ] + }, + "chalk@5.3.0": { + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + }, + "cli-cursor@4.0.0": { + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": [ + "restore-cursor" + ] + }, + "cli-truncate@4.0.0": { + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dependencies": [ + "slice-ansi@5.0.0", + "string-width" + ] + }, + "colorette@2.0.20": { + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, + "combined-stream@1.0.8": { + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": [ + "delayed-stream" + ] + }, + "comlink-async-generator@0.0.1": { + "integrity": "sha512-RjOPv6Tb7cL9FiIgwanUJuFG9aW4myAFyyzxZoEkEegeDQrZqr92d1Njv2WIgi7nbGpTiyy5GdNTUubDaNgZ6A==", + "dependencies": [ + "comlink" + ] + }, + "comlink@4.4.1": { + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==" + }, + "commander@11.1.0": { + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + }, + "commander@12.1.0": { + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==" + }, + "cross-spawn@7.0.3": { + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": [ + "path-key@3.1.1", + "shebang-command", + "which" + ] + }, + "cssstyle@4.1.0": { + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dependencies": [ + "rrweb-cssom" + ] + }, + "data-urls@5.0.0": { + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": [ + "whatwg-mimetype", + "whatwg-url@14.0.0" + ] + }, + "debug@3.2.7": { + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": [ + "ms@2.1.3" + ] + }, + "debug@4.3.4": { + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": [ + "ms@2.1.2" + ] + }, + "decimal.js@10.4.3": { + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "delayed-stream@1.0.0": { + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "dom-serializer@2.0.0": { + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": [ + "domelementtype", + "domhandler", + "entities" + ] + }, + "domelementtype@2.3.0": { + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler@5.0.3": { + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": [ + "domelementtype" + ] + }, + "dompurify@3.1.7": { + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==" + }, + "domutils@3.1.0": { + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": [ + "dom-serializer", + "domelementtype", + "domhandler" + ] + }, + "emoji-regex@10.3.0": { + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "eventemitter3@5.0.1": { + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "execa@8.0.1": { + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dependencies": [ + "cross-spawn", + "get-stream", + "human-signals", + "is-stream", + "merge-stream", + "npm-run-path", + "onetime@6.0.0", + "signal-exit@4.1.0", + "strip-final-newline" + ] + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, + "fill-range@7.0.1": { + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": [ + "to-regex-range" + ] + }, + "form-data@4.0.0": { + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": [ + "asynckit", + "combined-stream", + "mime-types" + ] + }, + "formdata-helper@0.3.0": { + "integrity": "sha512-QkRUFbNgWSu9lkc5TKLWri0ilTFowo950w13I5pRhj4cUxzMLuz0MIhGbE/gIRyfsZQoFeMNN0h06OCSOgfhUg==" + }, + "get-east-asian-width@1.2.0": { + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==" + }, + "get-stream@8.0.1": { + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==" + }, + "he@1.2.0": { + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hono-rate-limiter@0.3.0_hono@4.2.5": { + "integrity": "sha512-QUS1N+DCZs8CKjpoHugvEvAp/+e+LUllPPnaIlATK9GyT26niQxn4H4V+O5kHwcsXjD3P3JOc0jMb7fnW6gpFQ==", + "dependencies": [ + "hono" + ] + }, + "hono@4.2.5": { + "integrity": "sha512-uonJD3i/yy005kQ7bPZRVfG3rejYJwyPqBmPoUGijS4UB/qM+YlrZ7xzSWy+ByDu9buGHUG+f+SKzz03Y6V1Kw==" + }, + "html-encoding-sniffer@4.0.0": { + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": [ + "whatwg-encoding" + ] + }, + "htmlparser2@8.0.2": { + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dependencies": [ + "domelementtype", + "domhandler", + "domutils", + "entities" + ] + }, + "http-proxy-agent@7.0.2": { + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": [ + "agent-base", + "debug@4.3.4" + ] + }, + "https-proxy-agent@7.0.5": { + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": [ + "agent-base", + "debug@4.3.4" + ] + }, + "human-signals@5.0.0": { + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==" + }, + "iconv-lite@0.4.24": { + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": [ + "safer-buffer" + ] + }, + "iconv-lite@0.6.3": { + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": [ + "safer-buffer" + ] + }, + "is-fullwidth-code-point@4.0.0": { + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==" + }, + "is-fullwidth-code-point@5.0.0": { + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dependencies": [ + "get-east-asian-width" + ] + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-potential-custom-element-name@1.0.1": { + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "is-stream@3.0.0": { + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==" + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "iso-639-1@2.1.15": { + "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==" + }, + "isomorphic-dompurify@2.16.0": { + "integrity": "sha512-cXhX2owp8rPxafCr0ywqy2CGI/4ceLNgWkWBEvUz64KTbtg3oRL2ZRqq/zW0pzt4YtDjkHLbwcp/lozpKzAQjg==", + "dependencies": [ + "@types/dompurify", + "dompurify", + "jsdom" + ] + }, + "jsdom@25.0.1": { + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dependencies": [ + "cssstyle", + "data-urls", + "decimal.js", + "form-data", + "html-encoding-sniffer", + "http-proxy-agent", + "https-proxy-agent", + "is-potential-custom-element-name", + "nwsapi", + "parse5", + "rrweb-cssom", + "saxes", + "symbol-tree", + "tough-cookie", + "w3c-xmlserializer", + "webidl-conversions@7.0.0", + "whatwg-encoding", + "whatwg-mimetype", + "whatwg-url@14.0.0", + "ws", + "xml-name-validator" + ] + }, + "kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4": { + "integrity": "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ==", + "dependencies": [ + "kysely@0.27.3", + "postgres" + ] + }, + "kysely@0.27.3": { + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==" + }, + "kysely@0.27.4": { + "integrity": "sha512-dyNKv2KRvYOQPLCAOCjjQuCk4YFd33BvGdf/o5bC7FiW+BB6snA81Zt+2wT9QDFzKqxKa5rrOmvlK/anehCcgA==" + }, + "lande@1.0.10": { + "integrity": "sha512-yT52DQh+UV2pEp08jOYrA4drDv0DbjpiRyZYgl25ak9G2cVR2AimzrqkYQWrD9a7Ud+qkAcaiDDoNH9DXfHPmw==", + "dependencies": [ + "toygrad" + ] + }, + "light-bolt11-decoder@3.1.1": { + "integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==", + "dependencies": [ + "@scure/base@1.1.1" + ] + }, + "lilconfig@3.0.0": { + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==" + }, + "linkify-plugin-hashtag@4.1.3_linkifyjs@4.1.3": { + "integrity": "sha512-sq627UTrmmDhVnYoUbj/EFfSrhGBvAZYIUdUCjtLeW/AWBV7g9NX9JXEglAuJ7DIyJ84Ged0EHOe+xCXRe2Gmw==", + "dependencies": [ + "linkifyjs" + ] + }, + "linkify-string@4.1.3_linkifyjs@4.1.3": { + "integrity": "sha512-6dAgx4MiTcvEX87OS5aNpAioO7cSELUXp61k7azOvMYOLSmREx0w4yM1Uf0+O3JLC08YdkUyZhAX+YkasRt/mw==", + "dependencies": [ + "linkifyjs" + ] + }, + "linkifyjs@4.1.3": { + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" + }, + "lint-staged@15.2.2": { + "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "dependencies": [ + "chalk", + "commander@11.1.0", + "debug@4.3.4", + "execa", + "lilconfig", + "listr2", + "micromatch", + "pidtree", + "string-argv", + "yaml" + ] + }, + "listr2@8.0.1": { + "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "dependencies": [ + "cli-truncate", + "colorette", + "eventemitter3", + "log-update", + "rfdc", + "wrap-ansi" + ] + }, + "log-update@6.0.0": { + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dependencies": [ + "ansi-escapes", + "cli-cursor", + "slice-ansi@7.1.0", + "strip-ansi", + "wrap-ansi" + ] + }, + "lru-cache@10.2.2": { + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" + }, + "merge-stream@2.0.0": { + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "micromatch@4.0.5": { + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": [ + "braces", + "picomatch" + ] + }, + "mime-db@1.52.0": { + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types@2.1.35": { + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": [ + "mime-db" + ] + }, + "mimic-fn@2.1.0": { + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "mimic-fn@4.0.0": { + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==" + }, + "minimist@1.2.8": { + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "ms@2.1.2": { + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": [ + "whatwg-url@5.0.0" + ] + }, + "nostr-tools@2.5.1": { + "integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==", + "dependencies": [ + "@noble/ciphers", + "@noble/curves@1.2.0", + "@noble/hashes@1.3.1", + "@scure/base@1.1.1", + "@scure/bip32@1.3.1", + "@scure/bip39@1.2.1", + "nostr-wasm" + ] + }, + "nostr-tools@2.7.0": { + "integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==", + "dependencies": [ + "@noble/ciphers", + "@noble/curves@1.2.0", + "@noble/hashes@1.3.1", + "@scure/base@1.1.1", + "@scure/bip32@1.3.1", + "@scure/bip39@1.2.1", + "nostr-wasm" + ] + }, + "nostr-wasm@0.1.0": { + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==" + }, + "npm-run-path@5.3.0": { + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dependencies": [ + "path-key@4.0.0" + ] + }, + "nwsapi@2.2.13": { + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==" + }, + "onetime@5.1.2": { + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": [ + "mimic-fn@2.1.0" + ] + }, + "onetime@6.0.0": { + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dependencies": [ + "mimic-fn@4.0.0" + ] + }, + "parse5@7.1.2": { + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": [ + "entities" + ] + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-key@4.0.0": { + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==" + }, + "path-to-regexp@7.1.0": { + "integrity": "sha512-ZToe+MbUF4lBqk6dV8GKot4DKfzrxXsplOddH8zN3YK+qw9/McvP7+4ICjZvOne0jQhN4eJwHsX6tT0Ns19fvw==" + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pidtree@0.6.0": { + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==" + }, + "png-to-ico@2.1.8": { + "integrity": "sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==", + "dependencies": [ + "@types/node@17.0.45", + "minimist", + "pngjs" + ] + }, + "pngjs@6.0.0": { + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==" + }, + "postgres@3.4.4": { + "integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==" + }, + "prom-client@15.1.2": { + "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==", + "dependencies": [ + "@opentelemetry/api", + "tdigest" + ] + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "restore-cursor@4.0.0": { + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": [ + "onetime@5.1.2", + "signal-exit@3.0.7" + ] + }, + "rfdc@1.3.1": { + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "rrweb-cssom@0.7.1": { + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saxes@6.0.0": { + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": [ + "xmlchars" + ] + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit@3.0.7": { + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "signal-exit@4.1.0": { + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "slice-ansi@5.0.0": { + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dependencies": [ + "ansi-styles", + "is-fullwidth-code-point@4.0.0" + ] + }, + "slice-ansi@7.1.0": { + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dependencies": [ + "ansi-styles", + "is-fullwidth-code-point@5.0.0" + ] + }, + "string-argv@0.3.2": { + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==" + }, + "string-width@7.1.0": { + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dependencies": [ + "emoji-regex", + "get-east-asian-width", + "strip-ansi" + ] + }, + "strip-ansi@7.1.0": { + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": [ + "ansi-regex" + ] + }, + "strip-final-newline@3.0.0": { + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==" + }, + "symbol-tree@3.2.4": { + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "tdigest@0.1.2": { + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": [ + "bintrees" + ] + }, + "tldts-core@6.1.18": { + "integrity": "sha512-e4wx32F/7dMBSZyKAx825Yte3U0PQtZZ0bkWxYQiwLteRVnQ5zM40fEbi0IyNtwQssgJAk3GCr7Q+w39hX0VKA==" + }, + "tldts-core@6.1.49": { + "integrity": "sha512-ctRO/wzBasOCxAStJG/60Qe8/QpGmaVPsE8djdk0vioxN4uCOgKoveH71Qc2EOmVMIjVf0BjigI5p9ZDuLOygg==" + }, + "tldts@6.1.18": { + "integrity": "sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==", + "dependencies": [ + "tldts-core@6.1.18" + ] + }, + "tldts@6.1.49": { + "integrity": "sha512-E5se9HuCyfwWvmc0JiXiocOw+Cm4tlJCKewdB5RKMH8MmtiTsQCc+yu5BBYB5ZN4lNbz8Xg65bqJ1odS9+RhIA==", + "dependencies": [ + "tldts-core@6.1.49" + ] + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "tough-cookie@5.0.0": { + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dependencies": [ + "tldts@6.1.49" + ] + }, + "toygrad@2.6.0": { + "integrity": "sha512-g4zBmlSbvzOE5FOILxYkAybTSxijKLkj1WoNqVGnbMcWDyj4wWQ+eYSr3ik7XOpIgMq/7eBcPRTJX3DM2E0YMg==" + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tr46@5.0.0": { + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": [ + "punycode" + ] + }, + "tseep@1.2.1": { + "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==" + }, + "type-fest@3.13.1": { + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==" + }, + "type-fest@4.18.2": { + "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==" + }, + "unfurl.js@6.4.0": { + "integrity": "sha512-DogJFWPkOWMcu2xPdpmbcsL+diOOJInD3/jXOv6saX1upnWmMK8ndAtDWUfJkuInqNI9yzADud4ID9T+9UeWCw==", + "dependencies": [ + "debug@3.2.7", + "he", + "htmlparser2", + "iconv-lite@0.4.24", + "node-fetch" + ] + }, + "w3c-xmlserializer@5.0.0": { + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": [ + "xml-name-validator" + ] + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "webidl-conversions@7.0.0": { + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "websocket-ts@2.1.5": { + "integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA==" + }, + "whatwg-encoding@3.1.1": { + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": [ + "iconv-lite@0.6.3" + ] + }, + "whatwg-mimetype@4.0.0": { + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" + }, + "whatwg-url@14.0.0": { + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": [ + "tr46@5.0.0", + "webidl-conversions@7.0.0" + ] + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": [ + "tr46@0.0.3", + "webidl-conversions@3.0.1" + ] + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ] + }, + "wrap-ansi@9.0.0": { + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dependencies": [ + "ansi-styles", + "string-width", + "strip-ansi" + ] + }, + "ws@8.18.0": { + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" + }, + "xml-name-validator@5.0.0": { + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==" + }, + "xmlchars@2.2.0": { + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "yaml@2.3.4": { + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" + }, + "zod@3.23.8": { + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" } }, "redirects": { @@ -1913,49 +2043,49 @@ }, "workspace": { "dependencies": [ - "jsr:@b-fuze/deno-dom@^0.1.47", - "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", - "jsr:@gfx/canvas-wasm@^0.4.2", + "jsr:@b-fuze/deno-dom@~0.1.47", + "jsr:@bradenmacdonald/s3-lite-client@~0.7.4", + "jsr:@gfx/canvas-wasm@~0.4.2", "jsr:@hono/hono@^4.4.6", "jsr:@lambdalisue/async@^2.1.1", - "jsr:@nostrify/db@^0.35.0", - "jsr:@nostrify/nostrify@^0.36.0", - "jsr:@nostrify/policies@^0.35.0", - "jsr:@soapbox/kysely-pglite@^1.0.0", - "jsr:@soapbox/stickynotes@^0.4.0", - "jsr:@std/assert@^0.225.1", - "jsr:@std/cli@^0.223.0", - "jsr:@std/crypto@^0.224.0", - "jsr:@std/dotenv@^0.224.0", - "jsr:@std/encoding@^0.224.0", - "jsr:@std/fs@^0.229.3", - "jsr:@std/json@^0.223.0", - "jsr:@std/media-types@^0.224.1", - "jsr:@std/streams@^0.223.0", - "npm:@electric-sql/pglite@^0.2.8", + "jsr:@nostrify/db@0.35", + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/policies@0.35", + "jsr:@soapbox/kysely-pglite@1", + "jsr:@soapbox/stickynotes@0.4", + "jsr:@std/assert@~0.225.1", + "jsr:@std/cli@0.223", + "jsr:@std/crypto@0.224", + "jsr:@std/dotenv@0.224", + "jsr:@std/encoding@0.224", + "jsr:@std/fs@~0.229.3", + "jsr:@std/json@0.223", + "jsr:@std/media-types@~0.224.1", + "jsr:@std/streams@0.223", + "npm:@electric-sql/pglite@~0.2.8", "npm:@isaacs/ttlcache@^1.4.1", - "npm:@noble/secp256k1@^2.0.0", + "npm:@noble/secp256k1@2", "npm:@scure/base@^1.1.6", "npm:@scure/bip32@^1.5.0", "npm:comlink-async-generator@^0.0.1", "npm:comlink@^4.4.1", "npm:commander@12.1.0", "npm:entities@^4.5.0", - "npm:fast-stable-stringify@^1.0.0", - "npm:formdata-helper@^0.3.0", - "npm:hono-rate-limiter@^0.3.0", + "npm:fast-stable-stringify@1", + "npm:formdata-helper@0.3", + "npm:hono-rate-limiter@0.3", "npm:iso-639-1@2.1.15", "npm:isomorphic-dompurify@^2.16.0", "npm:kysely-postgres-js@2.0.0", - "npm:kysely@^0.27.4", + "npm:kysely@~0.27.4", "npm:lande@^1.0.10", - "npm:light-bolt11-decoder", + "npm:light-bolt11-decoder@*", "npm:linkify-plugin-hashtag@^4.1.1", "npm:linkify-string@^4.1.1", "npm:linkifyjs@^4.1.1", "npm:lru-cache@^10.2.2", "npm:nostr-tools@2.5.1", - "npm:nostr-wasm@^0.1.0", + "npm:nostr-wasm@0.1", "npm:path-to-regexp@^7.1.0", "npm:png-to-ico@^2.1.8", "npm:prom-client@^15.1.2", diff --git a/src/workers/policy.ts b/src/workers/policy.ts index debfe242..c0beb4d7 100644 --- a/src/workers/policy.ts +++ b/src/workers/policy.ts @@ -20,14 +20,16 @@ class PolicyWorker implements NPolicy { new URL('./policy.worker.ts', import.meta.url), { type: 'module', - deno: { - permissions: { - read: [Conf.denoDir, Conf.policy, Conf.dataDir], - write: [Conf.dataDir], - net: 'inherit', - env: false, - }, - }, + // FIXME: Disabled until Deno 2.0 adds support for `import` permission here. + // https://github.com/denoland/deno/issues/26074 + // deno: { + // permissions: { + // read: [Conf.denoDir, Conf.policy, Conf.dataDir], + // write: [Conf.dataDir], + // net: 'inherit', + // env: false, + // }, + // }, }, ), ); From df92483617f09a6fac9d8f71e44f023363ee3097 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 04:25:49 -0500 Subject: [PATCH 33/64] Add --allow-import to check task --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index dc107e8f..bb580e72 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,7 @@ "nostr:pull": "deno run -A scripts/nostr-pull.ts", "debug": "deno run -A --inspect src/server.ts", "test": "deno test -A --junit-path=./deno-test.xml", - "check": "deno check src/server.ts", + "check": "deno check --allow-import src/server.ts", "nsec": "deno run scripts/nsec.ts", "admin:event": "deno run -A scripts/admin-event.ts", "admin:role": "deno run -A scripts/admin-role.ts", From c1c25d7c08c9dad73c95d7200b96fbeb347e14b0 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 14:57:28 -0300 Subject: [PATCH 34/64] feat: create localeSchema --- src/schema.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index 9adcffdd..a9dd56e3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -42,7 +42,7 @@ const fileSchema = z.custom((value) => value instanceof File); const percentageSchema = z.coerce.number().int().gte(1).lte(100); const languageSchema = z.string().transform((val, ctx) => { - val = (val.toLowerCase()).split('-')[0]; // pt-BR -> pt + val = val.toLowerCase(); if (!ISO6391.validate(val)) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -53,6 +53,18 @@ const languageSchema = z.string().transform((val, ctx) => { return val as LanguageCode; }); +const localeSchema = z.string().transform((val, ctx) => { + try { + return new Intl.Locale(val); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid locale', + }); + return z.NEVER; + } +}); + export { booleanParamSchema, decode64Schema, @@ -60,6 +72,7 @@ export { filteredArray, hashtagSchema, languageSchema, + localeSchema, percentageSchema, safeUrlSchema, }; From 4f8c8fd1de7f59f606adce25fc9f4a8f34d34ca0 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 15:03:11 -0300 Subject: [PATCH 35/64] refactor: simply DittoTranslator interface and classes that implement it --- src/controllers/api/translate.ts | 98 ++++++++++--- src/translators/DeepLTranslator.test.ts | 131 +++-------------- src/translators/DeepLTranslator.ts | 107 ++++---------- .../LibreTranslateTranslator.test.ts | 133 +++--------------- src/translators/LibreTranslateTranslator.ts | 103 ++++---------- src/translators/translator.ts | 12 +- 6 files changed, 183 insertions(+), 401 deletions(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 37b0bcea..f2ca9eae 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -1,14 +1,15 @@ +import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { languageSchema } from '@/schema.ts'; -import { dittoTranslations, dittoTranslationsKey } from '@/translators/translator.ts'; +import { localeSchema } from '@/schema.ts'; +import { dittoTranslations, dittoTranslationsKey, MastodonTranslation } from '@/translators/translator.ts'; import { parseBody } from '@/utils/api.ts'; import { getEvent } from '@/queries.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; const translateSchema = z.object({ - lang: languageSchema, + lang: localeSchema, }); const translateController: AppController = async (c) => { @@ -24,7 +25,8 @@ const translateController: AppController = async (c) => { return c.json({ error: 'No translator configured.' }, 500); } - const { lang } = result.data; + const lang = result.data.lang.language.slice(0, 2) as LanguageCode; + const id = c.req.param('id'); const event = await getEvent(id, { signal }); @@ -39,6 +41,9 @@ const translateController: AppController = async (c) => { } const status = await renderStatus(event, { viewerPubkey }); + if (!status?.content) { + return c.json({ error: 'Bad request.', schema: result.error }, 400); + } const translatedId = `${lang}-${id}` as dittoTranslationsKey; const translationCache = dittoTranslations.get(translatedId); @@ -55,18 +60,79 @@ const translateController: AppController = async (c) => { }) ?? []; try { - const translation = await translator.translate( - status?.content ?? '', - status?.spoiler_text ?? '', - mediaAttachments, - null, - event.language, - lang, - { signal }, - ); - dittoTranslations.set(translatedId, translation); - return c.json(translation.data, 200); - } catch (_) { + const texts: string[] = []; + + const mastodonTranslation: MastodonTranslation = { + content: '', + spoiler_text: '', + media_attachments: [], + poll: null, + detected_source_language: event.language ?? 'en', + provider: translator.getProvider(), + }; + + if ((status?.poll as MastodonTranslation['poll'])?.options) { + mastodonTranslation.poll = { id: (status?.poll as MastodonTranslation['poll'])?.id!, options: [] }; + } + + type TranslationIndex = { + [key: number]: 'content' | 'spoilerText' | 'poll' | { type: 'media'; id: string }; + }; + const translationIndex: TranslationIndex = {}; + let index = 0; + + // Content + translationIndex[index] = 'content'; + texts.push(status.content); + index++; + + // Spoiler text + if (status.spoiler_text) { + translationIndex[index] = 'spoilerText'; + texts.push(status.spoiler_text); + index++; + } + + // Media description + for (const [mediaIndex, value] of mediaAttachments.entries()) { + translationIndex[index + mediaIndex] = { type: 'media', id: value.id }; + texts.push(mediaAttachments[mediaIndex].description); + index += mediaIndex; + } + + // Poll title + if (status?.poll) { + for (const [pollIndex] of (status?.poll as MastodonTranslation['poll'])!.options.entries()) { + translationIndex[index + pollIndex] = 'poll'; + texts.push((status.poll as MastodonTranslation['poll'])!.options[pollIndex].title); + index += pollIndex; + } + } + + const data = await translator.translate(texts, event.language, lang, { signal }); + const translatedTexts = data.results; + + for (let i = 0; i < texts.length; i++) { + if (translationIndex[i] === 'content') { + mastodonTranslation.content = translatedTexts[i]; + } else if (translationIndex[i] === 'spoilerText') { + mastodonTranslation.spoiler_text = translatedTexts[i]; + } else if (translationIndex[i] === 'poll') { + mastodonTranslation.poll?.options.push({ title: translatedTexts[i] }); + } else { + const media = translationIndex[i] as { type: 'media'; id: string }; + mastodonTranslation.media_attachments.push({ + id: media.id, + description: translatedTexts[i], + }); + } + } + + mastodonTranslation.detected_source_language = data.source_lang; + + dittoTranslations.set(translatedId, { data: mastodonTranslation }); + return c.json(mastodonTranslation, 200); + } catch { return c.json({ error: 'Service Unavailable' }, 503); } }; diff --git a/src/translators/DeepLTranslator.test.ts b/src/translators/DeepLTranslator.test.ts index f8a12ede..385c10fc 100644 --- a/src/translators/DeepLTranslator.test.ts +++ b/src/translators/DeepLTranslator.test.ts @@ -9,131 +9,44 @@ const apiKey = Conf.deepLapiKey; const translationProvider = Conf.translationProvider; const deepL = 'deepl'; -Deno.test('Translate status with EMPTY media_attachments and WITHOUT poll', { +Deno.test('DeepL translation with source language omitted', { ignore: !(translationProvider === deepL && apiKey), }, async () => { const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - const mastodonTranslation = await translator.translate( - 'Bom dia amigos do Element, meu nome รฉ Patrick', - '', - [], - null, - 'pt', - 'en', - ); - - assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments, []); - assertEquals(mastodonTranslation.data.poll, null); - assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); -}); - -Deno.test('Translate status WITH auto detect and with EMPTY media_attachments and WITHOUT poll', { - ignore: !(translationProvider === deepL && apiKey), -}, async () => { - const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - - const mastodonTranslation = await translator.translate( - 'Bom dia amigos do Element, meu nome รฉ Patrick', - '', - [], - null, + const data = await translator.translate( + [ + 'Bom dia amigos', + 'Meu nome รฉ Patrick', + 'Eu irei morar na America, eu prometo. Mas antes, eu devo mencionar que o lande estรก interpretando este texto como italiano, que estranho.', + ], undefined, 'en', ); - assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments, []); - assertEquals(mastodonTranslation.data.poll, null); - assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); + assertEquals(data.source_lang, 'pt'); + assertEquals(getLanguage(data.results[0]), 'en'); + assertEquals(getLanguage(data.results[1]), 'en'); + assertEquals(getLanguage(data.results[2]), 'en'); }); -Deno.test('Translate status WITH media_attachments and WITHOUT poll', { +Deno.test('DeepL translation with source language set', { ignore: !(translationProvider === deepL && apiKey), }, async () => { const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - const mastodonTranslation = await translator.translate( - 'Hello my friends, my name is Alex and I am american.', - "That is spoiler isn't it", - [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], - null, - 'en', - 'pt', - ); - - assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); - assertEquals(getLanguage(mastodonTranslation.data.spoiler_text), 'pt'); - assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); - assertEquals(mastodonTranslation.data.poll, null); - assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); -}); - -Deno.test('Translate status WITHOUT media_attachments and WITH poll', { - ignore: !(translationProvider === deepL && apiKey), -}, async () => { - const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - - const poll = { - 'id': '34858', - 'options': [ - { - 'title': 'Kill him right now', - }, - { - 'title': 'Save him right now', - }, + const data = await translator.translate( + [ + 'Bom dia amigos', + 'Meu nome รฉ Patrick', + 'Eu irei morar na America, eu prometo. Mas antes, eu devo mencionar que o lande estรก interpretando este texto como italiano, que estranho.', ], - }; - - const mastodonTranslation = await translator.translate( - 'Hello my friends, my name is Alex and I am american.', - '', - [], - poll, - 'en', 'pt', + 'en', ); - assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments, []); - assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); - assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); -}); - -Deno.test('Translate status WITH media_attachments and WITH poll', { - ignore: !(translationProvider === deepL && apiKey), -}, async () => { - const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - - const poll = { - 'id': '34858', - 'options': [ - { - 'title': 'Kill him right now', - }, - { - 'title': 'Save him right now', - }, - ], - }; - - const mastodonTranslation = await translator.translate( - 'Hello my friends, my name is Alex and I am american.', - '', - [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], - poll, - 'en', - 'pt', - ); - - assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); - assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); - assertEquals(mastodonTranslation.data.provider, 'DeepL.com'); + assertEquals(data.source_lang, 'pt'); + assertEquals(getLanguage(data.results[0]), 'en'); + assertEquals(getLanguage(data.results[1]), 'en'); + assertEquals(getLanguage(data.results[2]), 'en'); }); diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index b340a715..d97c59a1 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -1,12 +1,6 @@ import { z } from 'zod'; -import { - DittoTranslator, - MastodonTranslation, - Provider, - SourceLanguage, - TargetLanguage, -} from '@/translators/translator.ts'; +import { DittoTranslator, Provider, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; import { languageSchema } from '@/schema.ts'; interface DeepLTranslatorOpts { @@ -22,45 +16,43 @@ export class DeepLTranslator implements DittoTranslator { private readonly endpoint: string; private readonly apiKey: string; private readonly fetch: typeof fetch; - private readonly provider: Provider; + private static provider: Provider = 'DeepL.com'; constructor(opts: DeepLTranslatorOpts) { this.endpoint = opts.endpoint ?? 'https://api.deepl.com'; this.fetch = opts.fetch ?? globalThis.fetch; - this.provider = 'DeepL.com'; this.apiKey = opts.apiKey; } async translate( - contentHTMLencoded: string, - spoilerText: string, - mediaAttachments: { id: string; description: string }[], - poll: { id: string; options: { title: string }[] } | null, - sourceLanguage: SourceLanguage | undefined, + texts: string[], + source: SourceLanguage | undefined, + dest: TargetLanguage, + opts?: { signal?: AbortSignal }, + ) { + const data = (await this.translateMany(texts, source, dest, opts)).translations; + + return { + results: data.map((value) => value.text), + source_lang: data[0].detected_source_language, + }; + } + + /** DeepL translate request. */ + private async translateMany( + texts: string[], + source: SourceLanguage | undefined, targetLanguage: TargetLanguage, opts?: { signal?: AbortSignal }, ) { - // --------------------- START explanation - // Order of texts: - // 1 - contentHTMLencoded - // 2 - spoilerText - // 3 - mediaAttachments descriptions - // 4 - poll title options - const medias = mediaAttachments.map((value) => value.description); - - const polls = poll?.options.map((value) => value.title) ?? []; - - const text = [contentHTMLencoded, spoilerText].concat(medias, polls); - // --------------------- END explanation - const body: any = { - text, + text: texts, target_lang: targetLanguage.toUpperCase(), tag_handling: 'html', split_sentences: '1', }; - if (sourceLanguage) { - body.source_lang = sourceLanguage.toUpperCase(); + if (source) { + body.source_lang = source.toUpperCase(); } const headers = new Headers(); @@ -76,55 +68,9 @@ export class DeepLTranslator implements DittoTranslator { const response = await this.fetch(request); const json = await response.json(); - const data = DeepLTranslator.schema().parse(json).translations; + const data = DeepLTranslator.schema().parse(json); - const mastodonTranslation: MastodonTranslation = { - content: '', - spoiler_text: '', - media_attachments: [], - poll: null, - detected_source_language: 'en', - provider: this.provider, - }; - - /** Used to keep track of the offset. When slicing, should be used as the start value. */ - let startIndex = 0; - mastodonTranslation.content = data[0].text; - startIndex++; - - mastodonTranslation.spoiler_text = data[1].text; - startIndex++; - - if (medias.length) { - const mediasTranslated = data.slice(startIndex, startIndex + medias.length); - for (let i = 0; i < mediasTranslated.length; i++) { - mastodonTranslation.media_attachments.push({ - id: mediaAttachments[i].id, - description: mediasTranslated[i].text, - }); - } - startIndex += mediasTranslated.length; - } - - if (polls.length && poll) { - const pollsTranslated = data.slice(startIndex); - mastodonTranslation.poll = { - id: poll.id, - options: [], - }; - for (let i = 0; i < pollsTranslated.length; i++) { - mastodonTranslation.poll.options.push({ - title: pollsTranslated[i].text, - }); - } - startIndex += pollsTranslated.length; - } - - mastodonTranslation.detected_source_language = data[0].detected_source_language; - - return { - data: mastodonTranslation, - }; + return data; } /** DeepL response schema. @@ -139,4 +85,9 @@ export class DeepLTranslator implements DittoTranslator { ), }); } + + /** DeepL provider. */ + getProvider(): Provider { + return DeepLTranslator.provider; + } } diff --git a/src/translators/LibreTranslateTranslator.test.ts b/src/translators/LibreTranslateTranslator.test.ts index f7e89b30..6b87cc91 100644 --- a/src/translators/LibreTranslateTranslator.test.ts +++ b/src/translators/LibreTranslateTranslator.test.ts @@ -9,131 +9,44 @@ const apiKey = Conf.libreTranslateApiKey; const translationProvider = Conf.translationProvider; const libreTranslate = 'libretranslate'; -Deno.test('Translate status with EMPTY media_attachments and WITHOUT poll', { +Deno.test('LibreTranslate translation with source language omitted', { ignore: !(translationProvider === libreTranslate && apiKey), }, async () => { const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - const mastodonTranslation = await translator.translate( - 'Bom dia amigos do Element, meu nome รฉ Patrick', - '', - [], - null, - 'pt', - 'en', - ); - - assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments, []); - assertEquals(mastodonTranslation.data.poll, null); - assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); -}); - -Deno.test('Translate status WITH auto detect and with EMPTY media_attachments and WITHOUT poll', { - ignore: !(translationProvider === libreTranslate && apiKey), -}, async () => { - const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - - const mastodonTranslation = await translator.translate( - 'Bom dia amigos do Element, meu nome รฉ Patrick', - '', - [], - null, + const data = await translator.translate( + [ + 'Bom dia amigos', + 'Meu nome รฉ Patrick, um nome belo ou feio? A questรฃo รฉ mais profunda do que parece.', + 'A respiraรงรฃo รฉ mais importante do que comer e tomar agua.', + ], undefined, - 'en', + 'ca', ); - assertEquals(getLanguage(mastodonTranslation.data.content), 'en'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments, []); - assertEquals(mastodonTranslation.data.poll, null); - assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); + assertEquals(data.source_lang, 'pt'); + assertEquals(getLanguage(data.results[0]), 'ca'); + assertEquals(getLanguage(data.results[1]), 'ca'); + assertEquals(getLanguage(data.results[2]), 'ca'); }); -Deno.test('Translate status WITH media_attachments and WITHOUT poll', { +Deno.test('LibreTranslate translation with source language set', { ignore: !(translationProvider === libreTranslate && apiKey), }, async () => { const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - const mastodonTranslation = await translator.translate( - 'Hello my friends, my name is Alex and I am american.', - "That is spoiler isn't it", - [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], - null, - 'en', - 'pt', - ); - - assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); - assertEquals(getLanguage(mastodonTranslation.data.spoiler_text), 'pt'); - assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); - assertEquals(mastodonTranslation.data.poll, null); - assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); -}); - -Deno.test('Translate status WITHOUT media_attachments and WITH poll', { - ignore: !(translationProvider === libreTranslate && apiKey), -}, async () => { - const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - - const poll = { - 'id': '34858', - 'options': [ - { - 'title': 'Kill him right now', - }, - { - 'title': 'Save him right now', - }, + const data = await translator.translate( + [ + 'Bom dia amigos', + 'Meu nome รฉ Patrick, um nome belo ou feio? A questรฃo รฉ mais profunda do que parece.', + 'A respiraรงรฃo รฉ mais importante do que comer e tomar agua.', ], - }; - - const mastodonTranslation = await translator.translate( - 'Hello my friends, my name is Alex and I am american.', - '', - [], - poll, - 'en', 'pt', + 'ca', ); - assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments, []); - assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); - assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); -}); - -Deno.test('Translate status WITH media_attachments and WITH poll', { - ignore: !(translationProvider === libreTranslate && apiKey), -}, async () => { - const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); - - const poll = { - 'id': '34858', - 'options': [ - { - 'title': 'Kill him right now', - }, - { - 'title': 'Save him right now', - }, - ], - }; - - const mastodonTranslation = await translator.translate( - 'Hello my friends, my name is Alex and I am american.', - '', - [{ id: 'game', description: 'I should be playing Miles Edgeworth with my wife' }], - poll, - 'en', - 'pt', - ); - - assertEquals(getLanguage(mastodonTranslation.data.content), 'pt'); - assertEquals(mastodonTranslation.data.spoiler_text, ''); - assertEquals(mastodonTranslation.data.media_attachments.map((value) => getLanguage(value.description)), ['pt']); - assertEquals(mastodonTranslation.data.poll?.options.map((value) => getLanguage(value.title)), ['pt', 'pt']); - assertEquals(mastodonTranslation.data.provider, 'libretranslate.com'); + assertEquals(data.source_lang, 'pt'); + assertEquals(getLanguage(data.results[0]), 'ca'); + assertEquals(getLanguage(data.results[1]), 'ca'); + assertEquals(getLanguage(data.results[2]), 'ca'); }); diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index 80f44479..d632c71e 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -1,12 +1,8 @@ +import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { - DittoTranslator, - MastodonTranslation, - Provider, - SourceLanguage, - TargetLanguage, -} from '@/translators/translator.ts'; +import { DittoTranslator, Provider, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { languageSchema } from '@/schema.ts'; interface LibreTranslateTranslatorOpts { /** Libretranslate endpoint to use. Default: 'https://libretranslate.com' */ @@ -21,97 +17,37 @@ export class LibreTranslateTranslator implements DittoTranslator { private readonly endpoint: string; private readonly apiKey: string; private readonly fetch: typeof fetch; - private readonly provider: Provider; + private static provider: Provider = 'libretranslate.com'; constructor(opts: LibreTranslateTranslatorOpts) { this.endpoint = opts.endpoint ?? 'https://libretranslate.com'; this.fetch = opts.fetch ?? globalThis.fetch; - this.provider = 'libretranslate.com'; this.apiKey = opts.apiKey; } async translate( - contentHTMLencoded: string, - spoilerText: string, - mediaAttachments: { id: string; description: string }[], - poll: { id: string; options: { title: string }[] } | null, - sourceLanguage: SourceLanguage | undefined, - targetLanguage: TargetLanguage, + texts: string[], + source: SourceLanguage | undefined, + dest: TargetLanguage, opts?: { signal?: AbortSignal }, ) { - const mastodonTranslation: MastodonTranslation = { - content: '', - spoiler_text: '', - media_attachments: [], - poll: null, - detected_source_language: 'en', - provider: this.provider, - }; - - const translatedContent = await this.makeRequest(contentHTMLencoded, sourceLanguage, targetLanguage, 'html', { - signal: opts?.signal, - }); - mastodonTranslation.content = translatedContent; - - if (spoilerText.length) { - const translatedSpoilerText = await this.makeRequest(spoilerText, sourceLanguage, targetLanguage, 'text', { - signal: opts?.signal, - }); - mastodonTranslation.spoiler_text = translatedSpoilerText; - } - - if (mediaAttachments) { - for (const media of mediaAttachments) { - const translatedDescription = await this.makeRequest( - media.description, - sourceLanguage, - targetLanguage, - 'text', - { - signal: opts?.signal, - }, - ); - mastodonTranslation.media_attachments.push({ - id: media.id, - description: translatedDescription, - }); - } - } - - if (poll) { - mastodonTranslation.poll = { - id: poll.id, - options: [], - }; - - for (const option of poll.options) { - const translatedTitle = await this.makeRequest( - option.title, - sourceLanguage, - targetLanguage, - 'text', - { - signal: opts?.signal, - }, - ); - mastodonTranslation.poll.options.push({ - title: translatedTitle, - }); - } - } + const translations = await Promise.all( + texts.map((text) => this.translateOne(text, source, dest, 'html', { signal: opts?.signal })), + ); return { - data: mastodonTranslation, + results: translations.map((value) => value.translatedText), + source_lang: translations[0]?.detectedLanguage?.language ?? source as LanguageCode, // cast is ok }; } - private async makeRequest( + private async translateOne( q: string, sourceLanguage: string | undefined, targetLanguage: string, format: 'html' | 'text', opts?: { signal?: AbortSignal }, - ): Promise { + ) { const body = { q, source: sourceLanguage?.toLowerCase() ?? 'auto', @@ -132,7 +68,7 @@ export class LibreTranslateTranslator implements DittoTranslator { const response = await this.fetch(request); const json = await response.json(); - const data = LibreTranslateTranslator.schema().parse(json).translatedText; + const data = LibreTranslateTranslator.schema().parse(json); return data; } @@ -142,6 +78,15 @@ export class LibreTranslateTranslator implements DittoTranslator { private static schema() { return z.object({ translatedText: z.string(), + /** This field is only available if the 'source' is set to 'auto' */ + detectedLanguage: z.object({ + language: languageSchema, + }).optional(), }); } + + /** LibreTranslate provider. */ + getProvider(): Provider { + return LibreTranslateTranslator.provider; + } } diff --git a/src/translators/translator.ts b/src/translators/translator.ts index 515b335f..29874964 100644 --- a/src/translators/translator.ts +++ b/src/translators/translator.ts @@ -36,21 +36,15 @@ export type MastodonTranslation = { export interface DittoTranslator { /** Translate the 'content' into 'targetLanguage'. */ translate( - /** HTML-encoded content of the status. */ - content: string, - /** Spoiler warning of the status. */ - spoilerText: string, - /** Media descriptions of the status. */ - mediaAttachments: { id: string; description: string }[], - /** Poll of the status. */ - poll: { id: string; options: { title: string }[] } | null, + texts: string[], /** The language of the source text/status. */ sourceLanguage: SourceLanguage | undefined, /** The status content will be translated into this language. */ targetLanguage: TargetLanguage, /** Custom options. */ opts?: { signal?: AbortSignal }, - ): Promise; + ): Promise<{ results: string[]; source_lang: SourceLanguage }>; + getProvider(): Provider; } /** Includes the TARGET language and the status id. From 22fa3f944c77b7cc2183520500ded44dbd89595f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 15:15:32 -0300 Subject: [PATCH 36/64] chore: update nostrify:db --- deno.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deno.lock b/deno.lock index 0a0d9e31..a00d4d31 100644 --- a/deno.lock +++ b/deno.lock @@ -22,7 +22,7 @@ "jsr:@gleasonator/policy@0.8.0": "0.8.0", "jsr:@hono/hono@^4.4.6": "4.6.2", "jsr:@lambdalisue/async@^2.1.1": "2.1.1", - "jsr:@nostrify/db@0.35": "0.35.0", + "jsr:@nostrify/db@~0.36.1": "0.36.1", "jsr:@nostrify/nostrify@0.31": "0.31.0", "jsr:@nostrify/nostrify@0.32": "0.32.0", "jsr:@nostrify/nostrify@0.35": "0.35.0", @@ -270,10 +270,10 @@ "@lambdalisue/async@2.1.1": { "integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4" }, - "@nostrify/db@0.35.0": { - "integrity": "637191c41812544e361b7997dc44ea098f8bd7efebb28f37a8a7142a0ecada8d", + "@nostrify/db@0.36.1": { + "integrity": "b65b89ca6fe98d9dbcc0402b5c9c07b8430c2c91f84ba4128ff2eeed70c3d49f", "dependencies": [ - "jsr:@nostrify/nostrify@0.35", + "jsr:@nostrify/nostrify@0.36", "jsr:@nostrify/types@0.35", "npm:kysely@~0.27.3", "npm:nostr-tools@^2.7.0" @@ -2048,7 +2048,7 @@ "jsr:@gfx/canvas-wasm@~0.4.2", "jsr:@hono/hono@^4.4.6", "jsr:@lambdalisue/async@^2.1.1", - "jsr:@nostrify/db@0.35", + "jsr:@nostrify/db@~0.36.1", "jsr:@nostrify/nostrify@0.36", "jsr:@nostrify/policies@0.35", "jsr:@soapbox/kysely-pglite@1", From 57bbbb289b1a91a6d2af52fdddf2b477373d402a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 15:22:09 -0300 Subject: [PATCH 37/64] fix: types must have the type prefix apparently happens in Deno 2.0? --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index e58535c0..39d29a04 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { Context, Env as HonoEnv, Handler, Hono, Input as HonoInput, MiddlewareHandler } from '@hono/hono'; +import { type Context, Env as HonoEnv, Handler, Hono, Input as HonoInput, MiddlewareHandler } from '@hono/hono'; import { cors } from '@hono/hono/cors'; import { serveStatic } from '@hono/hono/deno'; import { logger } from '@hono/hono/logger'; From cad0da27320bbc0c3f0f29b7405083eb895b9cc9 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 15:24:01 -0300 Subject: [PATCH 38/64] feat: rateLimit translate endpoint --- src/app.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 39d29a04..6659169f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -227,7 +227,13 @@ app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requireSigner, bookmarkC app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requireSigner, unbookmarkController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requireSigner, pinController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requireSigner, unpinController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/translate', requireSigner, translatorMiddleware, translateController); +app.post( + '/api/v1/statuses/:id{[0-9a-f]{64}}/translate', + requireSigner, + rateLimitMiddleware(30, Time.minutes(1)), + translatorMiddleware, + translateController, +); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requireSigner, reblogStatusController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requireSigner, unreblogStatusController); app.post('/api/v1/statuses', requireSigner, createStatusController); From 54fa38795a7416afbb49d3698afcea4e3e1d3bd0 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 18:46:49 -0300 Subject: [PATCH 39/64] feat: return translation not supported error to the frontend --- src/controllers/api/translate.ts | 5 ++++- src/translators/DeepLTranslator.ts | 3 +++ src/translators/LibreTranslateTranslator.ts | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index f2ca9eae..4578c7fb 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -132,7 +132,10 @@ const translateController: AppController = async (c) => { dittoTranslations.set(translatedId, { data: mastodonTranslation }); return c.json(mastodonTranslation, 200); - } catch { + } catch (e: any) { + if (e.message?.includes('not supported') || e.error?.includes('not supported')) { + return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); + } return c.json({ error: 'Service Unavailable' }, 503); } }; diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index d97c59a1..4307898c 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -68,6 +68,9 @@ export class DeepLTranslator implements DittoTranslator { const response = await this.fetch(request); const json = await response.json(); + if (!response.ok) { + throw json; + } const data = DeepLTranslator.schema().parse(json); return data; diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index d632c71e..0e6bd55a 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -68,6 +68,9 @@ export class LibreTranslateTranslator implements DittoTranslator { const response = await this.fetch(request); const json = await response.json(); + if (!response.ok) { + throw json; + } const data = LibreTranslateTranslator.schema().parse(json); return data; From 87e0f594dfa036d735102b1aa17e0516d4f79111 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 19:17:03 -0300 Subject: [PATCH 40/64] refactor: throw new Error to have benefit of stack trace --- src/controllers/api/translate.ts | 2 +- src/translators/DeepLTranslator.ts | 2 +- src/translators/LibreTranslateTranslator.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 4578c7fb..5f229e0e 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -133,7 +133,7 @@ const translateController: AppController = async (c) => { dittoTranslations.set(translatedId, { data: mastodonTranslation }); return c.json(mastodonTranslation, 200); } catch (e: any) { - if (e.message?.includes('not supported') || e.error?.includes('not supported')) { + if (e.message?.includes('not supported')) { return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); } return c.json({ error: 'Service Unavailable' }, 503); diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index 4307898c..5c457903 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -69,7 +69,7 @@ export class DeepLTranslator implements DittoTranslator { const response = await this.fetch(request); const json = await response.json(); if (!response.ok) { - throw json; + throw new Error(json['message']); } const data = DeepLTranslator.schema().parse(json); diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index 0e6bd55a..a7d4eb85 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -69,7 +69,7 @@ export class LibreTranslateTranslator implements DittoTranslator { const response = await this.fetch(request); const json = await response.json(); if (!response.ok) { - throw json; + throw new Error(json['error']); } const data = LibreTranslateTranslator.schema().parse(json); From b86bd81ed2be71050a934808e20dcb221a08aefe Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 9 Oct 2024 19:19:54 -0300 Subject: [PATCH 41/64] refactor: cast correct error type --- src/controllers/api/translate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 5f229e0e..e1577478 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -132,8 +132,8 @@ const translateController: AppController = async (c) => { dittoTranslations.set(translatedId, { data: mastodonTranslation }); return c.json(mastodonTranslation, 200); - } catch (e: any) { - if (e.message?.includes('not supported')) { + } catch (e) { + if (e instanceof Error && e.message?.includes('not supported')) { return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); } return c.json({ error: 'Service Unavailable' }, 503); From 4d146dcc9804c879e9839e89c26d7f57ef51b04a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 17:58:11 -0500 Subject: [PATCH 42/64] Add names to web workers --- src/workers/fetch.ts | 2 +- src/workers/policy.ts | 1 + src/workers/verify.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/workers/fetch.ts b/src/workers/fetch.ts index 4fbc57bb..bb5588ed 100644 --- a/src/workers/fetch.ts +++ b/src/workers/fetch.ts @@ -5,7 +5,7 @@ import './handlers/abortsignal.ts'; import { fetchResponsesCounter } from '@/metrics.ts'; -const worker = new Worker(new URL('./fetch.worker.ts', import.meta.url), { type: 'module' }); +const worker = new Worker(new URL('./fetch.worker.ts', import.meta.url), { type: 'module', name: 'fetchWorker' }); const client = Comlink.wrap(worker); // Wait for the worker to be ready before we start using it. diff --git a/src/workers/policy.ts b/src/workers/policy.ts index c0beb4d7..a396468f 100644 --- a/src/workers/policy.ts +++ b/src/workers/policy.ts @@ -20,6 +20,7 @@ class PolicyWorker implements NPolicy { new URL('./policy.worker.ts', import.meta.url), { type: 'module', + name: 'PolicyWorker', // FIXME: Disabled until Deno 2.0 adds support for `import` permission here. // https://github.com/denoland/deno/issues/26074 // deno: { diff --git a/src/workers/verify.ts b/src/workers/verify.ts index 15ad783a..c0a2fea3 100644 --- a/src/workers/verify.ts +++ b/src/workers/verify.ts @@ -4,7 +4,7 @@ import * as Comlink from 'comlink'; import type { VerifyWorker } from './verify.worker.ts'; const worker = Comlink.wrap( - new Worker(new URL('./verify.worker.ts', import.meta.url), { type: 'module' }), + new Worker(new URL('./verify.worker.ts', import.meta.url), { type: 'module', name: 'verifyEventWorker' }), ); function verifyEventWorker(event: NostrEvent): Promise { From d4767f56c6f832fb4efca4aa65eb670daa138840 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 20:17:04 -0500 Subject: [PATCH 43/64] Enable translations dynamically depending on whether a TRANSLATION_PROVIDER is set --- src/controllers/api/instance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index c8f57f06..f6ae758c 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -129,7 +129,7 @@ const instanceV2Controller: AppController = async (c) => { max_expiration: 2629746, }, translation: { - enabled: true, + enabled: Boolean(Conf.translationProvider), }, }, nostr: { From c048cda2e5df72eedcb1445f560bd43917cb3620 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 20:22:12 -0500 Subject: [PATCH 44/64] Upgrade @gleasonator/policy in deno.lock, increase policy db timeout Fixes https://gitlab.com/soapbox-pub/ditto/-/issues/244 --- deno.lock | 17 +++++++++++++++++ src/workers/policy.worker.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/deno.lock b/deno.lock index a00d4d31..26b6549c 100644 --- a/deno.lock +++ b/deno.lock @@ -20,6 +20,7 @@ "jsr:@gleasonator/policy@0.7.0": "0.7.0", "jsr:@gleasonator/policy@0.7.1": "0.7.1", "jsr:@gleasonator/policy@0.8.0": "0.8.0", + "jsr:@gleasonator/policy@0.8.1": "0.8.1", "jsr:@hono/hono@^4.4.6": "4.6.2", "jsr:@lambdalisue/async@^2.1.1": "2.1.1", "jsr:@nostrify/db@~0.36.1": "0.36.1", @@ -35,6 +36,7 @@ "jsr:@nostrify/policies@0.35": "0.35.0", "jsr:@nostrify/policies@0.36": "0.36.0", "jsr:@nostrify/policies@~0.33.1": "0.33.1", + "jsr:@nostrify/policies@~0.36.1": "0.36.1", "jsr:@nostrify/types@0.30": "0.30.1", "jsr:@nostrify/types@0.35": "0.35.0", "jsr:@nostrify/types@~0.30.1": "0.30.1", @@ -240,6 +242,13 @@ "jsr:@nostrify/policies@0.36" ] }, + "@gleasonator/policy@0.8.1": { + "integrity": "32bc3da39f9fa0f8c4ab1d1d6f7ddf44c3c19ffbca34a7344b8ae9233e852fdb", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/policies@~0.36.1" + ] + }, "@hono/hono@4.4.6": { "integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453" }, @@ -415,6 +424,14 @@ "npm:nostr-tools@^2.7.0" ] }, + "@nostrify/policies@0.36.1": { + "integrity": "6d59af115a687fcd18b6caebab0e4f50ee6cdb0aafa2aacd0aec2065021275b4", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/types@0.35", + "npm:nostr-tools@^2.7.0" + ] + }, "@nostrify/types@0.30.0": { "integrity": "1f38fa849cff930bd709edbf94ef9ac02f46afb8b851f86c8736517b354616da" }, diff --git a/src/workers/policy.worker.ts b/src/workers/policy.worker.ts index 86cea87c..c5b4129d 100644 --- a/src/workers/policy.worker.ts +++ b/src/workers/policy.worker.ts @@ -37,7 +37,7 @@ export class CustomPolicy implements NPolicy { const store = new EventsDB({ kysely, pubkey, - timeout: 1_000, + timeout: 5_000, }); this.policy = new Policy({ store, pubkey }); From 33d2eb6ca3435131cb7ada06158365c24d86768b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 21:02:55 -0500 Subject: [PATCH 45/64] Remove Provider type (it's just a string) --- src/translators/DeepLTranslator.ts | 9 +++++---- src/translators/LibreTranslateTranslator.ts | 8 ++++---- src/translators/translator.ts | 7 ++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index 5c457903..f30d414a 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -1,6 +1,7 @@ +import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { DittoTranslator, Provider, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; import { languageSchema } from '@/schema.ts'; interface DeepLTranslatorOpts { @@ -16,7 +17,7 @@ export class DeepLTranslator implements DittoTranslator { private readonly endpoint: string; private readonly apiKey: string; private readonly fetch: typeof fetch; - private static provider: Provider = 'DeepL.com'; + private static provider = 'DeepL.com'; constructor(opts: DeepLTranslatorOpts) { this.endpoint = opts.endpoint ?? 'https://api.deepl.com'; @@ -34,7 +35,7 @@ export class DeepLTranslator implements DittoTranslator { return { results: data.map((value) => value.text), - source_lang: data[0].detected_source_language, + source_lang: data[0].detected_source_language as LanguageCode, }; } @@ -90,7 +91,7 @@ export class DeepLTranslator implements DittoTranslator { } /** DeepL provider. */ - getProvider(): Provider { + getProvider(): string { return DeepLTranslator.provider; } } diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index a7d4eb85..2c201575 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -1,7 +1,7 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { DittoTranslator, Provider, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; import { languageSchema } from '@/schema.ts'; interface LibreTranslateTranslatorOpts { @@ -17,7 +17,7 @@ export class LibreTranslateTranslator implements DittoTranslator { private readonly endpoint: string; private readonly apiKey: string; private readonly fetch: typeof fetch; - private static provider: Provider = 'libretranslate.com'; + private static provider = 'libretranslate.com'; constructor(opts: LibreTranslateTranslatorOpts) { this.endpoint = opts.endpoint ?? 'https://libretranslate.com'; @@ -37,7 +37,7 @@ export class LibreTranslateTranslator implements DittoTranslator { return { results: translations.map((value) => value.translatedText), - source_lang: translations[0]?.detectedLanguage?.language ?? source as LanguageCode, // cast is ok + source_lang: (translations[0]?.detectedLanguage?.language ?? source) as LanguageCode, // cast is ok }; } @@ -89,7 +89,7 @@ export class LibreTranslateTranslator implements DittoTranslator { } /** LibreTranslate provider. */ - getProvider(): Provider { + getProvider(): string { return LibreTranslateTranslator.provider; } } diff --git a/src/translators/translator.ts b/src/translators/translator.ts index 29874964..98adf5ee 100644 --- a/src/translators/translator.ts +++ b/src/translators/translator.ts @@ -3,9 +3,6 @@ import { LRUCache } from 'lru-cache'; import { Time } from '@/utils/time.ts'; -/** Supported providers. */ -export type Provider = 'DeepL.com' | 'libretranslate.com'; - /** Original language of the post */ export type SourceLanguage = LanguageCode; @@ -29,7 +26,7 @@ export type MastodonTranslation = { //** The language of the source text, as auto-detected by the machine translation provider. */ detected_source_language: SourceLanguage; /** The service that provided the machine translation. */ - provider: Provider; + provider: string; }; /** DittoTranslator class, used for status translation. */ @@ -44,7 +41,7 @@ export interface DittoTranslator { /** Custom options. */ opts?: { signal?: AbortSignal }, ): Promise<{ results: string[]; source_lang: SourceLanguage }>; - getProvider(): Provider; + getProvider(): string; } /** Includes the TARGET language and the status id. From df8004e2610f79276acbeb29bd2c3e0ba2b49afb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Oct 2024 21:04:05 -0500 Subject: [PATCH 46/64] Decrease rate limit of translations --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 6659169f..3f6f7413 100644 --- a/src/app.ts +++ b/src/app.ts @@ -230,7 +230,7 @@ app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requireSigner, unpinControl app.post( '/api/v1/statuses/:id{[0-9a-f]{64}}/translate', requireSigner, - rateLimitMiddleware(30, Time.minutes(1)), + rateLimitMiddleware(15, Time.minutes(1)), translatorMiddleware, translateController, ); From 81c28825a8f96d2a9e3d64409b84e5fa4d2158ac Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 13:20:03 -0500 Subject: [PATCH 47/64] Record HTTP response time metrics --- deno.json | 1 + deno.lock | 5 +++++ src/metrics.ts | 6 ++++++ src/middleware/metricsMiddleware.ts | 11 ++++++++++- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 201bff33..1a444b9f 100644 --- a/deno.json +++ b/deno.json @@ -36,6 +36,7 @@ "@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.8", + "@esroyo/scoped-performance": "jsr:@esroyo/scoped-performance@^3.1.0", "@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2", "@hono/hono": "jsr:@hono/hono@^4.4.6", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", diff --git a/deno.lock b/deno.lock index 26b6549c..c97291c1 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,7 @@ "jsr:@b-fuze/deno-dom@~0.1.47": "0.1.48", "jsr:@bradenmacdonald/s3-lite-client@~0.7.4": "0.7.6", "jsr:@denosaurs/plug@1.0.3": "1.0.3", + "jsr:@esroyo/scoped-performance@^3.1.0": "3.1.0", "jsr:@gfx/canvas-wasm@~0.4.2": "0.4.2", "jsr:@gleasonator/policy@*": "0.2.0", "jsr:@gleasonator/policy@0.2.0": "0.2.0", @@ -142,6 +143,9 @@ "jsr:@std/path@0.213.1" ] }, + "@esroyo/scoped-performance@3.1.0": { + "integrity": "e6a12a1d4edb32cbea7afce005123c1ef684e1815bf9b6caadfbf3e89fe66222" + }, "@gfx/canvas-wasm@0.4.2": { "integrity": "d653be3bd12cb2fa9bbe5d1b1f041a81b91d80b68502761204aaf60e4592532a", "dependencies": [ @@ -2062,6 +2066,7 @@ "dependencies": [ "jsr:@b-fuze/deno-dom@~0.1.47", "jsr:@bradenmacdonald/s3-lite-client@~0.7.4", + "jsr:@esroyo/scoped-performance@^3.1.0", "jsr:@gfx/canvas-wasm@~0.4.2", "jsr:@hono/hono@^4.4.6", "jsr:@lambdalisue/async@^2.1.1", diff --git a/src/metrics.ts b/src/metrics.ts index 633d72f0..42bcbd42 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -12,6 +12,12 @@ export const httpResponsesCounter = new Counter({ labelNames: ['method', 'path', 'status'], }); +export const httpResponseDurationHistogram = new Histogram({ + name: 'ditto_http_response_duration_seconds', + help: 'Histogram of HTTP response times in seconds', + labelNames: ['method', 'path', 'status'], +}); + export const streamingConnectionsGauge = new Gauge({ name: 'ditto_streaming_connections', help: 'Number of active connections to the streaming API', diff --git a/src/middleware/metricsMiddleware.ts b/src/middleware/metricsMiddleware.ts index e8a30972..0b213b82 100644 --- a/src/middleware/metricsMiddleware.ts +++ b/src/middleware/metricsMiddleware.ts @@ -1,9 +1,14 @@ +import { ScopedPerformance } from '@esroyo/scoped-performance'; import { MiddlewareHandler } from '@hono/hono'; -import { httpRequestsCounter, httpResponsesCounter } from '@/metrics.ts'; +import { httpRequestsCounter, httpResponseDurationHistogram, httpResponsesCounter } from '@/metrics.ts'; /** Prometheus metrics middleware that tracks HTTP requests by methods and responses by status code. */ export const metricsMiddleware: MiddlewareHandler = async (c, next) => { + // Start a timer to measure the duration of the response. + using perf = new ScopedPerformance(); + perf.mark('start'); + // HTTP Request. const { method } = c.req; httpRequestsCounter.inc({ method }); @@ -17,4 +22,8 @@ export const metricsMiddleware: MiddlewareHandler = async (c, next) => { // Tries to find actual route names first before falling back on potential middleware handlers like `app.use('*')`. const path = c.req.matchedRoutes.find((r) => r.method !== 'ALL')?.path ?? c.req.routePath; httpResponsesCounter.inc({ method, status, path }); + + // Measure the duration of the response. + const { duration } = perf.measure('total', 'start'); + httpResponseDurationHistogram.observe({ method, status, path }, duration / 1000); }; From 6107ce88e2a8f4f0b389a31d7affd8e2e328c98a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 13:20:48 -0500 Subject: [PATCH 48/64] metrics: ditto_db_query_duration_ms -> ditto_db_query_duration_seconds --- src/db/KyselyLogger.ts | 6 ++++-- src/metrics.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/db/KyselyLogger.ts b/src/db/KyselyLogger.ts index 72167e21..514f44a4 100644 --- a/src/db/KyselyLogger.ts +++ b/src/db/KyselyLogger.ts @@ -9,12 +9,14 @@ export const KyselyLogger: Logger = (event) => { const { query, queryDurationMillis } = event; const { sql, parameters } = query; + const queryDurationSeconds = queryDurationMillis / 1000; + dbQueriesCounter.inc(); - dbQueryDurationHistogram.observe(queryDurationMillis); + dbQueryDurationHistogram.observe(queryDurationSeconds); console.debug( sql, JSON.stringify(parameters), - `\x1b[90m(${(queryDurationMillis / 1000).toFixed(2)}s)\x1b[0m`, + `\x1b[90m(${(queryDurationSeconds / 1000).toFixed(2)}s)\x1b[0m`, ); }; diff --git a/src/metrics.ts b/src/metrics.ts index 42bcbd42..c005a6c7 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -97,7 +97,7 @@ export const dbAvailableConnectionsGauge = new Gauge({ }); export const dbQueryDurationHistogram = new Histogram({ - name: 'ditto_db_query_duration_ms', + name: 'ditto_db_query_duration_seconds', help: 'Duration of database queries', }); From b6f9fe57707c0fe4372c8550fe7619edb4e08a84 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 13:26:05 -0500 Subject: [PATCH 49/64] grafana: ditto_db_query_duration_ms -> ditto_db_query_duration_seconds --- grafana/Ditto-Dashboard.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana/Ditto-Dashboard.json b/grafana/Ditto-Dashboard.json index d722d5d9..3c03f23d 100644 --- a/grafana/Ditto-Dashboard.json +++ b/grafana/Ditto-Dashboard.json @@ -879,7 +879,7 @@ "uid": "${prometheus}" }, "editorMode": "code", - "expr": "increase(ditto_db_query_duration_ms_sum[$__rate_interval])", + "expr": "increase(ditto_db_query_duration_seconds_sum[$__rate_interval])", "instant": false, "legendFormat": "Time", "range": true, From 9469fff6ac4cd95bdb99a0c37b763c6c32af392f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 13:48:50 -0500 Subject: [PATCH 50/64] Rename translation variables from _ENDPOINT to _BASE_URL --- src/config.ts | 12 +++--- src/middleware/translatorMiddleware.ts | 37 +++++++------------ src/translators/DeepLTranslator.test.ts | 19 ++++++---- src/translators/DeepLTranslator.ts | 37 +++++++++---------- .../LibreTranslateTranslator.test.ts | 19 ++++++---- src/translators/LibreTranslateTranslator.ts | 16 +++----- src/translators/translator.ts | 3 +- 7 files changed, 66 insertions(+), 77 deletions(-) diff --git a/src/config.ts b/src/config.ts index 0e4fc816..806fe196 100644 --- a/src/config.ts +++ b/src/config.ts @@ -276,19 +276,19 @@ class Conf { return Deno.env.get('TRANSLATION_PROVIDER'); } /** DeepL URL endpoint. */ - static get deepLendpoint(): string | undefined { - return Deno.env.get('DEEPL_ENDPOINT'); + static get deeplBaseUrl(): string | undefined { + return Deno.env.get('DEEPL_BASE_URL'); } /** DeepL API KEY. */ - static get deepLapiKey(): string | undefined { + static get deeplApiKey(): string | undefined { return Deno.env.get('DEEPL_API_KEY'); } /** LibreTranslate URL endpoint. */ - static get libreTranslateEndpoint(): string | undefined { - return Deno.env.get('LIBRETRANSLATE_ENDPOINT'); + static get libretranslateBaseUrl(): string | undefined { + return Deno.env.get('LIBRETRANSLATE_BASE_URL'); } /** LibreTranslate API KEY. */ - static get libreTranslateApiKey(): string | undefined { + static get libretranslateApiKey(): string | undefined { return Deno.env.get('LIBRETRANSLATE_API_KEY'); } /** Cache settings. */ diff --git a/src/middleware/translatorMiddleware.ts b/src/middleware/translatorMiddleware.ts index b8a07686..f5a6baa2 100644 --- a/src/middleware/translatorMiddleware.ts +++ b/src/middleware/translatorMiddleware.ts @@ -6,33 +6,22 @@ import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator /** Set the translator used for translating posts. */ export const translatorMiddleware: AppMiddleware = async (c, next) => { - const deepLendpoint = Conf.deepLendpoint; - const deepLapiKey = Conf.deepLapiKey; - const libreTranslateEndpoint = Conf.libreTranslateEndpoint; - const libreTranslateApiKey = Conf.libreTranslateApiKey; - const translationProvider = Conf.translationProvider; + switch (Conf.translationProvider) { + case 'deepl': { + const { deeplApiKey: apiKey, deeplBaseUrl: baseUrl } = Conf; + if (apiKey) { + c.set('translator', new DeepLTranslator({ baseUrl, apiKey, fetch: fetchWorker })); + } + break; + } - switch (translationProvider) { - case 'deepl': - if (deepLapiKey) { - c.set( - 'translator', - new DeepLTranslator({ endpoint: deepLendpoint, apiKey: deepLapiKey, fetch: fetchWorker }), - ); - } - break; - case 'libretranslate': - if (libreTranslateApiKey) { - c.set( - 'translator', - new LibreTranslateTranslator({ - endpoint: libreTranslateEndpoint, - apiKey: libreTranslateApiKey, - fetch: fetchWorker, - }), - ); + case 'libretranslate': { + const { libretranslateApiKey: apiKey, libretranslateBaseUrl: baseUrl } = Conf; + if (apiKey) { + c.set('translator', new LibreTranslateTranslator({ baseUrl, apiKey, fetch: fetchWorker })); } break; + } } await next(); diff --git a/src/translators/DeepLTranslator.test.ts b/src/translators/DeepLTranslator.test.ts index 385c10fc..d78c0a0a 100644 --- a/src/translators/DeepLTranslator.test.ts +++ b/src/translators/DeepLTranslator.test.ts @@ -4,15 +4,18 @@ import { Conf } from '@/config.ts'; import { DeepLTranslator } from '@/translators/DeepLTranslator.ts'; import { getLanguage } from '@/test.ts'; -const endpoint = Conf.deepLendpoint; -const apiKey = Conf.deepLapiKey; -const translationProvider = Conf.translationProvider; -const deepL = 'deepl'; +const { + deeplBaseUrl: baseUrl, + deeplApiKey: apiKey, + translationProvider, +} = Conf; + +const deepl = 'deepl'; Deno.test('DeepL translation with source language omitted', { - ignore: !(translationProvider === deepL && apiKey), + ignore: !(translationProvider === deepl && apiKey), }, async () => { - const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + const translator = new DeepLTranslator({ fetch: fetch, baseUrl, apiKey: apiKey! }); const data = await translator.translate( [ @@ -31,9 +34,9 @@ Deno.test('DeepL translation with source language omitted', { }); Deno.test('DeepL translation with source language set', { - ignore: !(translationProvider === deepL && apiKey), + ignore: !(translationProvider === deepl && apiKey), }, async () => { - const translator = new DeepLTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + const translator = new DeepLTranslator({ fetch: fetch, baseUrl, apiKey: apiKey as string }); const data = await translator.translate( [ diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index f30d414a..2b4da175 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -5,8 +5,8 @@ import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/t import { languageSchema } from '@/schema.ts'; interface DeepLTranslatorOpts { - /** DeepL endpoint to use. Default: 'https://api.deepl.com' */ - endpoint?: string; + /** DeepL base URL to use. Default: 'https://api.deepl.com' */ + baseUrl?: string; /** DeepL API key. */ apiKey: string; /** Custom fetch implementation. */ @@ -14,13 +14,14 @@ interface DeepLTranslatorOpts { } export class DeepLTranslator implements DittoTranslator { - private readonly endpoint: string; + private readonly baseUrl: string; private readonly apiKey: string; private readonly fetch: typeof fetch; - private static provider = 'DeepL.com'; + + readonly provider = 'DeepL.com'; constructor(opts: DeepLTranslatorOpts) { - this.endpoint = opts.endpoint ?? 'https://api.deepl.com'; + this.baseUrl = opts.baseUrl ?? 'https://api.deepl.com'; this.fetch = opts.fetch ?? globalThis.fetch; this.apiKey = opts.apiKey; } @@ -31,11 +32,11 @@ export class DeepLTranslator implements DittoTranslator { dest: TargetLanguage, opts?: { signal?: AbortSignal }, ) { - const data = (await this.translateMany(texts, source, dest, opts)).translations; + const { translations } = await this.translateMany(texts, source, dest, opts); return { - results: data.map((value) => value.text), - source_lang: data[0].detected_source_language as LanguageCode, + results: translations.map((value) => value.text), + source_lang: translations[0]?.detected_source_language as LanguageCode, }; } @@ -56,25 +57,26 @@ export class DeepLTranslator implements DittoTranslator { body.source_lang = source.toUpperCase(); } - const headers = new Headers(); - headers.append('Authorization', 'DeepL-Auth-Key' + ' ' + this.apiKey); - headers.append('Content-Type', 'application/json'); + const url = new URL('/v2/translate', this.baseUrl); - const request = new Request(this.endpoint + '/v2/translate', { + const request = new Request(url, { method: 'POST', body: JSON.stringify(body), - headers, + headers: { + 'Authorization': `DeepL-Auth-Key ${this.apiKey}`, + 'Content-Type': 'application/json', + }, signal: opts?.signal, }); const response = await this.fetch(request); const json = await response.json(); + if (!response.ok) { throw new Error(json['message']); } - const data = DeepLTranslator.schema().parse(json); - return data; + return DeepLTranslator.schema().parse(json); } /** DeepL response schema. @@ -89,9 +91,4 @@ export class DeepLTranslator implements DittoTranslator { ), }); } - - /** DeepL provider. */ - getProvider(): string { - return DeepLTranslator.provider; - } } diff --git a/src/translators/LibreTranslateTranslator.test.ts b/src/translators/LibreTranslateTranslator.test.ts index 6b87cc91..edda3039 100644 --- a/src/translators/LibreTranslateTranslator.test.ts +++ b/src/translators/LibreTranslateTranslator.test.ts @@ -4,15 +4,18 @@ import { Conf } from '@/config.ts'; import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator.ts'; import { getLanguage } from '@/test.ts'; -const endpoint = Conf.libreTranslateEndpoint; -const apiKey = Conf.libreTranslateApiKey; -const translationProvider = Conf.translationProvider; -const libreTranslate = 'libretranslate'; +const { + libretranslateBaseUrl: baseUrl, + libretranslateApiKey: apiKey, + translationProvider, +} = Conf; + +const libretranslate = 'libretranslate'; Deno.test('LibreTranslate translation with source language omitted', { - ignore: !(translationProvider === libreTranslate && apiKey), + ignore: !(translationProvider === libretranslate && apiKey), }, async () => { - const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + const translator = new LibreTranslateTranslator({ fetch: fetch, baseUrl, apiKey: apiKey! }); const data = await translator.translate( [ @@ -31,9 +34,9 @@ Deno.test('LibreTranslate translation with source language omitted', { }); Deno.test('LibreTranslate translation with source language set', { - ignore: !(translationProvider === libreTranslate && apiKey), + ignore: !(translationProvider === libretranslate && apiKey), }, async () => { - const translator = new LibreTranslateTranslator({ fetch: fetch, endpoint, apiKey: apiKey as string }); + const translator = new LibreTranslateTranslator({ fetch: fetch, baseUrl, apiKey: apiKey! }); const data = await translator.translate( [ diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index 2c201575..bd2850ff 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -6,7 +6,7 @@ import { languageSchema } from '@/schema.ts'; interface LibreTranslateTranslatorOpts { /** Libretranslate endpoint to use. Default: 'https://libretranslate.com' */ - endpoint?: string; + baseUrl?: string; /** Libretranslate API key. */ apiKey: string; /** Custom fetch implementation. */ @@ -14,13 +14,14 @@ interface LibreTranslateTranslatorOpts { } export class LibreTranslateTranslator implements DittoTranslator { - private readonly endpoint: string; + private readonly baseUrl: string; private readonly apiKey: string; private readonly fetch: typeof fetch; - private static provider = 'libretranslate.com'; + + readonly provider = 'libretranslate.com'; constructor(opts: LibreTranslateTranslatorOpts) { - this.endpoint = opts.endpoint ?? 'https://libretranslate.com'; + this.baseUrl = opts.baseUrl ?? 'https://libretranslate.com'; this.fetch = opts.fetch ?? globalThis.fetch; this.apiKey = opts.apiKey; } @@ -59,7 +60,7 @@ export class LibreTranslateTranslator implements DittoTranslator { const headers = new Headers(); headers.append('Content-Type', 'application/json'); - const request = new Request(this.endpoint + '/translate', { + const request = new Request(new URL('/translate', this.baseUrl), { method: 'POST', body: JSON.stringify(body), headers, @@ -87,9 +88,4 @@ export class LibreTranslateTranslator implements DittoTranslator { }).optional(), }); } - - /** LibreTranslate provider. */ - getProvider(): string { - return LibreTranslateTranslator.provider; - } } diff --git a/src/translators/translator.ts b/src/translators/translator.ts index 98adf5ee..df3c7929 100644 --- a/src/translators/translator.ts +++ b/src/translators/translator.ts @@ -41,7 +41,8 @@ export interface DittoTranslator { /** Custom options. */ opts?: { signal?: AbortSignal }, ): Promise<{ results: string[]; source_lang: SourceLanguage }>; - getProvider(): string; + /** Provider name, eg `DeepL.com` */ + provider: string; } /** Includes the TARGET language and the status id. From 874da1baad5bac890431ae9de1462a3673038b1c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 14:01:18 -0500 Subject: [PATCH 51/64] Delete unused cacheMiddleware and ExpiringCache module --- src/middleware/cacheMiddleware.ts | 28 ------------- src/utils/expiring-cache.test.ts | 18 -------- src/utils/expiring-cache.ts | 68 ------------------------------- 3 files changed, 114 deletions(-) delete mode 100644 src/middleware/cacheMiddleware.ts delete mode 100644 src/utils/expiring-cache.test.ts delete mode 100644 src/utils/expiring-cache.ts diff --git a/src/middleware/cacheMiddleware.ts b/src/middleware/cacheMiddleware.ts deleted file mode 100644 index baa4976d..00000000 --- a/src/middleware/cacheMiddleware.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Debug from '@soapbox/stickynotes/debug'; -import { type MiddlewareHandler } from 'hono'; - -import ExpiringCache from '@/utils/expiring-cache.ts'; - -const debug = Debug('ditto:middleware:cache'); - -export const cacheMiddleware = (options: { - cacheName: string; - expires?: number; -}): MiddlewareHandler => { - return async (c, next) => { - const key = c.req.url.replace('http://', 'https://'); - const cache = new ExpiringCache(await caches.open(options.cacheName)); - const response = await cache.match(key); - if (!response) { - debug('Building cache for page', c.req.url); - await next(); - const response = c.res.clone(); - if (response.status < 500) { - await cache.putExpiring(key, response, options.expires ?? 0); - } - } else { - debug('Serving page from cache', c.req.url); - return response; - } - }; -}; diff --git a/src/utils/expiring-cache.test.ts b/src/utils/expiring-cache.test.ts deleted file mode 100644 index 8c6d7b18..00000000 --- a/src/utils/expiring-cache.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { assert } from '@std/assert'; - -import ExpiringCache from './expiring-cache.ts'; - -Deno.test('ExpiringCache', async () => { - const cache = new ExpiringCache(await caches.open('test')); - - await cache.putExpiring('http://mostr.local/1', new Response('hello world'), 300); - await cache.putExpiring('http://mostr.local/2', new Response('hello world'), -1); - - // const resp1 = await cache.match('http://mostr.local/1'); - const resp2 = await cache.match('http://mostr.local/2'); - - // assert(resp1!.headers.get('Expires')); - assert(!resp2); - - // await resp1!.text(); -}); diff --git a/src/utils/expiring-cache.ts b/src/utils/expiring-cache.ts deleted file mode 100644 index ebb5d2ee..00000000 --- a/src/utils/expiring-cache.ts +++ /dev/null @@ -1,68 +0,0 @@ -class ExpiringCache implements Cache { - #cache: Cache; - - constructor(cache: Cache) { - this.#cache = cache; - } - - add(request: RequestInfo | URL): Promise { - return this.#cache.add(request); - } - - addAll(requests: RequestInfo[]): Promise { - return this.#cache.addAll(requests); - } - - keys(request?: RequestInfo | URL | undefined, options?: CacheQueryOptions | undefined): Promise { - return this.#cache.keys(request, options); - } - - matchAll( - request?: RequestInfo | URL | undefined, - options?: CacheQueryOptions | undefined, - ): Promise { - return this.#cache.matchAll(request, options); - } - - put(request: RequestInfo | URL, response: Response): Promise { - return this.#cache.put(request, response); - } - - putExpiring(request: RequestInfo | URL, response: Response, expiresIn: number): Promise { - const expires = Date.now() + expiresIn; - - const clone = new Response(response.body, { - status: response.status, - headers: { - expires: new Date(expires).toUTCString(), - ...Object.fromEntries(response.headers.entries()), - }, - }); - - return this.#cache.put(request, clone); - } - - async match(request: RequestInfo | URL, options?: CacheQueryOptions | undefined): Promise { - const response = await this.#cache.match(request, options); - const expires = response?.headers.get('Expires'); - - if (response && expires) { - if (new Date(expires).getTime() > Date.now()) { - return response; - } else { - await Promise.all([ - this.delete(request), - response.text(), // Prevent memory leaks - ]); - } - } else if (response) { - return response; - } - } - - delete(request: RequestInfo | URL, options?: CacheQueryOptions | undefined): Promise { - return this.#cache.delete(request, options); - } -} - -export default ExpiringCache; From d639d9a14d1cbae36b00c360b571217e9676f7d8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 14:06:04 -0500 Subject: [PATCH 52/64] Reorganize translation interfaces/files --- src/app.ts | 2 +- src/caches/translationCache.ts | 16 ++++++ src/controllers/api/translate.ts | 21 ++++---- src/entities/MastodonTranslation.ts | 17 ++++++ src/interfaces/DittoTranslator.ts | 18 +++++++ src/translators/DeepLTranslator.ts | 10 ++-- src/translators/LibreTranslateTranslator.ts | 6 +-- src/translators/translator.ts | 57 --------------------- 8 files changed, 71 insertions(+), 76 deletions(-) create mode 100644 src/caches/translationCache.ts create mode 100644 src/entities/MastodonTranslation.ts create mode 100644 src/interfaces/DittoTranslator.ts delete mode 100644 src/translators/translator.ts diff --git a/src/app.ts b/src/app.ts index 3f6f7413..ad80cbb1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -120,6 +120,7 @@ import { indexController } from '@/controllers/site.ts'; import { manifestController } from '@/controllers/manifest.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; +import { DittoTranslator } from '@/interfaces/DittoTranslator.ts'; import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts'; @@ -129,7 +130,6 @@ import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts'; -import { DittoTranslator } from '@/translators/translator.ts'; import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts'; interface AppEnv extends HonoEnv { diff --git a/src/caches/translationCache.ts b/src/caches/translationCache.ts new file mode 100644 index 00000000..88121c1f --- /dev/null +++ b/src/caches/translationCache.ts @@ -0,0 +1,16 @@ +import { LanguageCode } from 'iso-639-1'; +import { LRUCache } from 'lru-cache'; + +import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; +import { Time } from '@/utils/time.ts'; + +/** Entity returned by DittoTranslator and LRUCache */ +interface DittoTranslation { + data: MastodonTranslation; +} + +/** Translations LRU cache. */ +export const translationCache = new LRUCache<`${LanguageCode}-${string}`, DittoTranslation>({ + max: 1000, + ttl: Time.hours(6), +}); diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index e1577478..56bd1b8d 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -2,10 +2,11 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { localeSchema } from '@/schema.ts'; -import { dittoTranslations, dittoTranslationsKey, MastodonTranslation } from '@/translators/translator.ts'; -import { parseBody } from '@/utils/api.ts'; +import { translationCache } from '@/caches/translationCache.ts'; +import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; import { getEvent } from '@/queries.ts'; +import { localeSchema } from '@/schema.ts'; +import { parseBody } from '@/utils/api.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; const translateSchema = z.object({ @@ -45,11 +46,11 @@ const translateController: AppController = async (c) => { return c.json({ error: 'Bad request.', schema: result.error }, 400); } - const translatedId = `${lang}-${id}` as dittoTranslationsKey; - const translationCache = dittoTranslations.get(translatedId); + const cacheKey: `${LanguageCode}-${string}` = `${lang}-${id}`; + const cached = translationCache.get(cacheKey); - if (translationCache) { - return c.json(translationCache.data, 200); + if (cached) { + return c.json(cached.data, 200); } const mediaAttachments = status?.media_attachments.map((value) => { @@ -68,7 +69,7 @@ const translateController: AppController = async (c) => { media_attachments: [], poll: null, detected_source_language: event.language ?? 'en', - provider: translator.getProvider(), + provider: translator.provider, }; if ((status?.poll as MastodonTranslation['poll'])?.options) { @@ -130,10 +131,10 @@ const translateController: AppController = async (c) => { mastodonTranslation.detected_source_language = data.source_lang; - dittoTranslations.set(translatedId, { data: mastodonTranslation }); + translationCache.set(cacheKey, { data: mastodonTranslation }); return c.json(mastodonTranslation, 200); } catch (e) { - if (e instanceof Error && e.message?.includes('not supported')) { + if (e instanceof Error && e.message.includes('not supported')) { return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); } return c.json({ error: 'Service Unavailable' }, 503); diff --git a/src/entities/MastodonTranslation.ts b/src/entities/MastodonTranslation.ts new file mode 100644 index 00000000..d59b9aad --- /dev/null +++ b/src/entities/MastodonTranslation.ts @@ -0,0 +1,17 @@ +import { LanguageCode } from 'iso-639-1'; + +/** https://docs.joinmastodon.org/entities/Translation/ */ +export interface MastodonTranslation { + /** HTML-encoded translated content of the status. */ + content: string; + /** The translated spoiler warning of the status. */ + spoiler_text: string; + /** The translated media descriptions of the status. */ + media_attachments: { id: string; description: string }[]; + /** The translated poll of the status. */ + poll: { id: string; options: { title: string }[] } | null; + //** The language of the source text, as auto-detected by the machine translation provider. */ + detected_source_language: LanguageCode; + /** The service that provided the machine translation. */ + provider: string; +} diff --git a/src/interfaces/DittoTranslator.ts b/src/interfaces/DittoTranslator.ts new file mode 100644 index 00000000..426e1428 --- /dev/null +++ b/src/interfaces/DittoTranslator.ts @@ -0,0 +1,18 @@ +import type { LanguageCode } from 'iso-639-1'; + +/** DittoTranslator class, used for status translation. */ +export interface DittoTranslator { + /** Translate the 'content' into 'targetLanguage'. */ + translate( + /** Texts to translate. */ + texts: string[], + /** The language of the source texts. */ + sourceLanguage: LanguageCode | undefined, + /** The texts will be translated into this language. */ + targetLanguage: LanguageCode, + /** Custom options. */ + opts?: { signal?: AbortSignal }, + ): Promise<{ results: string[]; source_lang: LanguageCode }>; + /** Provider name, eg `DeepL.com` */ + provider: string; +} diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index 2b4da175..b856d1ec 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -1,7 +1,7 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { DittoTranslator } from '@/interfaces/DittoTranslator.ts'; import { languageSchema } from '@/schema.ts'; interface DeepLTranslatorOpts { @@ -28,8 +28,8 @@ export class DeepLTranslator implements DittoTranslator { async translate( texts: string[], - source: SourceLanguage | undefined, - dest: TargetLanguage, + source: LanguageCode | undefined, + dest: LanguageCode, opts?: { signal?: AbortSignal }, ) { const { translations } = await this.translateMany(texts, source, dest, opts); @@ -43,8 +43,8 @@ export class DeepLTranslator implements DittoTranslator { /** DeepL translate request. */ private async translateMany( texts: string[], - source: SourceLanguage | undefined, - targetLanguage: TargetLanguage, + source: LanguageCode | undefined, + targetLanguage: LanguageCode, opts?: { signal?: AbortSignal }, ) { const body: any = { diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index bd2850ff..b7a749cf 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -1,7 +1,7 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { DittoTranslator } from '@/interfaces/DittoTranslator.ts'; import { languageSchema } from '@/schema.ts'; interface LibreTranslateTranslatorOpts { @@ -28,8 +28,8 @@ export class LibreTranslateTranslator implements DittoTranslator { async translate( texts: string[], - source: SourceLanguage | undefined, - dest: TargetLanguage, + source: LanguageCode | undefined, + dest: LanguageCode, opts?: { signal?: AbortSignal }, ) { const translations = await Promise.all( diff --git a/src/translators/translator.ts b/src/translators/translator.ts deleted file mode 100644 index df3c7929..00000000 --- a/src/translators/translator.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { LanguageCode } from 'iso-639-1'; -import { LRUCache } from 'lru-cache'; - -import { Time } from '@/utils/time.ts'; - -/** Original language of the post */ -export type SourceLanguage = LanguageCode; - -/** Content will be translated to this language */ -export type TargetLanguage = LanguageCode; - -/** Entity returned by DittoTranslator and LRUCache */ -type DittoTranslation = { - data: MastodonTranslation; -}; - -export type MastodonTranslation = { - /** HTML-encoded translated content of the status. */ - content: string; - /** The translated spoiler warning of the status. */ - spoiler_text: string; - /** The translated media descriptions of the status. */ - media_attachments: { id: string; description: string }[]; - /** The translated poll of the status. */ - poll: { id: string; options: { title: string }[] } | null; - //** The language of the source text, as auto-detected by the machine translation provider. */ - detected_source_language: SourceLanguage; - /** The service that provided the machine translation. */ - provider: string; -}; - -/** DittoTranslator class, used for status translation. */ -export interface DittoTranslator { - /** Translate the 'content' into 'targetLanguage'. */ - translate( - texts: string[], - /** The language of the source text/status. */ - sourceLanguage: SourceLanguage | undefined, - /** The status content will be translated into this language. */ - targetLanguage: TargetLanguage, - /** Custom options. */ - opts?: { signal?: AbortSignal }, - ): Promise<{ results: string[]; source_lang: SourceLanguage }>; - /** Provider name, eg `DeepL.com` */ - provider: string; -} - -/** Includes the TARGET language and the status id. - * Example: en-390f5b01b49a8ee6e13fe917420c023d889b3da8e983a14c9e84587e43d12c15 - * The example above means: - * I want the status 390f5b01b49a8ee6e13fe917420c023d889b3da8e983a14c9e84587e43d12c15 translated to english (if it exists in the LRUCache). */ -export type dittoTranslationsKey = `${TargetLanguage}-${string}`; - -export const dittoTranslations = new LRUCache({ - max: 1000, - ttl: Time.hours(6), -}); From 12d643e150edef61916d35a85cab92e1ffe4af3e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 14:12:20 -0500 Subject: [PATCH 53/64] Add translation cache metrics, let the cache be configurable --- src/caches/translationCache.ts | 13 ++++--------- src/config.ts | 7 +++++++ src/controllers/api/translate.ts | 7 +++++-- src/metrics.ts | 5 +++++ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/caches/translationCache.ts b/src/caches/translationCache.ts index 88121c1f..7bd27946 100644 --- a/src/caches/translationCache.ts +++ b/src/caches/translationCache.ts @@ -1,16 +1,11 @@ import { LanguageCode } from 'iso-639-1'; import { LRUCache } from 'lru-cache'; +import { Conf } from '@/config.ts'; import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; -import { Time } from '@/utils/time.ts'; - -/** Entity returned by DittoTranslator and LRUCache */ -interface DittoTranslation { - data: MastodonTranslation; -} /** Translations LRU cache. */ -export const translationCache = new LRUCache<`${LanguageCode}-${string}`, DittoTranslation>({ - max: 1000, - ttl: Time.hours(6), +export const translationCache = new LRUCache<`${LanguageCode}-${string}`, MastodonTranslation>({ + max: Conf.caches.translation.max, + ttl: Conf.caches.translation.ttl, }); diff --git a/src/config.ts b/src/config.ts index 806fe196..c0daf894 100644 --- a/src/config.ts +++ b/src/config.ts @@ -314,6 +314,13 @@ class Conf { ttl: Number(Deno.env.get('DITTO_CACHE_LINK_PREVIEW_TTL') || 12 * 60 * 60 * 1000), }; }, + /** Translation cache settings. */ + get translation(): { max: number; ttl: number } { + return { + max: Number(Deno.env.get('DITTO_CACHE_TRANSLATION_MAX') || 1000), + ttl: Number(Deno.env.get('DITTO_CACHE_TRANSLATION_TTL') || 6 * 60 * 60 * 1000), + }; + }, }; } diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index 56bd1b8d..d763c713 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { translationCache } from '@/caches/translationCache.ts'; import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; +import { cachedTranslationsSizeGauge } from '@/metrics.ts'; import { getEvent } from '@/queries.ts'; import { localeSchema } from '@/schema.ts'; import { parseBody } from '@/utils/api.ts'; @@ -50,7 +51,7 @@ const translateController: AppController = async (c) => { const cached = translationCache.get(cacheKey); if (cached) { - return c.json(cached.data, 200); + return c.json(cached, 200); } const mediaAttachments = status?.media_attachments.map((value) => { @@ -131,7 +132,9 @@ const translateController: AppController = async (c) => { mastodonTranslation.detected_source_language = data.source_lang; - translationCache.set(cacheKey, { data: mastodonTranslation }); + translationCache.set(cacheKey, mastodonTranslation); + cachedTranslationsSizeGauge.set(translationCache.size); + return c.json(mastodonTranslation, 200); } catch (e) { if (e instanceof Error && e.message.includes('not supported')) { diff --git a/src/metrics.ts b/src/metrics.ts index c005a6c7..2cb3eb2d 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -121,6 +121,11 @@ export const cachedLinkPreviewSizeGauge = new Gauge({ help: 'Number of link previews in cache', }); +export const cachedTranslationsSizeGauge = new Gauge({ + name: 'ditto_cached_translations_size', + help: 'Number of translated statuses in cache', +}); + export const internalSubscriptionsSizeGauge = new Gauge({ name: 'ditto_internal_subscriptions_size', help: "Number of active subscriptions to Ditto's internal relay", From 655e94ef91941d93e5362cb0998726ca44ddfb5c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 14:14:29 -0500 Subject: [PATCH 54/64] DittoTranslator: move `provider` to top of interface --- src/interfaces/DittoTranslator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interfaces/DittoTranslator.ts b/src/interfaces/DittoTranslator.ts index 426e1428..7e5e1d50 100644 --- a/src/interfaces/DittoTranslator.ts +++ b/src/interfaces/DittoTranslator.ts @@ -2,6 +2,8 @@ import type { LanguageCode } from 'iso-639-1'; /** DittoTranslator class, used for status translation. */ export interface DittoTranslator { + /** Provider name, eg `DeepL.com` */ + provider: string; /** Translate the 'content' into 'targetLanguage'. */ translate( /** Texts to translate. */ @@ -13,6 +15,4 @@ export interface DittoTranslator { /** Custom options. */ opts?: { signal?: AbortSignal }, ): Promise<{ results: string[]; source_lang: LanguageCode }>; - /** Provider name, eg `DeepL.com` */ - provider: string; } From df3b8863df6ff7e54467ca7850d48c98680a9e19 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 14:16:29 -0500 Subject: [PATCH 55/64] LibreTranslateTranslator: move headers to plain object, add url variable --- src/translators/LibreTranslateTranslator.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index b7a749cf..ef7fb1f8 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -57,13 +57,14 @@ export class LibreTranslateTranslator implements DittoTranslator { api_key: this.apiKey, }; - const headers = new Headers(); - headers.append('Content-Type', 'application/json'); + const url = new URL('/translate', this.baseUrl); - const request = new Request(new URL('/translate', this.baseUrl), { + const request = new Request(url, { method: 'POST', body: JSON.stringify(body), - headers, + headers: { + 'Content-Type': 'application/json', + }, signal: opts?.signal, }); From 1cb13b141a8052a92329544bc25e57e54f9ba691 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 10 Oct 2024 22:46:37 -0300 Subject: [PATCH 56/64] feat: improve setLanguage() function, remove links and emojis from event.content before using lande --- src/pipeline.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index a00456a9..00bd7f7d 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -3,6 +3,7 @@ import { Stickynotes } from '@soapbox/stickynotes'; import ISO6391 from 'iso-639-1'; import { Kysely, sql } from 'kysely'; import lande from 'lande'; +import linkify from 'linkifyjs'; import { LRUCache } from 'lru-cache'; import { z } from 'zod'; @@ -200,7 +201,16 @@ async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise { - const [topResult] = lande(event.content); + const contentWithoutEmoji = event.content.replace( + /[\p{Emoji}\p{Emoji_Modifier}\p{Emoji_Component}\p{Emoji_Modifier_Base}\p{Emoji_Presentation}]/gu, + '', + ); + const contentWithoutLinks = linkify.tokenize(contentWithoutEmoji).reduce((accumulator, current) => { + if (current.t === 'text') return accumulator + current.v; + return accumulator; + }, ''); + const parsedContent = contentWithoutLinks; + const [topResult] = lande(parsedContent); if (topResult) { const [iso6393, confidence] = topResult; From 6fb3bfafc08e6f64be3a805638ddd956388de784 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Oct 2024 02:52:41 -0500 Subject: [PATCH 57/64] Mark registrations enabled in v2 instance --- src/controllers/api/instance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index f6ae758c..56719715 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -142,7 +142,7 @@ const instanceV2Controller: AppController = async (c) => { }, }, registrations: { - enabled: false, + enabled: true, approval_required: false, message: null, url: null, From 380ac42033afe5e08815510ceb89919f89d0ccbc Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Fri, 11 Oct 2024 21:50:40 +0530 Subject: [PATCH 58/64] upgrade deno version in dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 636a94fa..90475c15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM denoland/deno:1.44.2 +FROM denoland/deno:2.0.0 ENV PORT 5000 WORKDIR /app From 36d09af4675c6fdda57ecad49f8a7eacb6a3ff6f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 11 Oct 2024 14:28:01 -0300 Subject: [PATCH 59/64] feat: make lande great again create detectLanguage() function that removes emojis, links and other weird invisible characters --- src/pipeline.ts | 39 ++++++++++++-------------------------- src/utils/language.test.ts | 28 +++++++++++++++++++++++++++ src/utils/language.ts | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 src/utils/language.test.ts create mode 100644 src/utils/language.ts diff --git a/src/pipeline.ts b/src/pipeline.ts index 00bd7f7d..7c81a484 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,9 +1,6 @@ import { NKinds, NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { Stickynotes } from '@soapbox/stickynotes'; -import ISO6391 from 'iso-639-1'; import { Kysely, sql } from 'kysely'; -import lande from 'lande'; -import linkify from 'linkifyjs'; import { LRUCache } from 'lru-cache'; import { z } from 'zod'; @@ -23,6 +20,7 @@ import { nip05Cache } from '@/utils/nip05.ts'; import { purifyEvent } from '@/utils/purify.ts'; import { updateStats } from '@/utils/stats.ts'; import { getTagSet } from '@/utils/tags.ts'; +import { detectLanguage } from '@/utils/language.ts'; const console = new Stickynotes('ditto:pipeline'); @@ -201,32 +199,19 @@ async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise { - const contentWithoutEmoji = event.content.replace( - /[\p{Emoji}\p{Emoji_Modifier}\p{Emoji_Component}\p{Emoji_Modifier_Base}\p{Emoji_Presentation}]/gu, - '', - ); - const contentWithoutLinks = linkify.tokenize(contentWithoutEmoji).reduce((accumulator, current) => { - if (current.t === 'text') return accumulator + current.v; - return accumulator; - }, ''); - const parsedContent = contentWithoutLinks; - const [topResult] = lande(parsedContent); + if (event.kind !== 1) return; - if (topResult) { - const [iso6393, confidence] = topResult; - const locale = new Intl.Locale(iso6393); + const language = detectLanguage(event.content, 0.90); + if (!language) return; - if (confidence >= 0.95 && ISO6391.validate(locale.language)) { - const kysely = await Storages.kysely(); - try { - await kysely.updateTable('nostr_events') - .set('language', locale.language) - .where('id', '=', event.id) - .execute(); - } catch { - // do nothing - } - } + const kysely = await Storages.kysely(); + try { + await kysely.updateTable('nostr_events') + .set('language', language) + .where('id', '=', event.id) + .execute(); + } catch { + // do nothing } } diff --git a/src/utils/language.test.ts b/src/utils/language.test.ts new file mode 100644 index 00000000..255f6b58 --- /dev/null +++ b/src/utils/language.test.ts @@ -0,0 +1,28 @@ +import { detectLanguage } from '@/utils/language.ts'; +import { assertEquals } from '@std/assert'; + +Deno.test('Detect English language', () => { + assertEquals(detectLanguage(``, 0.90), undefined); + assertEquals(detectLanguage(`Good morning my fellow friends`, 0.90), 'en'); + assertEquals( + detectLanguage( + `Would you listen to Michael Jackson's songs?\n\nnostr:nevent1qvzqqqqqqypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqyvhwumn8ghj7cm0vfexzen4d4sjucm0d5hhyetvv9usqg8htx8xcjq7ffrzxu7nrhlr8vljcv6gpmet0auy87mpj6djxk4myqha02kp`, + 0.90, + ), + 'en', + ); + assertEquals( + detectLanguage( + `https://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_uhttps://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_uhttps://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_uhttps://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_uWould you listen to Michael Jackson's songs?\n\nnostr:nevent1qvzqqqqqqypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqyvhwumn8ghj7cm0vfexzen4d4sjucm0d5hhyetvv9usqg8htx8xcjq7ffrzxu7nrhlr8vljcv6gpmet0auy87mpj6djxk4myqha02kp`, + 0.90, + ), + 'en', + ); + assertEquals( + detectLanguage( + `https://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_u ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ๐Ÿ˜‚๐Ÿ’ฏโ™กโŒจ๏ธŽ https://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_uhttps://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_uhttps://youtu.be/FxppefYTA2I?si=grgEpbEhFu_-3V_u Would you listen to Michael Jackson's songs?\n\nnostr:nevent1qvzqqqqqqypzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqyvhwumn8ghj7cm0vfexzen4d4sjucm0d5hhyetvv9usqg8htx8xcjq7ffrzxu7nrhlr8vljcv6gpmet0auy87mpj6djxk4myqha02kp`, + 0.90, + ), + 'en', + ); +}); diff --git a/src/utils/language.ts b/src/utils/language.ts new file mode 100644 index 00000000..972a6721 --- /dev/null +++ b/src/utils/language.ts @@ -0,0 +1,34 @@ +import ISO6391, { type LanguageCode } from 'iso-639-1'; +import lande from 'lande'; +import linkify from 'linkifyjs'; + +linkify.registerCustomProtocol('nostr', true); + +/** Returns the detected language if the confidence is greater or equal than 'minConfidence' + * 'minConfidence' must be a number between 0 and 1, such as 0.95 + */ +export function detectLanguage(text: string, minConfidence: number): LanguageCode | undefined { + // It's better to remove the emojis first + const sanitizedText = (linkify.tokenize( + text.replaceAll(/\p{Extended_Pictographic}/gu, '') + .replaceAll(/[\s\uFEFF\u00A0\u200B-\u200D\u{0FE0E}]+/gu, ' '), + ) + .reduce( + (acc, { t, v }) => t === 'text' ? acc + v : acc, + '', + )).trim(); + if (sanitizedText.length < 10) return; // heuristics + + const [topResult] = lande( + sanitizedText, + ); + if (topResult) { + const [iso6393, confidence] = topResult; + const locale = new Intl.Locale(iso6393); + + if (confidence >= minConfidence && ISO6391.validate(locale.language)) { + return locale.language as LanguageCode; + } + } + return; +} From c5ddd2ebb7bda8fd5c53696048b6418468c867ce Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Oct 2024 16:25:29 -0500 Subject: [PATCH 60/64] Make verify_credentials and update_credentials return a consistent CredentialAccount object --- src/controllers/api/accounts.ts | 41 ++++++++++++++++++--------------- src/entities/MastodonAccount.ts | 2 +- src/views/mastodon/accounts.ts | 19 ++++++++------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index c4a2bc40..50c7d140 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -51,7 +51,7 @@ const verifyCredentialsController: AppController = async (c) => { const store = await Storages.db(); - const [author, [settingsStore], [captcha]] = await Promise.all([ + const [author, [settingsEvent], [captcha]] = await Promise.all([ getAuthor(pubkey, { signal: AbortSignal.timeout(5000) }), store.query([{ @@ -71,21 +71,16 @@ const verifyCredentialsController: AppController = async (c) => { }]), ]); + let settingsStore: Record | undefined; + try { + settingsStore = n.json().pipe(z.record(z.string(), z.unknown())).parse(settingsEvent?.content); + } catch { + // Do nothing + } + const account = author - ? await renderAccount(author, { withSource: true }) - : await accountFromPubkey(pubkey, { withSource: true }); - - if (settingsStore) { - try { - account.pleroma.settings_store = JSON.parse(settingsStore.content); - } catch { - // Ignore - } - } - - if (captcha && account.source) { - account.source.ditto.captcha_solved = true; - } + ? await renderAccount(author, { withSource: true, settingsStore, captcha }) + : await accountFromPubkey(pubkey, { withSource: true, settingsStore, captcha }); return c.json(account); }; @@ -280,7 +275,7 @@ const updateCredentialsSchema = z.object({ bot: z.boolean().optional(), discoverable: z.boolean().optional(), nip05: z.string().email().or(z.literal('')).optional(), - pleroma_settings_store: z.unknown().optional(), + pleroma_settings_store: z.record(z.string(), z.unknown()).optional(), lud16: z.string().email().or(z.literal('')).optional(), website: z.string().url().or(z.literal('')).optional(), }); @@ -290,6 +285,7 @@ const updateCredentialsController: AppController = async (c) => { const pubkey = await signer.getPublicKey(); const body = await parseBody(c.req.raw); const result = updateCredentialsSchema.safeParse(body); + const store = await Storages.db(); if (!result.success) { return c.json(result.error, 422); @@ -339,8 +335,17 @@ const updateCredentialsController: AppController = async (c) => { c, ); - const account = await renderAccount(event, { withSource: true }); + const [captcha] = await store.query([{ + kinds: [1985], + authors: [Conf.pubkey], + '#L': ['pub.ditto.captcha'], + '#l': ['solved'], + '#p': [pubkey], + limit: 1, + }]); + const settingsStore = result.data.pleroma_settings_store; + const account = await renderAccount(event, { withSource: true, settingsStore, captcha }); if (settingsStore) { await createEvent({ @@ -350,8 +355,6 @@ const updateCredentialsController: AppController = async (c) => { }, c); } - account.pleroma.settings_store = settingsStore; - return c.json(account); }; diff --git a/src/entities/MastodonAccount.ts b/src/entities/MastodonAccount.ts index b713d70c..99409c6a 100644 --- a/src/entities/MastodonAccount.ts +++ b/src/entities/MastodonAccount.ts @@ -54,7 +54,7 @@ export interface MastodonAccount { is_moderator: boolean; is_suggested: boolean; is_local: boolean; - settings_store: unknown; + settings_store?: Record; tags: string[]; }; nostr: { diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 1e10c4ff..83bd705d 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -1,4 +1,4 @@ -import { NSchema as n } from '@nostrify/nostrify'; +import { type NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { escape } from 'entities'; import { nip19, UnsignedEvent } from 'nostr-tools'; @@ -12,16 +12,19 @@ import { faviconCache } from '@/utils/favicon.ts'; import { nostrDate, nostrNow } from '@/utils.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; -interface ToAccountOpts { - withSource?: boolean; -} +type ToAccountOpts = { + withSource: true; + settingsStore: Record | undefined; + captcha: NostrEvent | undefined; +} | { + withSource?: false; +}; async function renderAccount( event: Omit, opts: ToAccountOpts = {}, signal = AbortSignal.timeout(3000), ): Promise { - const { withSource = false } = opts; const { pubkey } = event; const names = getTagSet(event.user?.tags ?? [], 'n'); @@ -76,7 +79,7 @@ async function renderAccount( locked: false, note: about ? escape(about) : '', roles: [], - source: withSource + source: opts.withSource ? { fields: [], language: '', @@ -88,7 +91,7 @@ async function renderAccount( nip05, }, ditto: { - captcha_solved: false, + captcha_solved: Boolean(opts.captcha), }, } : undefined, @@ -107,7 +110,7 @@ async function renderAccount( is_moderator: names.has('admin') || names.has('moderator'), is_suggested: names.has('suggested'), is_local: parsed05?.domain === Conf.url.host, - settings_store: undefined as unknown, + settings_store: opts.withSource ? opts.settingsStore : undefined, tags: [...getTagSet(event.user?.tags ?? [], 't')], favicon: favicon?.toString(), }, From 9c7e35a6b455dc3bdf3a97cc9c5ebaff8d642d1d Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 11 Oct 2024 18:47:38 -0300 Subject: [PATCH 61/64] refactor: code preferences, formatting --- src/utils/language.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/language.ts b/src/utils/language.ts index 972a6721..8af8ddf9 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -9,15 +9,15 @@ linkify.registerCustomProtocol('nostr', true); */ export function detectLanguage(text: string, minConfidence: number): LanguageCode | undefined { // It's better to remove the emojis first - const sanitizedText = (linkify.tokenize( - text.replaceAll(/\p{Extended_Pictographic}/gu, '') + const sanitizedText = linkify.tokenize( + text + .replaceAll(/\p{Extended_Pictographic}/gu, '') .replaceAll(/[\s\uFEFF\u00A0\u200B-\u200D\u{0FE0E}]+/gu, ' '), - ) - .reduce( - (acc, { t, v }) => t === 'text' ? acc + v : acc, - '', - )).trim(); - if (sanitizedText.length < 10) return; // heuristics + ).reduce((acc, { t, v }) => t === 'text' ? acc + v : acc, '').trim(); + + if (sanitizedText.length < 10) { // heuristics + return; + } const [topResult] = lande( sanitizedText, From a484634d3604bbfa3948abbaa2ff9519592571a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Oct 2024 16:50:52 -0500 Subject: [PATCH 62/64] Switch captcha_solved from a label event to an n-tag on a user event --- src/controllers/api/accounts.ts | 26 ++++---------------------- src/controllers/api/captcha.ts | 13 ++----------- src/views/mastodon/accounts.ts | 3 +-- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 50c7d140..e68e077a 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -51,7 +51,7 @@ const verifyCredentialsController: AppController = async (c) => { const store = await Storages.db(); - const [author, [settingsEvent], [captcha]] = await Promise.all([ + const [author, [settingsEvent]] = await Promise.all([ getAuthor(pubkey, { signal: AbortSignal.timeout(5000) }), store.query([{ @@ -60,15 +60,6 @@ const verifyCredentialsController: AppController = async (c) => { '#d': ['pub.ditto.pleroma_settings_store'], limit: 1, }]), - - store.query([{ - kinds: [1985], - authors: [Conf.pubkey], - '#L': ['pub.ditto.captcha'], - '#l': ['solved'], - '#p': [pubkey], - limit: 1, - }]), ]); let settingsStore: Record | undefined; @@ -79,8 +70,8 @@ const verifyCredentialsController: AppController = async (c) => { } const account = author - ? await renderAccount(author, { withSource: true, settingsStore, captcha }) - : await accountFromPubkey(pubkey, { withSource: true, settingsStore, captcha }); + ? await renderAccount(author, { withSource: true, settingsStore }) + : await accountFromPubkey(pubkey, { withSource: true, settingsStore }); return c.json(account); }; @@ -335,17 +326,8 @@ const updateCredentialsController: AppController = async (c) => { c, ); - const [captcha] = await store.query([{ - kinds: [1985], - authors: [Conf.pubkey], - '#L': ['pub.ditto.captcha'], - '#l': ['solved'], - '#p': [pubkey], - limit: 1, - }]); - const settingsStore = result.data.pleroma_settings_store; - const account = await renderAccount(event, { withSource: true, settingsStore, captcha }); + const account = await renderAccount(event, { withSource: true, settingsStore }); if (settingsStore) { await createEvent({ diff --git a/src/controllers/api/captcha.ts b/src/controllers/api/captcha.ts index ef266745..0ff99ba6 100644 --- a/src/controllers/api/captcha.ts +++ b/src/controllers/api/captcha.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { createAdminEvent } from '@/utils/api.ts'; +import { createAdminEvent, updateUser } from '@/utils/api.ts'; interface Point { x: number; @@ -169,16 +169,7 @@ export const captchaVerifyController: AppController = async (c) => { if (solved) { captchas.delete(id); - - await createAdminEvent({ - kind: 1985, - tags: [ - ['L', 'pub.ditto.captcha'], - ['l', 'solved', 'pub.ditto.captcha'], - ['p', pubkey, Conf.relay], - ], - }, c); - + await updateUser(pubkey, { captcha_solved: true }, c); return new Response(null, { status: 204 }); } diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 83bd705d..a293a04b 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -15,7 +15,6 @@ import { renderEmojis } from '@/views/mastodon/emojis.ts'; type ToAccountOpts = { withSource: true; settingsStore: Record | undefined; - captcha: NostrEvent | undefined; } | { withSource?: false; }; @@ -91,7 +90,7 @@ async function renderAccount( nip05, }, ditto: { - captcha_solved: Boolean(opts.captcha), + captcha_solved: names.has('captcha_solved'), }, } : undefined, From d4f3c673a2f7b417c9e2cc4473e62add1c3d9cf8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Oct 2024 17:35:45 -0500 Subject: [PATCH 63/64] deno lint --- src/controllers/api/accounts.ts | 1 - src/controllers/api/captcha.ts | 2 +- src/utils/api.ts | 2 +- src/views/mastodon/accounts.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index e68e077a..b3c75ae5 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -276,7 +276,6 @@ const updateCredentialsController: AppController = async (c) => { const pubkey = await signer.getPublicKey(); const body = await parseBody(c.req.raw); const result = updateCredentialsSchema.safeParse(body); - const store = await Storages.db(); if (!result.success) { return c.json(result.error, 422); diff --git a/src/controllers/api/captcha.ts b/src/controllers/api/captcha.ts index 0ff99ba6..1bb92118 100644 --- a/src/controllers/api/captcha.ts +++ b/src/controllers/api/captcha.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { createAdminEvent, updateUser } from '@/utils/api.ts'; +import { updateUser } from '@/utils/api.ts'; interface Point { x: number; diff --git a/src/utils/api.ts b/src/utils/api.ts index c6d3c6b6..e7766979 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,4 +1,4 @@ -import { Context } from '@hono/hono'; +import { type Context } from '@hono/hono'; import { HTTPException } from '@hono/hono/http-exception'; import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index a293a04b..626203b8 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -1,4 +1,4 @@ -import { type NostrEvent, NSchema as n } from '@nostrify/nostrify'; +import { NSchema as n } from '@nostrify/nostrify'; import { escape } from 'entities'; import { nip19, UnsignedEvent } from 'nostr-tools'; From b2397bccdd8c3c669eb037d3aae090e314dcb6ad Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Oct 2024 17:44:02 -0500 Subject: [PATCH 64/64] Upgrade @gleasonator/policy in deno.lock --- deno.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deno.lock b/deno.lock index c97291c1..ee367c9a 100644 --- a/deno.lock +++ b/deno.lock @@ -22,6 +22,7 @@ "jsr:@gleasonator/policy@0.7.1": "0.7.1", "jsr:@gleasonator/policy@0.8.0": "0.8.0", "jsr:@gleasonator/policy@0.8.1": "0.8.1", + "jsr:@gleasonator/policy@0.9.0": "0.9.0", "jsr:@hono/hono@^4.4.6": "4.6.2", "jsr:@lambdalisue/async@^2.1.1": "2.1.1", "jsr:@nostrify/db@~0.36.1": "0.36.1", @@ -253,6 +254,13 @@ "jsr:@nostrify/policies@~0.36.1" ] }, + "@gleasonator/policy@0.9.0": { + "integrity": "483f87c3a18fb39f795495d3388453193f1115ab7e130981121f7828ce6b51bb", + "dependencies": [ + "jsr:@nostrify/nostrify@0.36", + "jsr:@nostrify/policies@~0.36.1" + ] + }, "@hono/hono@4.4.6": { "integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453" },