From 1ea5591393585912db846a8d7a3fedd754d46e23 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 28 Apr 2024 12:12:58 -0500 Subject: [PATCH 001/252] Improve mentions performance --- src/views/mastodon/statuses.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 9716d6ff..b3675554 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -1,12 +1,12 @@ +import { NostrEvent } from '@nostrify/nostrify'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; import { Conf } from '@/config.ts'; import { nip19 } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; -import { getAuthor } from '@/queries.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; -import { eventsDB } from '@/storages.ts'; +import { eventsDB, optimizer } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; import { nostrDate } from '@/utils.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; @@ -40,11 +40,17 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { ), ]; + const mentionedProfiles = await optimizer.query( + [{ authors: mentionedPubkeys, limit: mentionedPubkeys.length }], + ); + const { html, links, firstUrl } = parseNoteContent(event.content); const [mentions, card, relatedEvents] = await Promise .all([ - Promise.all(mentionedPubkeys.map(toMention)), + Promise.all( + mentionedPubkeys.map((pubkey) => toMention(pubkey, mentionedProfiles.find((event) => event.pubkey === pubkey))), + ), firstUrl ? unfurlCardCached(firstUrl) : null, viewerPubkey ? await eventsDB.query([ @@ -131,9 +137,8 @@ async function renderReblog(event: DittoEvent, opts: statusOpts) { }; } -async function toMention(pubkey: string) { - const author = await getAuthor(pubkey); - const account = author ? await renderAccount(author) : undefined; +async function toMention(pubkey: string, event?: NostrEvent) { + const account = event ? await renderAccount(event) : undefined; if (account) { return { From 0925f37929809cae9f8f064eaa63fb7785bdc43c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Apr 2024 15:04:13 -0500 Subject: [PATCH 002/252] Make storeMiddleware available in every request --- src/app.ts | 8 ++++---- src/controllers/api/timelines.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index abdec32a..7e74f6d9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -91,7 +91,7 @@ interface AppEnv extends HonoEnv { /** User associated with the pubkey, if any. */ user?: User; /** Store */ - store?: NStore; + store: NStore; }; } @@ -119,7 +119,7 @@ app.get('/api/v1/streaming', streamingController); app.get('/api/v1/streaming/', streamingController); app.get('/relay', relayController); -app.use('*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), auth19, auth98()); +app.use('*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), auth19, auth98(), storeMiddleware); app.get('/.well-known/webfinger', webfingerController); app.get('/.well-known/host-meta', hostMetaController); @@ -173,8 +173,8 @@ app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requirePubkey, deleteStatusCont app.post('/api/v1/media', mediaController); app.post('/api/v2/media', mediaController); -app.get('/api/v1/timelines/home', requirePubkey, storeMiddleware, homeTimelineController); -app.get('/api/v1/timelines/public', storeMiddleware, publicTimelineController); +app.get('/api/v1/timelines/home', requirePubkey, homeTimelineController); +app.get('/api/v1/timelines/public', publicTimelineController); app.get('/api/v1/timelines/tag/:hashtag', hashtagTimelineController); app.get('/api/v1/preferences', preferencesController); diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 191fce74..40b54c93 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -1,4 +1,4 @@ -import { NostrFilter, NStore } from '@nostrify/nostrify'; +import { NostrFilter } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppContext, type AppController } from '@/app.ts'; @@ -45,7 +45,7 @@ const hashtagTimelineController: AppController = (c) => { /** Render statuses for timelines. */ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { const { signal } = c.req.raw; - const store = c.get('store') as NStore; + const store = c.get('store'); const events = await store .query(filters, { signal }) From 25db277a9f18878408277d26bd5dda0ca73aac87 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Apr 2024 15:10:08 -0500 Subject: [PATCH 003/252] storeMiddleware: remove `as string` --- src/middleware/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/store.ts b/src/middleware/store.ts index 8bb595cb..a22bb790 100644 --- a/src/middleware/store.ts +++ b/src/middleware/store.ts @@ -4,7 +4,7 @@ import { eventsDB } from '@/storages.ts'; /** Store middleware. */ const storeMiddleware: AppMiddleware = async (c, next) => { - const pubkey = c.get('pubkey') as string; + const pubkey = c.get('pubkey'); if (pubkey) { const store = new UserStore(pubkey, eventsDB); From c786e1bc55e4460c8944ba11abfe21bacbcf203f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Apr 2024 15:32:18 -0500 Subject: [PATCH 004/252] Uploader: make second argument an options object --- src/uploaders/config.ts | 8 ++++---- src/uploaders/ipfs.ts | 8 ++++---- src/uploaders/s3.ts | 6 ++---- src/uploaders/types.ts | 4 ++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/uploaders/config.ts b/src/uploaders/config.ts index 2ee2f9ab..83874f6e 100644 --- a/src/uploaders/config.ts +++ b/src/uploaders/config.ts @@ -7,11 +7,11 @@ import type { Uploader } from './types.ts'; /** Meta-uploader determined from configuration. */ const configUploader: Uploader = { - upload(file, signal) { - return uploader().upload(file, signal); + upload(file, opts) { + return uploader().upload(file, opts); }, - delete(cid, signal) { - return uploader().delete(cid, signal); + delete(cid, opts) { + return uploader().delete(cid, opts); }, }; diff --git a/src/uploaders/ipfs.ts b/src/uploaders/ipfs.ts index 5d82e2d5..6e0e0e7e 100644 --- a/src/uploaders/ipfs.ts +++ b/src/uploaders/ipfs.ts @@ -18,7 +18,7 @@ const ipfsAddResponseSchema = z.object({ * and upload the file using the REST API. */ const ipfsUploader: Uploader = { - async upload(file, signal) { + async upload(file, opts) { const url = new URL('/api/v0/add', Conf.ipfs.apiUrl); const formData = new FormData(); @@ -27,7 +27,7 @@ const ipfsUploader: Uploader = { const response = await fetchWorker(url, { method: 'POST', body: formData, - signal, + signal: opts?.signal, }); const { Hash } = ipfsAddResponseSchema.parse(await response.json()); @@ -36,7 +36,7 @@ const ipfsUploader: Uploader = { cid: Hash, }; }, - async delete(cid, signal) { + async delete(cid, opts) { const url = new URL('/api/v0/pin/rm', Conf.ipfs.apiUrl); const query = new URLSearchParams(); @@ -46,7 +46,7 @@ const ipfsUploader: Uploader = { await fetchWorker(url, { method: 'POST', - signal, + signal: opts?.signal, }); }, }; diff --git a/src/uploaders/s3.ts b/src/uploaders/s3.ts index 2e02cc30..378b2790 100644 --- a/src/uploaders/s3.ts +++ b/src/uploaders/s3.ts @@ -9,10 +9,9 @@ import type { Uploader } from './types.ts'; * take advantage of IPFS features while not really using IPFS. */ const s3Uploader: Uploader = { - async upload(file, _signal) { + async upload(file) { const cid = await IpfsHash.of(file.stream()) as string; - // FIXME: Can't cancel S3 requests: https://github.com/bradenmacdonald/deno-s3-lite-client/issues/24 await client().putObject(`ipfs/${cid}`, file.stream(), { metadata: { 'Content-Type': file.type, @@ -24,8 +23,7 @@ const s3Uploader: Uploader = { cid, }; }, - async delete(cid, _signal) { - // FIXME: Can't cancel S3 requests: https://github.com/bradenmacdonald/deno-s3-lite-client/issues/24 + async delete(cid) { await client().deleteObject(`ipfs/${cid}`); }, }; diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index 8f115459..88980483 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -1,9 +1,9 @@ /** Modular uploader interface, to support uploading to different backends. */ interface Uploader { /** Upload the file to the backend. */ - upload(file: File, signal?: AbortSignal): Promise; + upload(file: File, opts?: { signal?: AbortSignal }): Promise; /** Delete the file from the backend. */ - delete(cid: string, signal?: AbortSignal): Promise; + delete(cid: string, opts?: { signal?: AbortSignal }): Promise; } /** Return value from the uploader after uploading a file. */ From 7ada849a6a217a462ab8c0205d40eb276320230c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Apr 2024 15:54:01 -0500 Subject: [PATCH 005/252] s3: support pathStyle --- deno.json | 3 +++ src/upload.ts | 3 +-- src/uploaders/ipfs.ts | 6 ++++-- src/uploaders/s3.ts | 31 ++++++++++++++++++++----------- src/uploaders/types.ts | 10 ++++++++-- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/deno.json b/deno.json index c5681e3d..d2412626 100644 --- a/deno.json +++ b/deno.json @@ -18,7 +18,10 @@ "@/": "./src/", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@std/cli": "jsr:@std/cli@^0.223.0", + "@std/crypto": "jsr:@std/crypto@^0.224.0", + "@std/encoding": "jsr:@std/encoding@^0.224.0", "@std/json": "jsr:@std/json@^0.223.0", + "@std/media-types": "jsr:@std/media-types@^0.224.0", "@std/streams": "jsr:@std/streams@^0.223.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", diff --git a/src/upload.ts b/src/upload.ts index 5c165013..632dbabf 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -16,8 +16,7 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal) { throw new Error('File size is too large.'); } - const { cid } = await uploader.upload(file, signal); - const url = new URL(`/ipfs/${cid}`, Conf.mediaDomain).toString(); + const { url } = await uploader.upload(file, { signal }); return insertUnattachedMedia({ pubkey, diff --git a/src/uploaders/ipfs.ts b/src/uploaders/ipfs.ts index 6e0e0e7e..21619b5b 100644 --- a/src/uploaders/ipfs.ts +++ b/src/uploaders/ipfs.ts @@ -30,10 +30,12 @@ const ipfsUploader: Uploader = { signal: opts?.signal, }); - const { Hash } = ipfsAddResponseSchema.parse(await response.json()); + const { Hash: cid } = ipfsAddResponseSchema.parse(await response.json()); return { - cid: Hash, + id: cid, + cid, + url: new URL(`/ipfs/${cid}`, Conf.mediaDomain).toString(), }; }, async delete(cid, opts) { diff --git a/src/uploaders/s3.ts b/src/uploaders/s3.ts index 378b2790..29f3043f 100644 --- a/src/uploaders/s3.ts +++ b/src/uploaders/s3.ts @@ -1,30 +1,39 @@ +import { join } from 'node:path'; + +import { crypto } from '@std/crypto'; +import { encodeHex } from '@std/encoding/hex'; +import { extensionsByType } from '@std/media-types'; + import { Conf } from '@/config.ts'; -import { IpfsHash, S3Client } from '@/deps.ts'; +import { S3Client } from '@/deps.ts'; import type { Uploader } from './types.ts'; -/** - * S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. - * Files are named by their IPFS CID and exposed at `/ipfs/`, letting it - * take advantage of IPFS features while not really using IPFS. - */ +/** S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. */ const s3Uploader: Uploader = { async upload(file) { - const cid = await IpfsHash.of(file.stream()) as string; + const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); + const ext = extensionsByType(file.type)?.[0] ?? 'bin'; + const filename = `${sha256}.${ext}`; - await client().putObject(`ipfs/${cid}`, file.stream(), { + await client().putObject(filename, file.stream(), { metadata: { 'Content-Type': file.type, 'x-amz-acl': 'public-read', }, }); + const { pathStyle, bucket } = Conf.s3; + const path = (pathStyle && bucket) ? join(bucket, filename) : filename; + return { - cid, + id: filename, + sha256, + url: new URL(path, Conf.mediaDomain).toString(), }; }, - async delete(cid) { - await client().deleteObject(`ipfs/${cid}`); + async delete(id) { + await client().deleteObject(id); }, }; diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index 88980483..c514ad1b 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -8,8 +8,14 @@ interface Uploader { /** Return value from the uploader after uploading a file. */ interface UploadResult { - /** IPFS CID for the file. */ - cid: string; + /** File ID specific to the uploader, so it can later be referenced or deleted. */ + id: string; + /** URL where the file can be accessed. */ + url: string; + /** SHA-256 hash of the file. */ + sha256?: string; + /** IPFS CID of the file. */ + cid?: string; } export type { Uploader }; From 303b0fe09859c640c43277d958a45e90e2dcce37 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Apr 2024 16:28:54 -0500 Subject: [PATCH 006/252] Add localUploader --- src/config.ts | 4 ++++ src/uploaders/config.ts | 13 ++++++++----- src/uploaders/local.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/uploaders/local.ts diff --git a/src/config.ts b/src/config.ts index c4d6a9b5..c2595636 100644 --- a/src/config.ts +++ b/src/config.ts @@ -139,6 +139,10 @@ class Conf { static get uploader() { return Deno.env.get('DITTO_UPLOADER'); } + /** Location to use for local uploads. */ + static get uploadsDir() { + return Deno.env.get('UPLOADS_DIR') || 'data/uploads'; + } /** Media base URL for uploads. */ static get mediaDomain() { const value = Deno.env.get('MEDIA_DOMAIN'); diff --git a/src/uploaders/config.ts b/src/uploaders/config.ts index 83874f6e..5b4c7aff 100644 --- a/src/uploaders/config.ts +++ b/src/uploaders/config.ts @@ -1,7 +1,8 @@ import { Conf } from '@/config.ts'; -import { ipfsUploader } from './ipfs.ts'; -import { s3Uploader } from './s3.ts'; +import { ipfsUploader } from '@/uploaders/ipfs.ts'; +import { localUploader } from '@/uploaders/local.ts'; +import { s3Uploader } from '@/uploaders/s3.ts'; import type { Uploader } from './types.ts'; @@ -10,8 +11,8 @@ const configUploader: Uploader = { upload(file, opts) { return uploader().upload(file, opts); }, - delete(cid, opts) { - return uploader().delete(cid, opts); + delete(id, opts) { + return uploader().delete(id, opts); }, }; @@ -22,8 +23,10 @@ function uploader() { return s3Uploader; case 'ipfs': return ipfsUploader; + case 'local': + return localUploader; default: - return ipfsUploader; + throw new Error('No `DITTO_UPLOADER` configured. Uploads are disabled.'); } } diff --git a/src/uploaders/local.ts b/src/uploaders/local.ts new file mode 100644 index 00000000..a2381a3b --- /dev/null +++ b/src/uploaders/local.ts @@ -0,0 +1,36 @@ +import { join } from 'node:path'; + +import { crypto } from '@std/crypto'; +import { encodeHex } from '@std/encoding/hex'; +import { extensionsByType } from '@std/media-types'; + +import { Conf } from '@/config.ts'; + +import type { Uploader } from './types.ts'; + +/** Local filesystem uploader. */ +const localUploader: Uploader = { + async upload(file) { + const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); + const ext = extensionsByType(file.type)?.[0] ?? 'bin'; + const filename = `${sha256}.${ext}`; + + await Deno.mkdir(Conf.uploadsDir, { recursive: true }); + await Deno.writeFile(join(Conf.uploadsDir, filename), file.stream()); + + const { mediaDomain } = Conf; + const url = new URL(mediaDomain); + const path = url.pathname === '/' ? filename : join(url.pathname, filename); + + return { + id: filename, + sha256, + url: new URL(path, url).toString(), + }; + }, + async delete(id) { + await Deno.remove(join(Conf.uploadsDir, id)); + }, +}; + +export { localUploader }; From d99fd753ee986765308c66f030b781de926e73d6 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 30 Apr 2024 11:18:20 -0300 Subject: [PATCH 007/252] refactor(queries): convert getDescendants to async function --- src/queries.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/queries.ts b/src/queries.ts index cf61b84f..147e96c5 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -82,9 +82,9 @@ async function getAncestors(event: NostrEvent, result: NostrEvent[] = []): Promi return result.reverse(); } -function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise { - return eventsDB.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); +async function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise { + const events = await eventsDB.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); + return hydrateEvents({ events, storage: eventsDB, signal }); } /** Returns whether the pubkey is followed by a local user. */ From 0e6b4e8b45b7899ed4133dffc8006b0bb12e19f6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 12:37:27 -0500 Subject: [PATCH 008/252] sentryMiddleware: ignore HTTPException errors --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 7e74f6d9..6143b6c0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -103,7 +103,7 @@ const app = new Hono(); if (Conf.sentryDsn) { // @ts-ignore Mismatched hono types. - app.use('*', sentryMiddleware({ dsn: Conf.sentryDsn })); + app.use('*', sentryMiddleware({ dsn: Conf.sentryDsn, ignoreErrors: 'HTTPException' })); } const debug = Debug('ditto:http'); From 9ecf5db1b15c27b0590d859887686c0e77550056 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 12:46:29 -0500 Subject: [PATCH 009/252] hono: catch HTTPException --- src/app.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index 6143b6c0..c759be3c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,13 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; -import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; +import { + type Context, + Env as HonoEnv, + type Handler, + Hono, + HTTPException, + Input as HonoInput, + type MiddlewareHandler, +} from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; import { Conf } from '@/config.ts'; @@ -103,9 +111,16 @@ const app = new Hono(); if (Conf.sentryDsn) { // @ts-ignore Mismatched hono types. - app.use('*', sentryMiddleware({ dsn: Conf.sentryDsn, ignoreErrors: 'HTTPException' })); + app.use('*', sentryMiddleware({ dsn: Conf.sentryDsn })); } +app.onError((err) => { + if (err instanceof HTTPException) { + return err.getResponse(); + } + throw err; +}); + const debug = Debug('ditto:http'); app.use('/api/*', logger(debug)); From f651bf416ad04a882dc382c938f8f4afac3a77ad Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 12:52:20 -0500 Subject: [PATCH 010/252] sentry: skip "no pubkey provided" error --- src/app.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app.ts b/src/app.ts index c759be3c..7eaec98e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -111,16 +111,15 @@ const app = new Hono(); if (Conf.sentryDsn) { // @ts-ignore Mismatched hono types. - app.use('*', sentryMiddleware({ dsn: Conf.sentryDsn })); + app.use( + '*', + sentryMiddleware({ + dsn: Conf.sentryDsn, + ignoreErrors: ['No pubkey provided'], + }), + ); } -app.onError((err) => { - if (err instanceof HTTPException) { - return err.getResponse(); - } - throw err; -}); - const debug = Debug('ditto:http'); app.use('/api/*', logger(debug)); From e722e754cd012d6c3595e51b53ea078cb2fec90a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 12:55:39 -0500 Subject: [PATCH 011/252] deno lint --- src/app.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app.ts b/src/app.ts index 7eaec98e..e58d1cb6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,13 +1,5 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; -import { - type Context, - Env as HonoEnv, - type Handler, - Hono, - HTTPException, - Input as HonoInput, - type MiddlewareHandler, -} from 'hono'; +import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; import { Conf } from '@/config.ts'; From f2b36f75f0061cce348e3a623a3efb723a605ba7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 13:01:43 -0500 Subject: [PATCH 012/252] Remove hono/sentry middleware, upgrade @sentry/deno --- deno.json | 1 + src/app.ts | 14 +------------- src/deps.ts | 2 -- src/sentry.ts | 5 +++-- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/deno.json b/deno.json index d2412626..6d047329 100644 --- a/deno.json +++ b/deno.json @@ -17,6 +17,7 @@ "imports": { "@/": "./src/", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", + "@sentry/deno": "npm:@sentry/deno@^7.112.2", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", diff --git a/src/app.ts b/src/app.ts index e58d1cb6..9911f0cf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,10 +2,9 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; -import { Conf } from '@/config.ts'; import '@/cron.ts'; import { type User } from '@/db/users.ts'; -import { Debug, sentryMiddleware } from '@/deps.ts'; +import { Debug } from '@/deps.ts'; import '@/firehose.ts'; import { Time } from '@/utils.ts'; @@ -101,17 +100,6 @@ type AppController = Handler(); -if (Conf.sentryDsn) { - // @ts-ignore Mismatched hono types. - app.use( - '*', - sentryMiddleware({ - dsn: Conf.sentryDsn, - ignoreErrors: ['No pubkey provided'], - }), - ); -} - const debug = Debug('ditto:http'); app.use('/api/*', logger(debug)); diff --git a/src/deps.ts b/src/deps.ts index 3beda47a..f7e09cbf 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -62,8 +62,6 @@ export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; export { default as uuid62 } from 'npm:uuid62@^1.0.2'; export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts'; -export * as Sentry from 'https://deno.land/x/sentry@7.78.0/index.js'; -export { sentry as sentryMiddleware } from 'npm:@hono/sentry@^1.0.0'; export * as Comlink from 'npm:comlink@^4.4.1'; export { EventEmitter } from 'npm:tseep@^1.1.3'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; diff --git a/src/sentry.ts b/src/sentry.ts index eefe9c59..84b662e2 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -1,5 +1,6 @@ -import { Conf } from './config.ts'; -import { Sentry } from './deps.ts'; +import * as Sentry from '@sentry/deno'; + +import { Conf } from '@/config.ts'; // Sentry if (Conf.sentryDsn) { From 18a3d0f9ad784ad5aa352cb5b4570ac69c464add Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 13:09:01 -0500 Subject: [PATCH 013/252] Switch back to @sentry/deno from deno.land --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 6d047329..b83aaa69 100644 --- a/deno.json +++ b/deno.json @@ -17,7 +17,7 @@ "imports": { "@/": "./src/", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", - "@sentry/deno": "npm:@sentry/deno@^7.112.2", + "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", From d22c606960a5e50f938075f7f3778dc9aa55cef5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 13:27:30 -0500 Subject: [PATCH 014/252] storeMiddleware: pass through admin UserStore --- src/middleware/store.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/middleware/store.ts b/src/middleware/store.ts index a22bb790..67bee0a0 100644 --- a/src/middleware/store.ts +++ b/src/middleware/store.ts @@ -1,16 +1,18 @@ import { AppMiddleware } from '@/app.ts'; +import { Conf } from '@/config.ts'; import { UserStore } from '@/storages/UserStore.ts'; import { eventsDB } from '@/storages.ts'; /** Store middleware. */ const storeMiddleware: AppMiddleware = async (c, next) => { const pubkey = c.get('pubkey'); + const adminStore = new UserStore(Conf.pubkey, eventsDB); if (pubkey) { - const store = new UserStore(pubkey, eventsDB); + const store = new UserStore(pubkey, adminStore); c.set('store', store); } else { - c.set('store', eventsDB); + c.set('store', adminStore); } await next(); }; From 35ab012276b215fe715a357db1185356d48d5a2d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 14:58:00 -0500 Subject: [PATCH 015/252] hashtagTimelineController: toLowerCase --- src/controllers/api/timelines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 40b54c93..27459a82 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -37,7 +37,7 @@ const publicTimelineController: AppController = (c) => { }; const hashtagTimelineController: AppController = (c) => { - const hashtag = c.req.param('hashtag')!; + const hashtag = c.req.param('hashtag')!.toLowerCase(); const params = paginationSchema.parse(c.req.query()); return renderStatuses(c, [{ kinds: [1], '#t': [hashtag], ...params }]); }; From 0bb4ccf5c97e9cca4116162cb5c1017a50b3487e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 18:43:53 -0500 Subject: [PATCH 016/252] Move nostr-tools to an import alias --- deno.json | 2 ++ src/config.ts | 4 +++- src/controllers/api/accounts.ts | 2 +- src/controllers/api/oauth.ts | 3 ++- src/controllers/api/search.ts | 2 +- src/controllers/api/statuses.ts | 3 ++- src/controllers/well-known/webfinger.ts | 2 +- src/deps.ts | 18 ------------------ src/middleware/auth19.ts | 3 ++- src/nostr-wasm.ts | 4 ++++ src/note.ts | 4 +++- src/schemas/nostr.ts | 2 +- src/server.ts | 1 + src/storages/InternalRelay.ts | 2 +- src/storages/hydrate.ts | 2 +- src/storages/pool-store.ts | 4 +++- src/utils.ts | 2 +- src/utils/api.ts | 3 ++- src/utils/nip05.ts | 4 +++- src/utils/nip98.ts | 3 ++- src/views/mastodon/accounts.ts | 4 +++- src/views/mastodon/emojis.ts | 3 ++- src/views/mastodon/statuses.ts | 2 +- src/workers/verify.worker.ts | 5 ++++- 24 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 src/nostr-wasm.ts diff --git a/deno.json b/deno.json index b83aaa69..a6175693 100644 --- a/deno.json +++ b/deno.json @@ -26,6 +26,8 @@ "@std/streams": "jsr:@std/streams@^0.223.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", + "nostr-tools": "npm:nostr-tools@^2.5.1", + "nostr-wasm": "npm:nostr-wasm@^0.1.0", "kysely": "npm:kysely@^0.26.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", "zod": "npm:zod@^3.23.4", diff --git a/src/config.ts b/src/config.ts index c2595636..8331f715 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,9 @@ import url from 'node:url'; + +import { getPublicKey, nip19 } from 'nostr-tools'; import { z } from 'zod'; -import { dotenv, getPublicKey, nip19 } from '@/deps.ts'; +import { dotenv } from '@/deps.ts'; /** Load environment config from `.env` */ await dotenv.load({ diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 02897993..a3ac3786 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -1,9 +1,9 @@ import { NostrFilter } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { nip19 } from '@/deps.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index 7ada2a4c..4f1f4959 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -1,6 +1,7 @@ +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; -import { lodash, nip19 } from '@/deps.ts'; +import { lodash } from '@/deps.ts'; import { AppController } from '@/app.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 2facd803..63561c45 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -1,8 +1,8 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { nip19 } from '@/deps.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; import { searchStore } from '@/storages.ts'; diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index d257d989..2c94a719 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,10 +1,11 @@ import { NIP05, NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; -import { ISO6391, nip19 } from '@/deps.ts'; +import { ISO6391 } from '@/deps.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { addTag, deleteTag } from '@/tags.ts'; diff --git a/src/controllers/well-known/webfinger.ts b/src/controllers/well-known/webfinger.ts index 9a245767..38bc9943 100644 --- a/src/controllers/well-known/webfinger.ts +++ b/src/controllers/well-known/webfinger.ts @@ -1,7 +1,7 @@ +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { Conf } from '@/config.ts'; -import { nip19 } from '@/deps.ts'; import { localNip05Lookup } from '@/utils/nip05.ts'; import type { AppContext, AppController } from '@/app.ts'; diff --git a/src/deps.ts b/src/deps.ts index f7e09cbf..1c001d72 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,18 +1,5 @@ import 'https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts'; export { RelayPoolWorker } from 'npm:nostr-relaypool2@0.6.34'; -export { - type EventTemplate, - getEventHash, - matchFilter, - matchFilters, - nip05, - nip13, - nip19, - nip21, - type UnsignedEvent, - type VerifiedEvent, -} from 'npm:nostr-tools@^2.3.1'; -export { finalizeEvent, getPublicKey, verifyEvent } from 'npm:nostr-tools@^2.3.1/wasm'; export { parseFormData } from 'npm:formdata-helper@^0.3.0'; // @deno-types="npm:@types/lodash@4.14.194" export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; @@ -69,8 +56,3 @@ export { default as Debug } from 'https://gitlab.com/soapbox-pub/stickynotes/-/r export { Stickynotes } from 'https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/mod.ts'; export type * as TypeFest from 'npm:type-fest@^4.3.0'; - -import { setNostrWasm } from 'npm:nostr-tools@^2.3.1/wasm'; -import { initNostrWasm } from 'npm:nostr-wasm@^0.1.0'; - -await initNostrWasm().then(setNostrWasm); diff --git a/src/middleware/auth19.ts b/src/middleware/auth19.ts index d81c2571..90fc4444 100644 --- a/src/middleware/auth19.ts +++ b/src/middleware/auth19.ts @@ -1,6 +1,7 @@ import { HTTPException } from 'hono'; +import { getPublicKey, nip19 } from 'nostr-tools'; + import { type AppMiddleware } from '@/app.ts'; -import { getPublicKey, nip19 } from '@/deps.ts'; /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); diff --git a/src/nostr-wasm.ts b/src/nostr-wasm.ts new file mode 100644 index 00000000..44135909 --- /dev/null +++ b/src/nostr-wasm.ts @@ -0,0 +1,4 @@ +import { setNostrWasm } from 'nostr-tools/wasm'; +import { initNostrWasm } from 'nostr-wasm'; + +await initNostrWasm().then(setNostrWasm); diff --git a/src/note.ts b/src/note.ts index a19a7936..fe03d7fc 100644 --- a/src/note.ts +++ b/src/note.ts @@ -1,5 +1,7 @@ +import { nip19, nip21 } from 'nostr-tools'; + import { Conf } from '@/config.ts'; -import { linkify, linkifyStr, mime, nip19, nip21 } from '@/deps.ts'; +import { linkify, linkifyStr, mime } from '@/deps.ts'; import { type DittoAttachment } from '@/views/mastodon/attachments.ts'; linkify.registerCustomProtocol('nostr', true); diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index 0497093d..7f51f6c1 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -1,6 +1,6 @@ +import { getEventHash, verifyEvent } from 'nostr-tools'; import { z } from 'zod'; -import { getEventHash, verifyEvent } from '@/deps.ts'; import { jsonSchema, safeUrlSchema } from '@/schema.ts'; /** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */ diff --git a/src/server.ts b/src/server.ts index 68af681e..4825e99d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,6 @@ import '@/precheck.ts'; import '@/sentry.ts'; +import '@/nostr-wasm.ts'; import app from '@/app.ts'; import { Conf } from '@/config.ts'; diff --git a/src/storages/InternalRelay.ts b/src/storages/InternalRelay.ts index d42f94f7..233a095c 100644 --- a/src/storages/InternalRelay.ts +++ b/src/storages/InternalRelay.ts @@ -9,8 +9,8 @@ import { NRelay, } from '@nostrify/nostrify'; import { Machina } from '@nostrify/nostrify/utils'; +import { matchFilter } from 'nostr-tools'; -import { matchFilter } from '@/deps.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 619b7986..dbe277ad 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -1,7 +1,7 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; +import { matchFilter } from 'nostr-tools'; import { db } from '@/db.ts'; -import { matchFilter } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Conf } from '@/config.ts'; diff --git a/src/storages/pool-store.ts b/src/storages/pool-store.ts index 6620ec94..93ca24e9 100644 --- a/src/storages/pool-store.ts +++ b/src/storages/pool-store.ts @@ -1,5 +1,7 @@ import { NostrEvent, NostrFilter, NSet, NStore } from '@nostrify/nostrify'; -import { Debug, matchFilters, type RelayPoolWorker } from '@/deps.ts'; +import { matchFilters } from 'nostr-tools'; + +import { Debug, type RelayPoolWorker } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { abortError } from '@/utils/abort.ts'; diff --git a/src/utils.ts b/src/utils.ts index 747ea43f..085d8af6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import { NostrEvent } from '@nostrify/nostrify'; +import { EventTemplate, getEventHash, nip19 } from 'nostr-tools'; import { z } from 'zod'; -import { type EventTemplate, getEventHash, nip19 } from '@/deps.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; /** Get the current time in Nostr format. */ diff --git a/src/utils/api.ts b/src/utils/api.ts index cd4e6e21..3f89d3f8 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,10 +1,11 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { type Context, HTTPException } from 'hono'; +import { EventTemplate } from 'nostr-tools'; import { z } from 'zod'; import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { Debug, EventTemplate, parseFormData, type TypeFest } from '@/deps.ts'; +import { Debug, parseFormData, type TypeFest } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; diff --git a/src/utils/nip05.ts b/src/utils/nip05.ts index 4de64112..d7a2d983 100644 --- a/src/utils/nip05.ts +++ b/src/utils/nip05.ts @@ -1,6 +1,8 @@ import { NIP05 } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; + import { Conf } from '@/config.ts'; -import { Debug, nip19 } from '@/deps.ts'; +import { Debug } from '@/deps.ts'; import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { Time } from '@/utils/time.ts'; import { eventsDB } from '@/storages.ts'; diff --git a/src/utils/nip98.ts b/src/utils/nip98.ts index 80df9aef..74e60a4f 100644 --- a/src/utils/nip98.ts +++ b/src/utils/nip98.ts @@ -1,5 +1,6 @@ import { NostrEvent } from '@nostrify/nostrify'; -import { type EventTemplate, nip13 } from '@/deps.ts'; +import { EventTemplate, nip13 } from 'nostr-tools'; + import { decode64Schema, jsonSchema } from '@/schema.ts'; import { signedEventSchema } from '@/schemas/nostr.ts'; import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts'; diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index a0f9bb74..18f3a9dc 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -1,5 +1,7 @@ +import { nip19, UnsignedEvent } from 'nostr-tools'; + import { Conf } from '@/config.ts'; -import { lodash, nip19, type UnsignedEvent } from '@/deps.ts'; +import { lodash } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { getLnurl } from '@/utils/lnurl.ts'; diff --git a/src/views/mastodon/emojis.ts b/src/views/mastodon/emojis.ts index 0ba28956..089f2cb1 100644 --- a/src/views/mastodon/emojis.ts +++ b/src/views/mastodon/emojis.ts @@ -1,4 +1,5 @@ -import { UnsignedEvent } from '@/deps.ts'; +import { UnsignedEvent } from 'nostr-tools'; + import { EmojiTag, emojiTagSchema } from '@/schemas/nostr.ts'; import { filteredArray } from '@/schema.ts'; diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index b3675554..06ef164a 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -1,8 +1,8 @@ import { NostrEvent } from '@nostrify/nostrify'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; +import { nip19 } from 'nostr-tools'; import { Conf } from '@/config.ts'; -import { nip19 } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; diff --git a/src/workers/verify.worker.ts b/src/workers/verify.worker.ts index 0b5f6681..d72387a2 100644 --- a/src/workers/verify.worker.ts +++ b/src/workers/verify.worker.ts @@ -1,5 +1,8 @@ import { NostrEvent } from '@nostrify/nostrify'; -import { Comlink, type VerifiedEvent, verifyEvent } from '@/deps.ts'; +import { VerifiedEvent, verifyEvent } from 'nostr-tools'; + +import { Comlink } from '@/deps.ts'; +import '@/nostr-wasm.ts'; export const VerifyWorker = { verifyEvent(event: NostrEvent): event is VerifiedEvent { From ba6d33c115fbbb6e1166eb18d893d78a363a4220 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 30 Apr 2024 21:20:19 -0300 Subject: [PATCH 017/252] feat: create getAdminStore() func --- src/storages/adminStore.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/storages/adminStore.ts diff --git a/src/storages/adminStore.ts b/src/storages/adminStore.ts new file mode 100644 index 00000000..0b5bb384 --- /dev/null +++ b/src/storages/adminStore.ts @@ -0,0 +1,7 @@ +import { UserStore } from '@/storages/UserStore.ts'; +import { Conf } from '@/config.ts'; +import { eventsDB } from '@/storages.ts'; + +export function getAdminStore() { + return new UserStore(Conf.pubkey, eventsDB); +} From e8e45360d32e2741f3c3ecbc810f79de6cfc1add Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 30 Apr 2024 21:21:32 -0300 Subject: [PATCH 018/252] refactor(store middleware): get adminStore through function --- src/middleware/store.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/middleware/store.ts b/src/middleware/store.ts index 67bee0a0..f0c417fb 100644 --- a/src/middleware/store.ts +++ b/src/middleware/store.ts @@ -1,13 +1,11 @@ import { AppMiddleware } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { UserStore } from '@/storages/UserStore.ts'; -import { eventsDB } from '@/storages.ts'; +import { getAdminStore } from '@/storages/adminStore.ts'; /** Store middleware. */ const storeMiddleware: AppMiddleware = async (c, next) => { const pubkey = c.get('pubkey'); - const adminStore = new UserStore(Conf.pubkey, eventsDB); - + const adminStore = getAdminStore(); if (pubkey) { const store = new UserStore(pubkey, adminStore); c.set('store', store); From f0c66c1e92c4c679bd6bdbd383dd1146f8b7b04f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 30 Apr 2024 21:23:25 -0300 Subject: [PATCH 019/252] fix(streaming): don't show posts from blocked users --- src/controllers/api/streaming.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 668218dc..c190c3e3 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -8,8 +8,9 @@ import { getFeedPubkeys } from '@/queries.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { eventsDB } from '@/storages.ts'; import { Storages } from '@/storages.ts'; +import { UserStore } from '@/storages/UserStore.ts'; +import { getAdminStore } from '@/storages/adminStore.ts'; const debug = Debug('ditto:streaming'); @@ -71,9 +72,14 @@ const streamingController: AppController = (c) => { try { for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { - const [event] = await hydrateEvents({ - events: [msg[2]], - storage: eventsDB, + const store = new UserStore(pubkey as string, getAdminStore()); + + const [event] = await store.query([{ ids: [msg[2].id] }]); + if (!event) continue; + + await hydrateEvents({ + events: [event], + storage: store, signal: AbortSignal.timeout(1000), }); From de08aeac104c8bca5b93bf7b1b58d7cd8b71e695 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 30 Apr 2024 21:28:45 -0300 Subject: [PATCH 020/252] fix: allow to query kind 0 of blocked users --- src/storages/UserStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/UserStore.ts b/src/storages/UserStore.ts index 78c3d336..a3f0726c 100644 --- a/src/storages/UserStore.ts +++ b/src/storages/UserStore.ts @@ -29,7 +29,7 @@ export class UserStore implements NStore { const mutedPubkeys = getTagSet(mutedPubkeysEvent.tags, 'p'); return allEvents.filter((event) => { - return mutedPubkeys.has(event.pubkey) === false; + return event.kind === 0 || mutedPubkeys.has(event.pubkey) === false; }); } From 6cc44c468e094d645b20b56d4f4661567a250957 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 30 Apr 2024 20:38:21 -0500 Subject: [PATCH 021/252] Don't await cleanupMedia on startup --- src/cron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cron.ts b/src/cron.ts index bfaf773d..bbddaf3b 100644 --- a/src/cron.ts +++ b/src/cron.ts @@ -25,5 +25,5 @@ async function cleanupMedia() { console.info(`Removed ${media?.length ?? 0} orphaned media files.`); } -await cleanupMedia(); +cleanupMedia(); cron.every15Minute(cleanupMedia); From caa9e471615e8964dc7b51c77668905e0c0423b8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 09:08:36 -0500 Subject: [PATCH 022/252] Remove cron.ts --- src/app.ts | 1 - src/cron.ts | 29 ----------------------------- 2 files changed, 30 deletions(-) delete mode 100644 src/cron.ts diff --git a/src/app.ts b/src/app.ts index 9911f0cf..370abc5a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,6 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; -import '@/cron.ts'; import { type User } from '@/db/users.ts'; import { Debug } from '@/deps.ts'; import '@/firehose.ts'; diff --git a/src/cron.ts b/src/cron.ts deleted file mode 100644 index bbddaf3b..00000000 --- a/src/cron.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { deleteUnattachedMediaByUrl, getUnattachedMedia } from '@/db/unattached-media.ts'; -import { cron } from '@/deps.ts'; -import { Time } from '@/utils/time.ts'; -import { configUploader as uploader } from '@/uploaders/config.ts'; -import { cidFromUrl } from '@/utils/ipfs.ts'; - -/** Delete files that aren't attached to any events. */ -async function cleanupMedia() { - console.info('Deleting orphaned media files...'); - - const until = new Date(Date.now() - Time.minutes(15)); - const media = await getUnattachedMedia(until); - - for (const { url } of media) { - const cid = cidFromUrl(new URL(url))!; - try { - await uploader.delete(cid); - await deleteUnattachedMediaByUrl(url); - } catch (e) { - console.error(`Failed to delete file ${url}`); - console.error(e); - } - } - - console.info(`Removed ${media?.length ?? 0} orphaned media files.`); -} - -cleanupMedia(); -cron.every15Minute(cleanupMedia); From 23e00b0042c64abc3e09ce53c9ebd7c47dc38f3a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 09:26:17 -0500 Subject: [PATCH 023/252] Make Kysely an import alias --- src/db.ts | 3 ++- src/db/DittoDB.ts | 3 ++- src/db/adapters/DittoSQLite.ts | 4 +++- src/deps.ts | 12 ------------ src/pipeline.ts | 4 +++- src/stats.ts | 4 +++- src/storages/events-db.ts | 4 +++- src/workers/sqlite.ts | 3 ++- src/workers/sqlite.worker.ts | 4 +++- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/db.ts b/src/db.ts index 7125f136..1a5f06d8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,8 +1,9 @@ import fs from 'node:fs/promises'; import path from 'node:path'; +import { FileMigrationProvider, Migrator } from 'kysely'; + import { DittoDB } from '@/db/DittoDB.ts'; -import { FileMigrationProvider, Migrator } from '@/deps.ts'; const db = await DittoDB.getInstance(); diff --git a/src/db/DittoDB.ts b/src/db/DittoDB.ts index 8ebe5e6a..abe068b8 100644 --- a/src/db/DittoDB.ts +++ b/src/db/DittoDB.ts @@ -1,8 +1,9 @@ +import { Kysely } from 'kysely'; + import { Conf } from '@/config.ts'; import { DittoPostgres } from '@/db/adapters/DittoPostgres.ts'; import { DittoSQLite } from '@/db/adapters/DittoSQLite.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { Kysely } from '@/deps.ts'; export class DittoDB { static getInstance(): Promise> { diff --git a/src/db/adapters/DittoSQLite.ts b/src/db/adapters/DittoSQLite.ts index c91407a1..cc05fa01 100644 --- a/src/db/adapters/DittoSQLite.ts +++ b/src/db/adapters/DittoSQLite.ts @@ -1,6 +1,8 @@ +import { Kysely, sql } from 'kysely'; + import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { Kysely, PolySqliteDialect, sql } from '@/deps.ts'; +import { PolySqliteDialect } from '@/deps.ts'; import SqliteWorker from '@/workers/sqlite.ts'; export class DittoSQLite { diff --git a/src/deps.ts b/src/deps.ts index 1c001d72..be507007 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -30,18 +30,6 @@ export { } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; export * as dotenv from 'https://deno.land/std@0.198.0/dotenv/mod.ts'; -export { - type CompiledQuery, - FileMigrationProvider, - type Insertable, - type InsertQueryBuilder, - Kysely, - Migrator, - type NullableInsertKeys, - type QueryResult, - type SelectQueryBuilder, - sql, -} from 'npm:kysely@^0.26.3'; export { PolySqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/v2.0.0/mod.ts'; export { default as tldts } from 'npm:tldts@^6.0.14'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; diff --git a/src/pipeline.ts b/src/pipeline.ts index 1b19a78c..c4b94be6 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,10 +1,12 @@ import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; +import { sql } from 'kysely'; + import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; import { addRelays } from '@/db/relays.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; -import { Debug, sql } from '@/deps.ts'; +import { Debug } from '@/deps.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; diff --git a/src/stats.ts b/src/stats.ts index 48cc41b2..6bfe568c 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,7 +1,9 @@ import { NostrEvent } from '@nostrify/nostrify'; +import { InsertQueryBuilder } from 'kysely'; + import { db } from '@/db.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { Debug, type InsertQueryBuilder } from '@/deps.ts'; +import { Debug } from '@/deps.ts'; import { eventsDB } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 6d80f70c..22f15e11 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,7 +1,9 @@ import { NIP50, NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; +import { Kysely, type SelectQueryBuilder } from 'kysely'; + import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { Debug, Kysely, type SelectQueryBuilder } from '@/deps.ts'; +import { Debug } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; diff --git a/src/workers/sqlite.ts b/src/workers/sqlite.ts index a6d2fac2..1d29d4a7 100644 --- a/src/workers/sqlite.ts +++ b/src/workers/sqlite.ts @@ -1,7 +1,8 @@ +import type { CompiledQuery, QueryResult } from 'kysely'; + import { Comlink } from '@/deps.ts'; import type { SqliteWorker as _SqliteWorker } from './sqlite.worker.ts'; -import type { CompiledQuery, QueryResult } from '@/deps.ts'; class SqliteWorker { #worker: Worker; diff --git a/src/workers/sqlite.worker.ts b/src/workers/sqlite.worker.ts index 1222e337..4739638a 100644 --- a/src/workers/sqlite.worker.ts +++ b/src/workers/sqlite.worker.ts @@ -1,6 +1,8 @@ /// import { ScopedPerformance } from 'https://deno.land/x/scoped_performance@v2.0.0/mod.ts'; -import { Comlink, type CompiledQuery, DenoSqlite3, type QueryResult, Stickynotes } from '@/deps.ts'; +import { CompiledQuery, QueryResult } from 'kysely'; + +import { Comlink, DenoSqlite3, Stickynotes } from '@/deps.ts'; import '@/sentry.ts'; let db: DenoSqlite3 | undefined; From 4291691aa756412366462953692bfc44fb19e48c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 09:27:19 -0500 Subject: [PATCH 024/252] Make @soapbox/kysely-deno-sqlite an import alias --- deno.json | 5 +++-- src/db/adapters/DittoSQLite.ts | 2 +- src/deps.ts | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deno.json b/deno.json index a6175693..72675bb5 100644 --- a/deno.json +++ b/deno.json @@ -18,6 +18,7 @@ "@/": "./src/", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", + "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", @@ -26,10 +27,10 @@ "@std/streams": "jsr:@std/streams@^0.223.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", - "nostr-tools": "npm:nostr-tools@^2.5.1", - "nostr-wasm": "npm:nostr-wasm@^0.1.0", "kysely": "npm:kysely@^0.26.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", + "nostr-tools": "npm:nostr-tools@^2.5.1", + "nostr-wasm": "npm:nostr-wasm@^0.1.0", "zod": "npm:zod@^3.23.4", "~/fixtures/": "./fixtures/" }, diff --git a/src/db/adapters/DittoSQLite.ts b/src/db/adapters/DittoSQLite.ts index cc05fa01..6848aeec 100644 --- a/src/db/adapters/DittoSQLite.ts +++ b/src/db/adapters/DittoSQLite.ts @@ -1,8 +1,8 @@ +import { PolySqliteDialect } from '@soapbox/kysely-deno-sqlite'; import { Kysely, sql } from 'kysely'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { PolySqliteDialect } from '@/deps.ts'; import SqliteWorker from '@/workers/sqlite.ts'; export class DittoSQLite { diff --git a/src/deps.ts b/src/deps.ts index be507007..2c84d9e3 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -30,7 +30,6 @@ export { } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; export * as dotenv from 'https://deno.land/std@0.198.0/dotenv/mod.ts'; -export { PolySqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/v2.0.0/mod.ts'; export { default as tldts } from 'npm:tldts@^6.0.14'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; From d45a1b500132c499293f7ca0df330932565e0d39 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 09:32:24 -0500 Subject: [PATCH 025/252] Upgrade Kysely to v0.27.3 --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 72675bb5..5174dc16 100644 --- a/deno.json +++ b/deno.json @@ -27,7 +27,7 @@ "@std/streams": "jsr:@std/streams@^0.223.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", - "kysely": "npm:kysely@^0.26.3", + "kysely": "npm:kysely@^0.27.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", From 621a6328930984fdccc539855ebb42ed66d50a6e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 09:36:36 -0500 Subject: [PATCH 026/252] Update kysely imports in migrations --- src/db/migrations/002_events_fts.ts | 3 ++- src/db/migrations/005_rework_tags.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/db/migrations/002_events_fts.ts b/src/db/migrations/002_events_fts.ts index 9324195c..ffaf5fbf 100644 --- a/src/db/migrations/002_events_fts.ts +++ b/src/db/migrations/002_events_fts.ts @@ -1,5 +1,6 @@ +import { Kysely, sql } from 'kysely'; + import { Conf } from '@/config.ts'; -import { Kysely, sql } from '@/deps.ts'; export async function up(db: Kysely): Promise { if (Conf.databaseUrl.protocol === 'sqlite:') { diff --git a/src/db/migrations/005_rework_tags.ts b/src/db/migrations/005_rework_tags.ts index f2746701..1f95810e 100644 --- a/src/db/migrations/005_rework_tags.ts +++ b/src/db/migrations/005_rework_tags.ts @@ -1,4 +1,4 @@ -import { Kysely, sql } from '@/deps.ts'; +import { Kysely, sql } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema From c190d2c8ce4034e7b720559edcbaf9641c378e18 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 14:56:47 -0500 Subject: [PATCH 027/252] Refactor Storages to get lazy-loaded only when they are used --- src/controllers/api/accounts.ts | 18 ++-- src/controllers/api/admin.ts | 6 +- src/controllers/api/blocks.ts | 4 +- src/controllers/api/bookmarks.ts | 4 +- src/controllers/api/ditto.ts | 6 +- src/controllers/api/instance.ts | 4 +- src/controllers/api/notifications.ts | 4 +- src/controllers/api/pleroma.ts | 4 +- src/controllers/api/search.ts | 10 +-- src/controllers/api/statuses.ts | 8 +- src/controllers/api/streaming.ts | 5 +- src/controllers/nostr/relay-info.ts | 4 +- src/controllers/nostr/relay.ts | 5 +- src/db/users.ts | 4 +- src/middleware/store.ts | 8 +- src/pipeline.ts | 26 +++--- src/pipeline/DVM.ts | 4 +- src/queries.ts | 18 ++-- src/stats.ts | 8 +- src/storages.ts | 118 ++++++++++++++++++--------- src/storages/adminStore.ts | 7 -- src/utils/api.ts | 6 +- src/utils/nip05.ts | 4 +- src/utils/outbox.ts | 4 +- src/views.ts | 16 ++-- src/views/mastodon/relationships.ts | 4 +- src/views/mastodon/statuses.ts | 6 +- 27 files changed, 175 insertions(+), 140 deletions(-) delete mode 100644 src/storages/adminStore.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index a3ac3786..96d4afa5 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -7,7 +7,7 @@ import { Conf } from '@/config.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; -import { eventsDB, searchStore } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts'; import { uploadFile } from '@/upload.ts'; import { nostrNow } from '@/utils.ts'; @@ -92,12 +92,12 @@ const accountSearchController: AppController = async (c) => { const [event, events] = await Promise.all([ lookupAccount(query), - searchStore.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), + Storages.search.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), ]); const results = await hydrateEvents({ events: event ? [event, ...events] : events, - storage: eventsDB, + storage: Storages.db, signal: c.req.raw.signal, }); @@ -143,7 +143,7 @@ const accountStatusesController: AppController = async (c) => { const { signal } = c.req.raw; if (pinned) { - const [pinEvent] = await eventsDB.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal }); + const [pinEvent] = await Storages.db.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal }); if (pinEvent) { const pinnedEventIds = getTagSet(pinEvent.tags, 'e'); return renderStatuses(c, [...pinnedEventIds].reverse()); @@ -164,8 +164,8 @@ const accountStatusesController: AppController = async (c) => { filter['#t'] = [tagged]; } - const events = await eventsDB.query([filter], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })) + const events = await Storages.db.query([filter], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })) .then((events) => { if (exclude_replies) { return events.filter((event) => !findReplyTag(event.tags)); @@ -306,7 +306,7 @@ const favouritesController: AppController = async (c) => { const params = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events7 = await eventsDB.query( + const events7 = await Storages.db.query( [{ kinds: [7], authors: [pubkey], ...params }], { signal }, ); @@ -315,8 +315,8 @@ const favouritesController: AppController = async (c) => { .map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1]) .filter((id): id is string => !!id); - const events1 = await eventsDB.query([{ kinds: [1], ids }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); const statuses = await Promise.all(events1.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); return paginated(c, events1, statuses); diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 990c0fc5..15420564 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { booleanParamSchema } from '@/schema.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; @@ -41,9 +41,9 @@ const adminAccountsController: AppController = async (c) => { const { since, until, limit } = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events = await eventsDB.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal }); + const events = await Storages.db.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal }); const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); - const authors = await eventsDB.query([{ kinds: [0], authors: pubkeys }], { signal }); + const authors = await Storages.db.query([{ kinds: [0], authors: pubkeys }], { signal }); for (const event of events) { const d = event.tags.find(([name]) => name === 'd')?.[1]; diff --git a/src/controllers/api/blocks.ts b/src/controllers/api/blocks.ts index d54773ac..16fa5cb1 100644 --- a/src/controllers/api/blocks.ts +++ b/src/controllers/api/blocks.ts @@ -1,5 +1,5 @@ import { type AppController } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { renderAccounts } from '@/views.ts'; @@ -8,7 +8,7 @@ const blocksController: AppController = async (c) => { const pubkey = c.get('pubkey')!; const { signal } = c.req.raw; - const [event10000] = await eventsDB.query( + const [event10000] = await Storages.db.query( [{ kinds: [10000], authors: [pubkey], limit: 1 }], { signal }, ); diff --git a/src/controllers/api/bookmarks.ts b/src/controllers/api/bookmarks.ts index 16e87e76..8d44f953 100644 --- a/src/controllers/api/bookmarks.ts +++ b/src/controllers/api/bookmarks.ts @@ -1,5 +1,5 @@ import { type AppController } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { renderStatuses } from '@/views.ts'; @@ -8,7 +8,7 @@ const bookmarksController: AppController = async (c) => { const pubkey = c.get('pubkey')!; const { signal } = c.req.raw; - const [event10003] = await eventsDB.query( + const [event10003] = await Storages.db.query( [{ kinds: [10003], authors: [pubkey], limit: 1 }], { signal }, ); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 425dcfba..e9485932 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; const relaySchema = z.object({ @@ -15,7 +15,7 @@ const relaySchema = z.object({ type RelayEntity = z.infer; export const adminRelaysController: AppController = async (c) => { - const [event] = await eventsDB.query([ + const [event] = await Storages.db.query([ { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, ]); @@ -36,7 +36,7 @@ export const adminSetRelaysController: AppController = async (c) => { created_at: Math.floor(Date.now() / 1000), }); - await eventsDB.event(event); + await Storages.db.event(event); return c.json(renderRelays(event)); }; diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 1355330f..d4239d31 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,13 +1,13 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; const instanceController: AppController = async (c) => { const { host, protocol } = Conf.url; const { signal } = c.req.raw; - const [event] = await eventsDB.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); + const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); const meta = jsonServerMetaSchema.parse(event?.content); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index 703e79f1..7820dd86 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -1,5 +1,5 @@ import { type AppController } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; @@ -8,7 +8,7 @@ const notificationsController: AppController = async (c) => { const { since, until } = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events = await eventsDB.query( + const events = await Storages.db.query( [{ kinds: [1], '#p': [pubkey], since, until }], { signal }, ); diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 64984d7c..51f48dc3 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -4,7 +4,7 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { createAdminEvent } from '@/utils/api.ts'; import { jsonSchema } from '@/schema.ts'; @@ -66,7 +66,7 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => { async function getConfigs(signal: AbortSignal): Promise { const { pubkey } = Conf; - const [event] = await eventsDB.query([{ + const [event] = await Storages.db.query([{ kinds: [30078], authors: [pubkey], '#d': ['pub.ditto.pleroma.config'], diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 63561c45..f595e22f 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; -import { searchStore } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { dedupeEvents } from '@/utils.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; @@ -91,8 +91,8 @@ function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: Abort filter.authors = [account_id]; } - return searchStore.query([filter], { signal }) - .then((events) => hydrateEvents({ events, storage: searchStore, signal })); + return Storages.search.query([filter], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.search, signal })); } /** Get event kinds to search from `type` query param. */ @@ -111,8 +111,8 @@ function typeToKinds(type: SearchQuery['type']): number[] { async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise { const filters = await getLookupFilters(query, signal); - return searchStore.query(filters, { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: searchStore, signal })) + return Storages.search.query(filters, { limit: 1, signal }) + .then((events) => hydrateEvents({ events, storage: Storages.search, signal })) .then(([event]) => event); } diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 2c94a719..42923365 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -15,7 +15,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { getLnurl } from '@/utils/lnurl.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { asyncReplaceAll } from '@/utils/text.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; const createStatusSchema = z.object({ @@ -137,7 +137,7 @@ const createStatusController: AppController = async (c) => { if (data.quote_id) { await hydrateEvents({ events: [event], - storage: eventsDB, + storage: Storages.db, signal: c.req.raw.signal, }); } @@ -242,7 +242,7 @@ const reblogStatusController: AppController = async (c) => { await hydrateEvents({ events: [reblogEvent], - storage: eventsDB, + storage: Storages.db, signal: signal, }); @@ -262,7 +262,7 @@ const unreblogStatusController: AppController = async (c) => { if (!event) return c.json({ error: 'Event not found.' }, 404); const filters: NostrFilter[] = [{ kinds: [6], authors: [pubkey], '#e': [event.id] }]; - const [repostedEvent] = await eventsDB.query(filters, { limit: 1 }); + const [repostedEvent] = await Storages.db.query(filters, { limit: 1 }); if (!repostedEvent) return c.json({ error: 'Event not found.' }, 404); await createEvent({ diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index c190c3e3..4bbc627b 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -10,7 +10,6 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { UserStore } from '@/storages/UserStore.ts'; -import { getAdminStore } from '@/storages/adminStore.ts'; const debug = Debug('ditto:streaming'); @@ -69,11 +68,11 @@ const streamingController: AppController = (c) => { const filter = await topicToFilter(stream, c.req.query(), pubkey); if (!filter) return; + const store = pubkey ? new UserStore(pubkey, Storages.admin) : Storages.admin; + try { for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { - const store = new UserStore(pubkey as string, getAdminStore()); - const [event] = await store.query([{ ids: [msg[2].id] }]); if (!event) continue; diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index 9d246448..7f1ddf7f 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,11 +1,11 @@ import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; const relayInfoController: AppController = async (c) => { const { signal } = c.req.raw; - const [event] = await eventsDB.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); + const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); const meta = jsonServerMetaSchema.parse(event?.content); return c.json({ diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 3db72c31..2fe8f921 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -1,6 +1,5 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; -import { eventsDB } from '@/storages.ts'; import * as pipeline from '@/pipeline.ts'; import { type ClientCLOSE, @@ -71,7 +70,7 @@ function connectStream(socket: WebSocket) { controllers.get(subId)?.abort(); controllers.set(subId, controller); - for (const event of await eventsDB.query(filters, { limit: FILTER_LIMIT })) { + for (const event of await Storages.db.query(filters, { limit: FILTER_LIMIT })) { send(['EVENT', subId, event]); } @@ -115,7 +114,7 @@ function connectStream(socket: WebSocket) { /** Handle COUNT. Return the number of events matching the filters. */ async function handleCount([_, subId, ...rest]: ClientCOUNT): Promise { - const { count } = await eventsDB.count(prepareFilters(rest)); + const { count } = await Storages.db.count(prepareFilters(rest)); send(['COUNT', subId, { count, approximate: false }]); } diff --git a/src/db/users.ts b/src/db/users.ts index 61c7341b..841e981f 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -3,7 +3,7 @@ import { Conf } from '@/config.ts'; import { Debug } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; const debug = Debug('ditto:users'); @@ -59,7 +59,7 @@ async function findUser(user: Partial, signal?: AbortSignal): Promise { const pubkey = c.get('pubkey'); - const adminStore = getAdminStore(); + if (pubkey) { - const store = new UserStore(pubkey, adminStore); + const store = new UserStore(pubkey, Storages.admin); c.set('store', store); } else { - c.set('store', adminStore); + c.set('store', Storages.admin); } await next(); }; diff --git a/src/pipeline.ts b/src/pipeline.ts index c4b94be6..5fa9f23f 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -12,7 +12,7 @@ import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; import { updateStats } from '@/stats.ts'; import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts'; -import { cache, eventsDB, reqmeister, Storages } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { eventAge, isRelay, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; import { fetchWorker } from '@/workers/fetch.ts'; @@ -70,15 +70,15 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - const [existing] = await cache.query([{ ids: [event.id], limit: 1 }]); - cache.event(event); - reqmeister.event(event, { signal }); + const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]); + Storages.cache.event(event); + Storages.reqmeister.event(event, { signal }); return !!existing; } /** Hydrate the event with the user, if applicable. */ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { - await hydrateEvents({ events: [event], storage: eventsDB, signal }); + await hydrateEvents({ events: [event], storage: Storages.db, signal }); const domain = await db .selectFrom('pubkey_domains') @@ -93,7 +93,7 @@ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { if (isEphemeralKind(event.kind)) return; - const [deletion] = await eventsDB.query( + const [deletion] = await Storages.db.query( [{ kinds: [5], authors: [Conf.pubkey, event.pubkey], '#e': [event.id], limit: 1 }], { signal }, ); @@ -102,7 +102,7 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise id); - await eventsDB.remove([{ ids: deleteIds }], { signal }); + await Storages.db.remove([{ ids: deleteIds }], { signal }); } } } @@ -202,14 +202,14 @@ function trackRelays(event: NostrEvent) { /** Queue related events to fetch. */ async function fetchRelatedEvents(event: DittoEvent, signal: AbortSignal) { if (!event.user) { - reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }).catch(() => {}); + Storages.reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }).catch(() => {}); } for (const [name, id, relay] of event.tags) { if (name === 'e') { - const { count } = await cache.count([{ ids: [id] }]); + const { count } = await Storages.cache.count([{ ids: [id] }]); if (!count) { - reqmeister.req({ ids: [id] }, { relays: [relay] }).catch(() => {}); + Storages.reqmeister.req({ ids: [id] }, { relays: [relay] }).catch(() => {}); } } } diff --git a/src/pipeline/DVM.ts b/src/pipeline/DVM.ts index 96e3c40d..953e9be0 100644 --- a/src/pipeline/DVM.ts +++ b/src/pipeline/DVM.ts @@ -3,7 +3,7 @@ import { NIP05, NostrEvent } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; export class DVM { static async event(event: NostrEvent): Promise { @@ -34,7 +34,7 @@ export class DVM { return DVM.feedback(event, 'error', `Forbidden user: ${user}`); } - const [label] = await eventsDB.query([{ + const [label] = await Storages.db.query([{ kinds: [1985], authors: [admin], '#L': ['nip05'], diff --git a/src/queries.ts b/src/queries.ts index 147e96c5..e4fdc21d 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -1,6 +1,6 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; -import { eventsDB, optimizer } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { Debug } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoRelation } from '@/interfaces/DittoFilter.ts'; @@ -31,8 +31,8 @@ const getEvent = async ( filter.kinds = [kind]; } - return await optimizer.query([filter], { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: optimizer, signal })) + return await Storages.optimizer.query([filter], { limit: 1, signal }) + .then((events) => hydrateEvents({ events, storage: Storages.optimizer, signal })) .then(([event]) => event); }; @@ -40,14 +40,14 @@ const getEvent = async ( const getAuthor = async (pubkey: string, opts: GetEventOpts = {}): Promise => { const { signal = AbortSignal.timeout(1000) } = opts; - return await optimizer.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: optimizer, signal })) + return await Storages.optimizer.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal }) + .then((events) => hydrateEvents({ events, storage: Storages.optimizer, signal })) .then(([event]) => event); }; /** Get users the given pubkey follows. */ const getFollows = async (pubkey: string, signal?: AbortSignal): Promise => { - const [event] = await eventsDB.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); + const [event] = await Storages.db.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); return event; }; @@ -83,15 +83,15 @@ async function getAncestors(event: NostrEvent, result: NostrEvent[] = []): Promi } async function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise { - const events = await eventsDB.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); - return hydrateEvents({ events, storage: eventsDB, signal }); + const events = await Storages.db.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); + return hydrateEvents({ events, storage: Storages.db, signal }); } /** Returns whether the pubkey is followed by a local user. */ async function isLocallyFollowed(pubkey: string): Promise { const { host } = Conf.url; - const [event] = await eventsDB.query( + const [event] = await Storages.db.query( [{ kinds: [3], '#p': [pubkey], search: `domain:${host}`, limit: 1 }], { limit: 1 }, ); diff --git a/src/stats.ts b/src/stats.ts index 6bfe568c..e75b57c7 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -4,7 +4,7 @@ import { InsertQueryBuilder } from 'kysely'; import { db } from '@/db.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Debug } from '@/deps.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; type AuthorStat = keyof Omit; @@ -65,7 +65,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr case 5: { if (!firstTaggedId) break; - const [repostedEvent] = await eventsDB.query( + const [repostedEvent] = await Storages.db.query( [{ kinds: [6], ids: [firstTaggedId], authors: [event.pubkey] }], { limit: 1 }, ); @@ -77,7 +77,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr const eventBeingRepostedPubkey = repostedEvent.tags.find(([name]) => name === 'p')?.[1]; if (!eventBeingRepostedId || !eventBeingRepostedPubkey) break; - const [eventBeingReposted] = await eventsDB.query( + const [eventBeingReposted] = await Storages.db.query( [{ kinds: [1], ids: [eventBeingRepostedId], authors: [eventBeingRepostedPubkey] }], { limit: 1 }, ); @@ -154,7 +154,7 @@ function eventStatsQuery(diffs: EventStatDiff[]) { /** Get the last version of the event, if any. */ async function maybeGetPrev(event: NostrEvent): Promise { - const [prev] = await eventsDB.query([ + const [prev] = await Storages.db.query([ { kinds: [event.kind], authors: [event.pubkey], limit: 1 }, ]); diff --git a/src/storages.ts b/src/storages.ts index 6c6a4a5e..21fe9d50 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -9,51 +9,95 @@ import { PoolStore } from '@/storages/pool-store.ts'; import { Reqmeister } from '@/storages/reqmeister.ts'; import { SearchStore } from '@/storages/search-store.ts'; import { InternalRelay } from '@/storages/InternalRelay.ts'; +import { UserStore } from '@/storages/UserStore.ts'; import { Time } from '@/utils/time.ts'; -/** Relay pool storage. */ -const client = new PoolStore({ - pool, - relays: activeRelays, - publisher: pipeline, -}); - -/** SQLite database to store events this Ditto server cares about. */ -const eventsDB = new EventsDB(db); - -/** In-memory data store for cached events. */ -const cache = new NCache({ max: 3000 }); - -/** Batches requests for single events. */ -const reqmeister = new Reqmeister({ - client, - delay: Time.seconds(1), - timeout: Time.seconds(1), -}); - -/** Main Ditto storage adapter */ -const optimizer = new Optimizer({ - db: eventsDB, - cache, - client: reqmeister, -}); - -/** Storage to use for remote search. */ -const searchStore = new SearchStore({ - relay: Conf.searchRelay, - fallback: optimizer, -}); - export class Storages { + private static _db: EventsDB | undefined; + private static _admin: UserStore | undefined; + private static _cache: NCache | undefined; + private static _client: PoolStore | undefined; + private static _optimizer: Optimizer | undefined; + private static _reqmeister: Reqmeister | undefined; private static _pubsub: InternalRelay | undefined; + private static _search: SearchStore | undefined; - static get pubsub(): InternalRelay { + /** SQLite database to store events this Ditto server cares about. */ + public static get db(): EventsDB { + if (!this._db) { + this._db = new EventsDB(db); + } + return this._db; + } + + /** Admin user storage. */ + public static get admin(): UserStore { + if (!this._admin) { + this._admin = new UserStore(Conf.pubkey, this.db); + } + return this._admin; + } + + /** Internal pubsub relay between controllers and the pipeline. */ + public static get pubsub(): InternalRelay { if (!this._pubsub) { this._pubsub = new InternalRelay(); } - return this._pubsub; } -} -export { cache, client, eventsDB, optimizer, reqmeister, searchStore }; + /** Relay pool storage. */ + public static get client(): PoolStore { + if (!this._client) { + this._client = new PoolStore({ + pool, + relays: activeRelays, + publisher: pipeline, + }); + } + return this._client; + } + + /** In-memory data store for cached events. */ + public static get cache(): NCache { + if (!this._cache) { + this._cache = new NCache({ max: 3000 }); + } + return this._cache; + } + + /** Batches requests for single events. */ + public static get reqmeister(): Reqmeister { + if (!this._reqmeister) { + this._reqmeister = new Reqmeister({ + client: this.client, + delay: Time.seconds(1), + timeout: Time.seconds(1), + }); + } + return this._reqmeister; + } + + /** Main Ditto storage adapter */ + public static get optimizer(): Optimizer { + if (!this._optimizer) { + this._optimizer = new Optimizer({ + db: this.db, + cache: this.cache, + client: this.reqmeister, + }); + } + return this._optimizer; + } + + /** Storage to use for remote search. */ + public static get search(): SearchStore { + if (!this._search) { + this._search = new SearchStore({ + relay: Conf.searchRelay, + fallback: this.optimizer, + }); + } + return this._search; + } +} diff --git a/src/storages/adminStore.ts b/src/storages/adminStore.ts deleted file mode 100644 index 0b5bb384..00000000 --- a/src/storages/adminStore.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { UserStore } from '@/storages/UserStore.ts'; -import { Conf } from '@/config.ts'; -import { eventsDB } from '@/storages.ts'; - -export function getAdminStore() { - return new UserStore(Conf.pubkey, eventsDB); -} diff --git a/src/utils/api.ts b/src/utils/api.ts index 3f89d3f8..7b090ee6 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -9,7 +9,7 @@ import { Debug, parseFormData, type TypeFest } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; -import { client, eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; const debug = Debug('ditto:api'); @@ -43,7 +43,7 @@ async function updateEvent( fn: (prev: NostrEvent | undefined) => E, c: AppContext, ): Promise { - const [prev] = await eventsDB.query([filter], { limit: 1, signal: c.req.raw.signal }); + const [prev] = await Storages.db.query([filter], { limit: 1, signal: c.req.raw.signal }); return createEvent(fn(prev), c); } @@ -80,7 +80,7 @@ async function publishEvent(event: NostrEvent, c: AppContext): Promise( ); async function localNip05Lookup(name: string): Promise { - const [label] = await eventsDB.query([{ + const [label] = await Storages.db.query([{ kinds: [1985], authors: [Conf.pubkey], '#L': ['nip05'], diff --git a/src/utils/outbox.ts b/src/utils/outbox.ts index 8189fe25..13edaf69 100644 --- a/src/utils/outbox.ts +++ b/src/utils/outbox.ts @@ -1,10 +1,10 @@ import { Conf } from '@/config.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; export async function getRelays(pubkey: string): Promise> { const relays = new Set<`wss://${string}`>(); - const events = await eventsDB.query([ + const events = await Storages.db.query([ { kinds: [10002], authors: [pubkey, Conf.pubkey], limit: 2 }, ]); diff --git a/src/views.ts b/src/views.ts index 94bb8df3..9c31dfcb 100644 --- a/src/views.ts +++ b/src/views.ts @@ -1,6 +1,6 @@ import { NostrFilter } from '@nostrify/nostrify'; import { AppContext } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; @@ -12,15 +12,15 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal return c.json([]); } - const events = await eventsDB.query(filters, { signal }); + const events = await Storages.db.query(filters, { signal }); const pubkeys = new Set(events.map(({ pubkey }) => pubkey)); if (!pubkeys.size) { return c.json([]); } - const authors = await eventsDB.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const authors = await Storages.db.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); const accounts = await Promise.all( authors.map((event) => renderAccount(event)), @@ -32,8 +32,8 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal async function renderAccounts(c: AppContext, authors: string[], signal = AbortSignal.timeout(1000)) { const { since, until, limit } = paginationSchema.parse(c.req.query()); - const events = await eventsDB.query([{ kinds: [0], authors, since, until, limit }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const events = await Storages.db.query([{ kinds: [0], authors, since, until, limit }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); const accounts = await Promise.all( events.map((event) => renderAccount(event)), @@ -50,8 +50,8 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal const { limit } = paginationSchema.parse(c.req.query()); - const events = await eventsDB.query([{ kinds: [1], ids, limit }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const events = await Storages.db.query([{ kinds: [1], ids, limit }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); if (!events.length) { return c.json([]); diff --git a/src/views/mastodon/relationships.ts b/src/views/mastodon/relationships.ts index 983b1342..4cfbfc28 100644 --- a/src/views/mastodon/relationships.ts +++ b/src/views/mastodon/relationships.ts @@ -1,8 +1,8 @@ -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { hasTag } from '@/tags.ts'; async function renderRelationship(sourcePubkey: string, targetPubkey: string) { - const events = await eventsDB.query([ + const events = await Storages.db.query([ { kinds: [3], authors: [sourcePubkey], limit: 1 }, { kinds: [3], authors: [targetPubkey], limit: 1 }, { kinds: [10000], authors: [sourcePubkey], limit: 1 }, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 06ef164a..f63c5010 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -6,7 +6,7 @@ import { Conf } from '@/config.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; -import { eventsDB, optimizer } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; import { nostrDate } from '@/utils.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; @@ -40,7 +40,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { ), ]; - const mentionedProfiles = await optimizer.query( + const mentionedProfiles = await Storages.optimizer.query( [{ authors: mentionedPubkeys, limit: mentionedPubkeys.length }], ); @@ -53,7 +53,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { ), firstUrl ? unfurlCardCached(firstUrl) : null, viewerPubkey - ? await eventsDB.query([ + ? await Storages.db.query([ { kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, { kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, { kinds: [9734], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, From 08f0ef3cdd091921ad9ade45d571659291303b5e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 15:23:54 -0500 Subject: [PATCH 028/252] Add a deno.lock again --- deno.json | 1 - deno.lock | 1505 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1505 insertions(+), 1 deletion(-) create mode 100644 deno.lock diff --git a/deno.json b/deno.json index 5174dc16..3eae6324 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,5 @@ { "$schema": "https://deno.land/x/deno@v1.41.0/cli/schemas/config-file.v1.json", - "lock": false, "tasks": { "start": "deno run -A src/server.ts", "dev": "deno run -A --watch src/server.ts", diff --git a/deno.lock b/deno.lock new file mode 100644 index 00000000..f984b36f --- /dev/null +++ b/deno.lock @@ -0,0 +1,1505 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@nostrify/nostrify@^0.15.0": "jsr:@nostrify/nostrify@0.15.0", + "jsr:@soapbox/kysely-deno-sqlite@^2.0.2": "jsr:@soapbox/kysely-deno-sqlite@2.0.2", + "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", + "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", + "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.0", + "jsr:@std/media-types@^0.224.0": "jsr:@std/media-types@0.224.0", + "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/bip39@^1.3.0": "npm:@scure/bip39@1.3.0", + "npm:@types/lodash@4.14.194": "npm:@types/lodash@4.14.194", + "npm:@types/mime@3.0.0": "npm:@types/mime@3.0.0", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:@types/node-forge@^1.3.1": "npm:@types/node-forge@1.3.11", + "npm:@types/sanitize-html@2.9.0": "npm:@types/sanitize-html@2.9.0", + "npm:comlink@^4.4.1": "npm:comlink@4.4.1", + "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:ipfs-only-hash@^4.0.0": "npm:ipfs-only-hash@4.0.0", + "npm:iso-639-1@2.1.15": "npm:iso-639-1@2.1.15", + "npm:kysely@^0.27.2": "npm:kysely@0.27.3", + "npm:kysely@^0.27.3": "npm:kysely@0.27.3", + "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:lru-cache@^10.2.0": "npm:lru-cache@10.2.0", + "npm:mime@^3.0.0": "npm:mime@3.0.0", + "npm:node-forge@^1.3.1": "npm:node-forge@1.3.1", + "npm:nostr-relaypool2@0.6.34": "npm:nostr-relaypool2@0.6.34", + "npm:nostr-tools@^1.14.0": "npm:nostr-tools@1.17.0", + "npm:nostr-tools@^2.5.0": "npm:nostr-tools@2.5.1", + "npm:nostr-tools@^2.5.1": "npm:nostr-tools@2.5.1", + "npm:nostr-wasm@^0.1.0": "npm:nostr-wasm@0.1.0", + "npm:sanitize-html@^2.11.0": "npm:sanitize-html@2.13.0", + "npm:tldts@^6.0.14": "npm:tldts@6.1.18", + "npm:tseep@^1.1.3": "npm:tseep@1.2.1", + "npm:type-fest@^4.3.0": "npm:type-fest@4.15.0", + "npm:unfurl.js@^6.4.0": "npm:unfurl.js@6.4.0", + "npm:uuid62@^1.0.2": "npm:uuid62@1.0.2", + "npm:websocket-ts@^2.1.5": "npm:websocket-ts@2.1.5", + "npm:zod@^3.21.0": "npm:zod@3.23.4", + "npm:zod@^3.23.4": "npm:zod@3.23.4" + }, + "jsr": { + "@nostrify/nostrify@0.15.0": { + "integrity": "51c2fe9ac7264d22567cd1919a5bf5101a5207f651e65bc00b3de43f9038dfc8", + "dependencies": [ + "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.4" + ] + }, + "@soapbox/kysely-deno-sqlite@2.0.2": { + "integrity": "296f1d6c258b3fa2e8ad51f59782fce0e92549d4cb34ba159a582bcebf35d5e9", + "dependencies": [ + "npm:kysely@^0.27.2" + ] + }, + "@std/assert@0.224.0": { + "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" + }, + "@std/crypto@0.224.0": { + "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", + "dependencies": [ + "jsr:@std/assert@^0.224.0", + "jsr:@std/encoding@^0.224.0" + ] + }, + "@std/encoding@0.224.0": { + "integrity": "efb6dca97d3e9c31392bd5c8cfd9f9fc9decf5a1f4d1f78af7900a493bcf89b5" + }, + "@std/media-types@0.224.0": { + "integrity": "5ac87989393f8cb1c81bee02aef6f5d4c8289b416deabc04f9ad25dff292d0b0" + } + }, + "npm": { + "@assemblyscript/loader@0.9.4": { + "integrity": "sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA==", + "dependencies": {} + }, + "@babel/code-frame@7.24.2": { + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "@babel/highlight@7.24.2", + "picocolors": "picocolors@1.0.0" + } + }, + "@babel/helper-validator-identifier@7.22.20": { + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dependencies": {} + }, + "@babel/highlight@7.24.2": { + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "@babel/helper-validator-identifier@7.22.20", + "chalk": "chalk@2.4.2", + "js-tokens": "js-tokens@4.0.0", + "picocolors": "picocolors@1.0.0" + } + }, + "@isaacs/ttlcache@1.4.1": { + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "dependencies": {} + }, + "@multiformats/base-x@4.0.1": { + "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==", + "dependencies": {} + }, + "@noble/ciphers@0.2.0": { + "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", + "dependencies": {} + }, + "@noble/ciphers@0.5.2": { + "integrity": "sha512-GADtQmZCdgbnNp+daPLc3OY3ibEtGGDV/+CzeM3MFnhiQ7ELQKlsHWYq0YbYUXx4jU3/Y1erAxU6r+hwpewqmQ==", + "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/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/secp256k1@2.1.0": { + "integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==", + "dependencies": {} + }, + "@protobufjs/aspromise@1.1.2": { + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dependencies": {} + }, + "@protobufjs/base64@1.1.2": { + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dependencies": {} + }, + "@protobufjs/codegen@2.0.4": { + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dependencies": {} + }, + "@protobufjs/eventemitter@1.1.0": { + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dependencies": {} + }, + "@protobufjs/fetch@1.1.0": { + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "@protobufjs/aspromise@1.1.2", + "@protobufjs/inquire": "@protobufjs/inquire@1.1.0" + } + }, + "@protobufjs/float@1.0.2": { + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dependencies": {} + }, + "@protobufjs/inquire@1.1.0": { + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dependencies": {} + }, + "@protobufjs/path@1.1.2": { + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dependencies": {} + }, + "@protobufjs/pool@1.1.0": { + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dependencies": {} + }, + "@protobufjs/utf8@1.1.0": { + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "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/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/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/lodash@4.14.194": { + "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==", + "dependencies": {} + }, + "@types/long@4.0.2": { + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "dependencies": {} + }, + "@types/mime@3.0.0": { + "integrity": "sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w==", + "dependencies": {} + }, + "@types/minimist@1.2.5": { + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dependencies": {} + }, + "@types/node-forge@1.3.11": { + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/node@20.12.7": { + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dependencies": { + "undici-types": "undici-types@5.26.5" + } + }, + "@types/normalize-package-data@2.4.4": { + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dependencies": {} + }, + "@types/sanitize-html@2.9.0": { + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dependencies": { + "htmlparser2": "htmlparser2@8.0.2" + } + }, + "ansi-styles@3.2.1": { + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "color-convert@1.9.3" + } + }, + "arrify@1.0.1": { + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dependencies": {} + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bl@5.1.0": { + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dependencies": { + "buffer": "buffer@6.0.3", + "inherits": "inherits@2.0.4", + "readable-stream": "readable-stream@3.6.2" + } + }, + "blakejs@1.2.1": { + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "dependencies": {} + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "camelcase-keys@6.2.2": { + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dependencies": { + "camelcase": "camelcase@5.3.1", + "map-obj": "map-obj@4.3.0", + "quick-lru": "quick-lru@4.0.1" + } + }, + "camelcase@5.3.1": { + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dependencies": {} + }, + "chalk@2.4.2": { + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "ansi-styles@3.2.1", + "escape-string-regexp": "escape-string-regexp@1.0.5", + "supports-color": "supports-color@5.5.0" + } + }, + "cids@1.1.9": { + "integrity": "sha512-l11hWRfugIcbGuTZwAM5PwpjPPjyb6UZOGwlHSnOBV5o07XhQ4gNpBN67FbODvpjyHtd+0Xs6KNvUcGBiDRsdg==", + "dependencies": { + "multibase": "multibase@4.0.6", + "multicodec": "multicodec@3.2.1", + "multihashes": "multihashes@4.0.3", + "uint8arrays": "uint8arrays@3.1.1" + } + }, + "color-convert@1.9.3": { + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "color-name@1.1.3" + } + }, + "color-name@1.1.3": { + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dependencies": {} + }, + "comlink@4.4.1": { + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==", + "dependencies": {} + }, + "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" + } + }, + "decamelize-keys@1.1.1": { + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dependencies": { + "decamelize": "decamelize@1.2.0", + "map-obj": "map-obj@1.0.1" + } + }, + "decamelize@1.2.0": { + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dependencies": {} + }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dependencies": {} + }, + "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" + } + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dependencies": {} + }, + "err-code@3.0.1": { + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "dependencies": {} + }, + "error-ex@1.3.2": { + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "is-arrayish@0.2.1" + } + }, + "escape-string-regexp@1.0.5": { + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dependencies": {} + }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "find-up@4.1.0": { + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "locate-path@5.0.0", + "path-exists": "path-exists@4.0.0" + } + }, + "formdata-helper@0.3.0": { + "integrity": "sha512-QkRUFbNgWSu9lkc5TKLWri0ilTFowo950w13I5pRhj4cUxzMLuz0MIhGbE/gIRyfsZQoFeMNN0h06OCSOgfhUg==", + "dependencies": {} + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dependencies": {} + }, + "hamt-sharding@2.0.1": { + "integrity": "sha512-vnjrmdXG9dDs1m/H4iJ6z0JFI2NtgsW5keRkTcM85NGak69Mkf5PHUqBz+Xs0T4sg0ppvj9O5EGAJo40FTxmmA==", + "dependencies": { + "sparse-array": "sparse-array@1.3.2", + "uint8arrays": "uint8arrays@3.1.1" + } + }, + "hard-rejection@2.1.0": { + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dependencies": {} + }, + "has-flag@3.0.0": { + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dependencies": {} + }, + "hasown@2.0.2": { + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "function-bind@1.1.2" + } + }, + "he@1.2.0": { + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dependencies": {} + }, + "hosted-git-info@2.8.9": { + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dependencies": {} + }, + "hosted-git-info@4.1.0": { + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dependencies": { + "lru-cache": "lru-cache@6.0.0" + } + }, + "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" + } + }, + "iconv-lite@0.4.24": { + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": "safer-buffer@2.1.2" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "indent-string@4.0.0": { + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dependencies": {} + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dependencies": {} + }, + "interface-ipld-format@1.0.1": { + "integrity": "sha512-WV/ar+KQJVoQpqRDYdo7YPGYIUHJxCuOEhdvsRpzLqoOIVCqPKdMMYmsLL1nCRsF3yYNio+PAJbCKiv6drrEAg==", + "dependencies": { + "cids": "cids@1.1.9", + "multicodec": "multicodec@3.2.1", + "multihashes": "multihashes@4.0.3" + } + }, + "ipfs-only-hash@4.0.0": { + "integrity": "sha512-TE1DZCvfw8i3gcsTq3P4TFx3cKFJ3sluu/J3XINkJhIN9OwJgNMqKA+WnKx6ByCb1IoPXsTp1KM7tupElb6SyA==", + "dependencies": { + "ipfs-unixfs-importer": "ipfs-unixfs-importer@7.0.3", + "meow": "meow@9.0.0" + } + }, + "ipfs-unixfs-importer@7.0.3": { + "integrity": "sha512-qeFOlD3AQtGzr90sr5Tq1Bi8pT5Nr2tSI8z310m7R4JDYgZc6J1PEZO3XZQ8l1kuGoqlAppBZuOYmPEqaHcVQQ==", + "dependencies": { + "bl": "bl@5.1.0", + "cids": "cids@1.1.9", + "err-code": "err-code@3.0.1", + "hamt-sharding": "hamt-sharding@2.0.1", + "ipfs-unixfs": "ipfs-unixfs@4.0.3", + "ipld-dag-pb": "ipld-dag-pb@0.22.3", + "it-all": "it-all@1.0.6", + "it-batch": "it-batch@1.0.9", + "it-first": "it-first@1.0.7", + "it-parallel-batch": "it-parallel-batch@1.0.11", + "merge-options": "merge-options@3.0.4", + "multihashing-async": "multihashing-async@2.1.4", + "rabin-wasm": "rabin-wasm@0.1.5", + "uint8arrays": "uint8arrays@2.1.10" + } + }, + "ipfs-unixfs@4.0.3": { + "integrity": "sha512-hzJ3X4vlKT8FQ3Xc4M1szaFVjsc1ZydN+E4VQ91aXxfpjFn9G2wsMo1EFdAXNq/BUnN5dgqIOMP5zRYr3DTsAw==", + "dependencies": { + "err-code": "err-code@3.0.1", + "protobufjs": "protobufjs@6.11.4" + } + }, + "ipld-dag-pb@0.22.3": { + "integrity": "sha512-dfG5C5OVAR4FEP7Al2CrHWvAyIM7UhAQrjnOYOIxXGQz5NlEj6wGX0XQf6Ru6or1na6upvV3NQfstapQG8X2rg==", + "dependencies": { + "cids": "cids@1.1.9", + "interface-ipld-format": "interface-ipld-format@1.0.1", + "multicodec": "multicodec@3.2.1", + "multihashing-async": "multihashing-async@2.1.4", + "protobufjs": "protobufjs@6.11.4", + "stable": "stable@0.1.8", + "uint8arrays": "uint8arrays@2.1.10" + } + }, + "is-arrayish@0.2.1": { + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dependencies": {} + }, + "is-core-module@2.13.1": { + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "hasown@2.0.2" + } + }, + "is-plain-obj@1.1.0": { + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dependencies": {} + }, + "is-plain-obj@2.1.0": { + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dependencies": {} + }, + "is-plain-object@5.0.0": { + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dependencies": {} + }, + "iso-639-1@2.1.15": { + "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==", + "dependencies": {} + }, + "isomorphic-ws@5.0.0_ws@8.16.0": { + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dependencies": { + "ws": "ws@8.16.0" + } + }, + "it-all@1.0.6": { + "integrity": "sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A==", + "dependencies": {} + }, + "it-batch@1.0.9": { + "integrity": "sha512-7Q7HXewMhNFltTsAMdSz6luNhyhkhEtGGbYek/8Xb/GiqYMtwUmopE1ocPSiJKKp3rM4Dt045sNFoUu+KZGNyA==", + "dependencies": {} + }, + "it-first@1.0.7": { + "integrity": "sha512-nvJKZoBpZD/6Rtde6FXqwDqDZGF1sCADmr2Zoc0hZsIvnE449gRFnGctxDf09Bzc/FWnHXAdaHVIetY6lrE0/g==", + "dependencies": {} + }, + "it-parallel-batch@1.0.11": { + "integrity": "sha512-UWsWHv/kqBpMRmyZJzlmZeoAMA0F3SZr08FBdbhtbe+MtoEBgr/ZUAKrnenhXCBrsopy76QjRH2K/V8kNdupbQ==", + "dependencies": { + "it-batch": "it-batch@1.0.9" + } + }, + "js-sha3@0.8.0": { + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dependencies": {} + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dependencies": {} + }, + "json-parse-even-better-errors@2.3.1": { + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dependencies": {} + }, + "kind-of@6.0.3": { + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dependencies": {} + }, + "kysely@0.27.3": { + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", + "dependencies": {} + }, + "lines-and-columns@1.2.4": { + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "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": {} + }, + "locate-path@5.0.0": { + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "p-locate@4.1.0" + } + }, + "long@4.0.0": { + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dependencies": {} + }, + "lru-cache@10.2.0": { + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dependencies": {} + }, + "lru-cache@6.0.0": { + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "yallist@4.0.0" + } + }, + "map-obj@1.0.1": { + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dependencies": {} + }, + "map-obj@4.3.0": { + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dependencies": {} + }, + "meow@9.0.0": { + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dependencies": { + "@types/minimist": "@types/minimist@1.2.5", + "camelcase-keys": "camelcase-keys@6.2.2", + "decamelize": "decamelize@1.2.0", + "decamelize-keys": "decamelize-keys@1.1.1", + "hard-rejection": "hard-rejection@2.1.0", + "minimist-options": "minimist-options@4.1.0", + "normalize-package-data": "normalize-package-data@3.0.3", + "read-pkg-up": "read-pkg-up@7.0.1", + "redent": "redent@3.0.0", + "trim-newlines": "trim-newlines@3.0.1", + "type-fest": "type-fest@0.18.1", + "yargs-parser": "yargs-parser@20.2.9" + } + }, + "merge-options@3.0.4": { + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "dependencies": { + "is-plain-obj": "is-plain-obj@2.1.0" + } + }, + "mime@3.0.0": { + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dependencies": {} + }, + "min-indent@1.0.1": { + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dependencies": {} + }, + "minimist-options@4.1.0": { + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dependencies": { + "arrify": "arrify@1.0.1", + "is-plain-obj": "is-plain-obj@1.1.0", + "kind-of": "kind-of@6.0.3" + } + }, + "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": {} + }, + "multibase@4.0.6": { + "integrity": "sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ==", + "dependencies": { + "@multiformats/base-x": "@multiformats/base-x@4.0.1" + } + }, + "multicodec@3.2.1": { + "integrity": "sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw==", + "dependencies": { + "uint8arrays": "uint8arrays@3.1.1", + "varint": "varint@6.0.0" + } + }, + "multiformats@9.9.0": { + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "dependencies": {} + }, + "multihashes@4.0.3": { + "integrity": "sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA==", + "dependencies": { + "multibase": "multibase@4.0.6", + "uint8arrays": "uint8arrays@3.1.1", + "varint": "varint@5.0.2" + } + }, + "multihashing-async@2.1.4": { + "integrity": "sha512-sB1MiQXPSBTNRVSJc2zM157PXgDtud2nMFUEIvBrsq5Wv96sUclMRK/ecjoP1T/W61UJBqt4tCTwMkUpt2Gbzg==", + "dependencies": { + "blakejs": "blakejs@1.2.1", + "err-code": "err-code@3.0.1", + "js-sha3": "js-sha3@0.8.0", + "multihashes": "multihashes@4.0.3", + "murmurhash3js-revisited": "murmurhash3js-revisited@3.0.0", + "uint8arrays": "uint8arrays@3.1.1" + } + }, + "murmurhash3js-revisited@3.0.0": { + "integrity": "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==", + "dependencies": {} + }, + "nanoid@3.3.7": { + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-forge@1.3.1": { + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dependencies": {} + }, + "normalize-package-data@2.5.0": { + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "hosted-git-info@2.8.9", + "resolve": "resolve@1.22.8", + "semver": "semver@5.7.2", + "validate-npm-package-license": "validate-npm-package-license@3.0.4" + } + }, + "normalize-package-data@3.0.3": { + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dependencies": { + "hosted-git-info": "hosted-git-info@4.1.0", + "is-core-module": "is-core-module@2.13.1", + "semver": "semver@7.6.0", + "validate-npm-package-license": "validate-npm-package-license@3.0.4" + } + }, + "nostr-relaypool2@0.6.34": { + "integrity": "sha512-e3FDh9w/wQkY513mvoJps1Hc/Y5wiWXeBM6MD+YKSyAg+px+/8uHSSHAuHhlavw7oOEOvEsIGlMDMc57DG3MOA==", + "dependencies": { + "isomorphic-ws": "isomorphic-ws@5.0.0_ws@8.16.0", + "nostr-tools": "nostr-tools@1.17.0", + "safe-stable-stringify": "safe-stable-stringify@2.4.3" + } + }, + "nostr-tools@1.17.0": { + "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", + "dependencies": { + "@noble/ciphers": "@noble/ciphers@0.2.0", + "@noble/curves": "@noble/curves@1.1.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-tools@2.5.1": { + "integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==", + "dependencies": { + "@noble/ciphers": "@noble/ciphers@0.5.2", + "@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": {} + }, + "p-limit@2.3.0": { + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "p-try@2.2.0" + } + }, + "p-locate@4.1.0": { + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "p-limit@2.3.0" + } + }, + "p-try@2.2.0": { + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dependencies": {} + }, + "parse-json@5.2.0": { + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "@babel/code-frame@7.24.2", + "error-ex": "error-ex@1.3.2", + "json-parse-even-better-errors": "json-parse-even-better-errors@2.3.1", + "lines-and-columns": "lines-and-columns@1.2.4" + } + }, + "parse-srcset@1.0.2": { + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "dependencies": {} + }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dependencies": {} + }, + "path-parse@1.0.7": { + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dependencies": {} + }, + "picocolors@1.0.0": { + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dependencies": {} + }, + "postcss@8.4.38": { + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dependencies": { + "nanoid": "nanoid@3.3.7", + "picocolors": "picocolors@1.0.0", + "source-map-js": "source-map-js@1.2.0" + } + }, + "protobufjs@6.11.4": { + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "dependencies": { + "@protobufjs/aspromise": "@protobufjs/aspromise@1.1.2", + "@protobufjs/base64": "@protobufjs/base64@1.1.2", + "@protobufjs/codegen": "@protobufjs/codegen@2.0.4", + "@protobufjs/eventemitter": "@protobufjs/eventemitter@1.1.0", + "@protobufjs/fetch": "@protobufjs/fetch@1.1.0", + "@protobufjs/float": "@protobufjs/float@1.0.2", + "@protobufjs/inquire": "@protobufjs/inquire@1.1.0", + "@protobufjs/path": "@protobufjs/path@1.1.2", + "@protobufjs/pool": "@protobufjs/pool@1.1.0", + "@protobufjs/utf8": "@protobufjs/utf8@1.1.0", + "@types/long": "@types/long@4.0.2", + "@types/node": "@types/node@20.12.7", + "long": "long@4.0.0" + } + }, + "quick-lru@4.0.1": { + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dependencies": {} + }, + "rabin-wasm@0.1.5": { + "integrity": "sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA==", + "dependencies": { + "@assemblyscript/loader": "@assemblyscript/loader@0.9.4", + "bl": "bl@5.1.0", + "debug": "debug@4.3.4", + "minimist": "minimist@1.2.8", + "node-fetch": "node-fetch@2.7.0", + "readable-stream": "readable-stream@3.6.2" + } + }, + "read-pkg-up@7.0.1": { + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dependencies": { + "find-up": "find-up@4.1.0", + "read-pkg": "read-pkg@5.2.0", + "type-fest": "type-fest@0.8.1" + } + }, + "read-pkg@5.2.0": { + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dependencies": { + "@types/normalize-package-data": "@types/normalize-package-data@2.4.4", + "normalize-package-data": "normalize-package-data@2.5.0", + "parse-json": "parse-json@5.2.0", + "type-fest": "type-fest@0.6.0" + } + }, + "readable-stream@3.6.2": { + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "inherits@2.0.4", + "string_decoder": "string_decoder@1.3.0", + "util-deprecate": "util-deprecate@1.0.2" + } + }, + "redent@3.0.0": { + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dependencies": { + "indent-string": "indent-string@4.0.0", + "strip-indent": "strip-indent@3.0.0" + } + }, + "resolve@1.22.8": { + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "is-core-module@2.13.1", + "path-parse": "path-parse@1.0.7", + "supports-preserve-symlinks-flag": "supports-preserve-symlinks-flag@1.0.0" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "safe-stable-stringify@2.4.3": { + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dependencies": {} + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dependencies": {} + }, + "sanitize-html@2.13.0": { + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "dependencies": { + "deepmerge": "deepmerge@4.3.1", + "escape-string-regexp": "escape-string-regexp@4.0.0", + "htmlparser2": "htmlparser2@8.0.2", + "is-plain-object": "is-plain-object@5.0.0", + "parse-srcset": "parse-srcset@1.0.2", + "postcss": "postcss@8.4.38" + } + }, + "semver@5.7.2": { + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dependencies": {} + }, + "semver@7.6.0": { + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "lru-cache@6.0.0" + } + }, + "source-map-js@1.2.0": { + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dependencies": {} + }, + "sparse-array@1.3.2": { + "integrity": "sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==", + "dependencies": {} + }, + "spdx-correct@3.2.0": { + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "spdx-expression-parse@3.0.1", + "spdx-license-ids": "spdx-license-ids@3.0.17" + } + }, + "spdx-exceptions@2.5.0": { + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dependencies": {} + }, + "spdx-expression-parse@3.0.1": { + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "spdx-exceptions@2.5.0", + "spdx-license-ids": "spdx-license-ids@3.0.17" + } + }, + "spdx-license-ids@3.0.17": { + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "dependencies": {} + }, + "stable@0.1.8": { + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dependencies": {} + }, + "string_decoder@1.3.0": { + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "strip-indent@3.0.0": { + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dependencies": { + "min-indent": "min-indent@1.0.1" + } + }, + "supports-color@5.5.0": { + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "has-flag@3.0.0" + } + }, + "supports-preserve-symlinks-flag@1.0.0": { + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dependencies": {} + }, + "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" + } + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "trim-newlines@3.0.1": { + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dependencies": {} + }, + "tseep@1.2.1": { + "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==", + "dependencies": {} + }, + "type-fest@0.18.1": { + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dependencies": {} + }, + "type-fest@0.6.0": { + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dependencies": {} + }, + "type-fest@0.8.1": { + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dependencies": {} + }, + "type-fest@4.15.0": { + "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==", + "dependencies": {} + }, + "uint8arrays@2.1.10": { + "integrity": "sha512-Q9/hhJa2836nQfEJSZTmr+pg9+cDJS9XEAp7N2Vg5MzL3bK/mkMVfjscRGYruP9jNda6MAdf4QD/y78gSzkp6A==", + "dependencies": { + "multiformats": "multiformats@9.9.0" + } + }, + "uint8arrays@3.1.1": { + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "dependencies": { + "multiformats": "multiformats@9.9.0" + } + }, + "undici-types@5.26.5": { + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "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" + } + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dependencies": {} + }, + "uuid62@1.0.2": { + "integrity": "sha512-vI7jxJboVd6eFRpyZn5ONx5DAQgu7hO0TcE6Qy+riw/XSw8A8+qc3SplJPZ9+nKqlAuN7RMriSn2ehMWeIPCiA==", + "dependencies": { + "base-x": "base-x@3.0.9", + "buffer": "buffer@6.0.3", + "uuid": "uuid@8.3.2" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "validate-npm-package-license@3.0.4": { + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "spdx-correct@3.2.0", + "spdx-expression-parse": "spdx-expression-parse@3.0.1" + } + }, + "varint@5.0.2": { + "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==", + "dependencies": {} + }, + "varint@6.0.0": { + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dependencies": {} + }, + "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" + } + }, + "ws@8.16.0": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": {} + }, + "yallist@4.0.0": { + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dependencies": {} + }, + "yargs-parser@20.2.9": { + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dependencies": {} + }, + "zod@3.23.4": { + "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", + "dependencies": {} + } + } + }, + "redirects": { + "https://esm.sh/v135/@types/lodash@4.17.0/index": "https://esm.sh/v135/@types/lodash@4.17.0/index~.d.ts", + "https://esm.sh/v135/@types/lodash@~4.17/index.d.ts": "https://esm.sh/v135/@types/lodash@4.17.0/index.d.ts" + }, + "remote": { + "https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", + "https://deno.land/std@0.160.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", + "https://deno.land/std@0.160.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", + "https://deno.land/std@0.160.0/async/debounce.ts": "dc8b92d4a4fe7eac32c924f2b8d3e62112530db70cadce27042689d82970b350", + "https://deno.land/std@0.160.0/async/deferred.ts": "d8fb253ffde2a056e4889ef7e90f3928f28be9f9294b6505773d33f136aab4e6", + "https://deno.land/std@0.160.0/async/delay.ts": "0419dfc993752849692d1f9647edf13407c7facc3509b099381be99ffbc9d699", + "https://deno.land/std@0.160.0/async/mod.ts": "dd0a8ed4f3984ffabe2fcca7c9f466b7932d57b1864ffee148a5d5388316db6b", + "https://deno.land/std@0.160.0/async/mux_async_iterator.ts": "3447b28a2a582224a3d4d3596bccbba6e85040da3b97ed64012f7decce98d093", + "https://deno.land/std@0.160.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239", + "https://deno.land/std@0.160.0/async/tee.ts": "9af3a3e7612af75861308b52249e167f5ebc3dcfc8a1a4d45462d96606ee2b70", + "https://deno.land/std@0.160.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4", + "https://deno.land/std@0.160.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a", + "https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179", + "https://deno.land/std@0.160.0/crypto/_fnv/fnv32.ts": "aa9bddead8c6345087d3abd4ef35fb9655622afc333fc41fff382b36e64280b5", + "https://deno.land/std@0.160.0/crypto/_fnv/fnv64.ts": "625d7e7505b6cb2e9801b5fd6ed0a89256bac12b2bbb3e4664b85a88b0ec5bef", + "https://deno.land/std@0.160.0/crypto/_fnv/index.ts": "a8f6a361b4c6d54e5e89c16098f99b6962a1dd6ad1307dbc97fa1ecac5d7060a", + "https://deno.land/std@0.160.0/crypto/_fnv/util.ts": "4848313bed7f00f55be3cb080aa0583fc007812ba965b03e4009665bde614ce3", + "https://deno.land/std@0.160.0/crypto/_wasm_crypto/lib/deno_std_wasm_crypto.generated.mjs": "258b484c2da27578bec61c01d4b62c21f72268d928d03c968c4eb590cb3bd830", + "https://deno.land/std@0.160.0/crypto/_wasm_crypto/mod.ts": "6c60d332716147ded0eece0861780678d51b560f533b27db2e15c64a4ef83665", + "https://deno.land/std@0.160.0/crypto/keystack.ts": "e481eed28007395e554a435e880fee83a5c73b9259ed8a135a75e4b1e4f381f7", + "https://deno.land/std@0.160.0/crypto/mod.ts": "fadedc013b4a86fda6305f1adc6d1c02225834d53cff5d95cc05f62b25127517", + "https://deno.land/std@0.160.0/crypto/timing_safe_equal.ts": "82a29b737bc8932d75d7a20c404136089d5d23629e94ba14efa98a8cc066c73e", + "https://deno.land/std@0.160.0/datetime/formatter.ts": "7c8e6d16a0950f400aef41b9f1eb9168249869776ec520265dfda785d746589e", + "https://deno.land/std@0.160.0/datetime/mod.ts": "ea927ca96dfb28c7b9a5eed5bdc7ac46bb9db38038c4922631895cea342fea87", + "https://deno.land/std@0.160.0/datetime/tokenizer.ts": "7381e28f6ab51cb504c7e132be31773d73ef2f3e1e50a812736962b9df1e8c47", + "https://deno.land/std@0.160.0/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2", + "https://deno.land/std@0.160.0/encoding/base64url.ts": "a5f82a9fa703bd85a5eb8e7c1296bc6529e601ebd9642cc2b5eaa6b38fa9e05a", + "https://deno.land/std@0.160.0/encoding/hex.ts": "4cc5324417cbb4ac9b828453d35aed45b9cc29506fad658f1f138d981ae33795", + "https://deno.land/std@0.160.0/fmt/colors.ts": "9e36a716611dcd2e4865adea9c4bec916b5c60caad4cdcdc630d4974e6bb8bd4", + "https://deno.land/std@0.160.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289", + "https://deno.land/std@0.160.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.160.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.160.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", + "https://deno.land/std@0.160.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.160.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.160.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac", + "https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24", + "https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d", + "https://deno.land/std@0.160.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", + "https://deno.land/std@0.160.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", + "https://deno.land/std@0.160.0/testing/asserts.ts": "1e340c589853e82e0807629ba31a43c84ebdcdeca910c4a9705715dfdb0f5ce8", + "https://deno.land/std@0.176.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.176.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.176.0/encoding/hex.ts": "50f8c95b52eae24395d3dfcb5ec1ced37c5fe7610ef6fffdcc8b0fdc38e3b32f", + "https://deno.land/std@0.176.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471", + "https://deno.land/std@0.176.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.176.0/fs/copy.ts": "14214efd94fc3aa6db1e4af2b4b9578e50f7362b7f3725d5a14ad259a5df26c8", + "https://deno.land/std@0.176.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", + "https://deno.land/std@0.176.0/fs/ensure_dir.ts": "724209875497a6b4628dfb256116e5651c4f7816741368d6c44aab2531a1e603", + "https://deno.land/std@0.176.0/fs/ensure_file.ts": "c38602670bfaf259d86ca824a94e6cb9e5eb73757fefa4ebf43a90dd017d53d9", + "https://deno.land/std@0.176.0/fs/ensure_link.ts": "c0f5b2f0ec094ed52b9128eccb1ee23362a617457aa0f699b145d4883f5b2fb4", + "https://deno.land/std@0.176.0/fs/ensure_symlink.ts": "2955cc8332aeca9bdfefd05d8d3976b94e282b0f353392a71684808ed2ffdd41", + "https://deno.land/std@0.176.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", + "https://deno.land/std@0.176.0/fs/exists.ts": "b8c8a457b71e9d7f29b9d2f87aad8dba2739cbe637e8926d6ba6e92567875f8e", + "https://deno.land/std@0.176.0/fs/expand_glob.ts": "45d17e89796a24bd6002e4354eda67b4301bb8ba67d2cac8453cdabccf1d9ab0", + "https://deno.land/std@0.176.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", + "https://deno.land/std@0.176.0/fs/move.ts": "4cb47f880e3f0582c55e71c9f8b1e5e8cfaacb5e84f7390781dd563b7298ec19", + "https://deno.land/std@0.176.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", + "https://deno.land/std@0.176.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.176.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.176.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.176.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.176.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.176.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", + "https://deno.land/std@0.176.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.176.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.176.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.179.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.179.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.179.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.179.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.179.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.179.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.179.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.179.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", + "https://deno.land/std@0.179.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.179.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.179.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.190.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.190.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.190.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab", + "https://deno.land/std@0.190.0/streams/readable_stream_from_iterable.ts": "cd4bb9e9bf6dbe84c213beb1f5085c326624421671473e410cfaecad15f01865", + "https://deno.land/std@0.198.0/_util/diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.198.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.198.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.198.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.198.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.198.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.198.0/assert/assert_equals.ts": "a0ee60574e437bcab2dcb79af9d48dc88845f8fd559468d9c21b15fd638ef943", + "https://deno.land/std@0.198.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.198.0/assert/assert_false.ts": "a9962749f4bf5844e3fa494257f1de73d69e4fe0e82c34d0099287552163a2dc", + "https://deno.land/std@0.198.0/assert/assert_instance_of.ts": "09fd297352a5b5bbb16da2b5e1a0d8c6c44da5447772648622dcc7df7af1ddb8", + "https://deno.land/std@0.198.0/assert/assert_is_error.ts": "b4eae4e5d182272efc172bf28e2e30b86bb1650cd88aea059e5d2586d4160fb9", + "https://deno.land/std@0.198.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.198.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.198.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.198.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.198.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.198.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.198.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.198.0/assert/assert_strict_equals.ts": "5cf29b38b3f8dece95287325723272aa04e04dbf158d886d662fa594fddc9ed3", + "https://deno.land/std@0.198.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.198.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.198.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.198.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.198.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.198.0/assert/mod.ts": "08d55a652c22c5da0215054b21085cec25a5da47ce4a6f9de7d9ad36df35bdee", + "https://deno.land/std@0.198.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.198.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.198.0/collections/filter_values.ts": "16e1fc456a7969e770ec5b89edf5ac97b295ca534b47c1a83f061b409aad7814", + "https://deno.land/std@0.198.0/collections/without_all.ts": "1e3cccb1ed0659455b473c0766d9414b7710d8cef48862c899f445178f66b779", + "https://deno.land/std@0.198.0/dotenv/mod.ts": "ff7acf1c97ba57af512ecb6f9094fa96e1f63cca1960a7687616fa86bab7e356", + "https://deno.land/std@0.198.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc", + "https://deno.land/x/deno_cron@v1.0.0/cron.ts": "7f984d0c4c7ac4fb1ad3cd241d457e7808a9362735d910abb02dc689883ee3ef", + "https://deno.land/x/hono@v3.10.1/adapter/deno/serve-static.ts": "ba10cf6aaf39da942b0d49c3b9877ddba69d41d414c6551d890beb1085f58eea", + "https://deno.land/x/hono@v3.10.1/client/client.ts": "ff340f58041203879972dd368b011ed130c66914f789826610869a90603406bf", + "https://deno.land/x/hono@v3.10.1/client/index.ts": "3ff4cf246f3543f827a85a2c84d66a025ac350ee927613629bda47e854bfb7ba", + "https://deno.land/x/hono@v3.10.1/client/utils.ts": "053273c002963b549d38268a1b423ac8ca211a8028bdab1ed0b781a62aa5e661", + "https://deno.land/x/hono@v3.10.1/compose.ts": "e8ab4b345aa367f2dd65f221c9fe829dd885326a613f4215b654f93a4066bb5c", + "https://deno.land/x/hono@v3.10.1/context.ts": "261cc8b8b1e8f04b98beab1cca6692f317b7dc6d2b75b4f84c982e54cf1db730", + "https://deno.land/x/hono@v3.10.1/helper/cookie/index.ts": "55ccd20bbd8d9a8bb2ecd998e90845c1d306c19027f54b3d1b89a5be35968b80", + "https://deno.land/x/hono@v3.10.1/helper/html/index.ts": "aba19e8d29f217c7fffa5719cf606c4e259b540d51296e82bbea3c992e2ecbc6", + "https://deno.land/x/hono@v3.10.1/hono-base.ts": "cc55e0a4c63a7bdf44df3e804ea4737d5399eeb6606b45d102f8e48c3ff1e925", + "https://deno.land/x/hono@v3.10.1/hono.ts": "2cc4c292e541463a4d6f83edbcea58048d203e9564ae62ec430a3d466b49a865", + "https://deno.land/x/hono@v3.10.1/http-exception.ts": "6071df078b5f76d279684d52fe82a590f447a64ffe1b75eb5064d0c8a8d2d676", + "https://deno.land/x/hono@v3.10.1/jsx/index.ts": "019512d3a9b3897b879e87fa5fb179cd34f3d326f8ff8b93379c2bb707ec168a", + "https://deno.land/x/hono@v3.10.1/jsx/streaming.ts": "5d03b4d02eaa396c8f0f33c3f6e8c7ed3afb7598283c2d4a7ddea0ada8c212a7", + "https://deno.land/x/hono@v3.10.1/middleware.ts": "57b2047c4b9d775a052a9c44a3b805802c1d1cb477ab9c4bb6185d27382d1b96", + "https://deno.land/x/hono@v3.10.1/middleware/basic-auth/index.ts": "5505288ccf9364f56f7be2dfac841543b72e20656e54ac646a1a73a0aa853261", + "https://deno.land/x/hono@v3.10.1/middleware/bearer-auth/index.ts": "d11fe14e0a3006f6d35c391e455fe20d8ece9561e48b6a5580e4b87dd491cd90", + "https://deno.land/x/hono@v3.10.1/middleware/cache/index.ts": "9e5d31d33206bb5dba46dde16ed606dd2cb361d75c26b02e02c72bd1fb6fe53e", + "https://deno.land/x/hono@v3.10.1/middleware/compress/index.ts": "85d315c9a942d7758e5c524dc94b736124646a56752e56c6e4284f3989b4692a", + "https://deno.land/x/hono@v3.10.1/middleware/cors/index.ts": "d481eba7e05d3448cd326d3dca8b9c7e16ecf0d27a37fd7d700485834123ae5e", + "https://deno.land/x/hono@v3.10.1/middleware/etag/index.ts": "4ad675e108dc98dccca0e9e35cd903701669a1aea676b8b51266c3b602e4d54c", + "https://deno.land/x/hono@v3.10.1/middleware/jsx-renderer/index.ts": "5352d6dda872d419ebafbd4d6b408f66ad473fc3d395d82327850c1e786d7344", + "https://deno.land/x/hono@v3.10.1/middleware/jwt/index.ts": "c6e02a94a3911299d21392b3b1f8710bda7cacf0d60db59c0e2f0d9fa8ff1a70", + "https://deno.land/x/hono@v3.10.1/middleware/logger/index.ts": "c139f372f482baeffbad68b14bef990e011fe8df578dcee71fb612ffad7fe748", + "https://deno.land/x/hono@v3.10.1/middleware/powered-by/index.ts": "c36b7a3d1322c6a37f3d1510f7ff04a85aa6cacfac2173e5f1913eb16c3cc869", + "https://deno.land/x/hono@v3.10.1/middleware/pretty-json/index.ts": "f6967ceecdb42c95ddd5e2e7bc8545d3e8bda111fa659f3f1336b2e6fe6b0bb0", + "https://deno.land/x/hono@v3.10.1/middleware/secure-headers/index.ts": "d2b8a7978e3d201ead5ac8fd22e3adc9094189aebcba0d9cd51b98773927a5d5", + "https://deno.land/x/hono@v3.10.1/middleware/timing/index.ts": "d6976a07d9d51a7b26dae1311fe51d0744f7d234498bac3fe024ec7088c0ca47", + "https://deno.land/x/hono@v3.10.1/mod.ts": "90114a97be9111b348129ad0143e764a64921f60dd03b8f3da529db98a0d3a82", + "https://deno.land/x/hono@v3.10.1/request.ts": "52330303dd7a3bf4f580fde0463ba608bc4c88a8b7b5edd7c1327064c7cf65ce", + "https://deno.land/x/hono@v3.10.1/router.ts": "39d573f48baee429810cd583c931dd44274273c30804d538c86967d310ea4ab5", + "https://deno.land/x/hono@v3.10.1/router/linear-router/index.ts": "8a2a7144c50b1f4a92d9ee99c2c396716af144c868e10608255f969695efccd0", + "https://deno.land/x/hono@v3.10.1/router/linear-router/router.ts": "bc63e8b5bc1dabc815306d50bebd1bb5877ffa3936ba2ad7550d093c95ee6bd1", + "https://deno.land/x/hono@v3.10.1/router/pattern-router/index.ts": "304a66c50e243872037ed41c7dd79ed0c89d815e17e172e7ad7cdc4bc07d3383", + "https://deno.land/x/hono@v3.10.1/router/pattern-router/router.ts": "a9a5a2a182cce8c3ae82139892cc0502be7dd8f579f31e76d0302b19b338e548", + "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db", + "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/node.ts": "5b3fb80411db04c65df066e69fedb2c8c0844753c2633d703336de84d569252c", + "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/router.ts": "fbe8917aa24fe25d0208bfa82ce7f49ba0507f9ae158d4d0c177f6a061b0a561", + "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/trie.ts": "852ce7207e6701e47fa30889a0d2b8bfcd56d8862c97e7bc9831e0a64bd8835f", + "https://deno.land/x/hono@v3.10.1/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef", + "https://deno.land/x/hono@v3.10.1/router/smart-router/router.ts": "71979c06b32b093960a6e8efc4c185e558f280bff18846b8b1cdc757ade6ff99", + "https://deno.land/x/hono@v3.10.1/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41", + "https://deno.land/x/hono@v3.10.1/router/trie-router/node.ts": "3af15fa9c9994a8664a2b7a7c11233504b5bb9d4fcf7bb34cf30d7199052c39f", + "https://deno.land/x/hono@v3.10.1/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d", + "https://deno.land/x/hono@v3.10.1/utils/body.ts": "7a16a6656331a96bcae57642f8d5e3912bd361cbbcc2c0d2157ecc3f218f7a92", + "https://deno.land/x/hono@v3.10.1/utils/buffer.ts": "9066a973e64498cb262c7e932f47eed525a51677b17f90893862b7279dc0773e", + "https://deno.land/x/hono@v3.10.1/utils/cookie.ts": "19920ba6756944aae1ad8585c3ddeaa9df479733f59d05359db096f7361e5e4b", + "https://deno.land/x/hono@v3.10.1/utils/crypto.ts": "bda0e141bbe46d3a4a20f8fbcb6380d473b617123d9fdfa93e4499410b537acc", + "https://deno.land/x/hono@v3.10.1/utils/encode.ts": "3b7c7d736123b5073542b34321700d4dbf5ff129c138f434bb2144a4d425ee89", + "https://deno.land/x/hono@v3.10.1/utils/filepath.ts": "18461b055a914d6da85077f453051b516281bb17cf64fa74bf5ef604dc9d2861", + "https://deno.land/x/hono@v3.10.1/utils/html.ts": "01c1520a4256f899da1954357cf63ae11c348eda141a505f72d7090cf5481aba", + "https://deno.land/x/hono@v3.10.1/utils/jwt/index.ts": "5e4b82a42eb3603351dfce726cd781ca41cb57437395409d227131aec348d2d5", + "https://deno.land/x/hono@v3.10.1/utils/jwt/jwt.ts": "02ff7bbf1298ffcc7a40266842f8eac44b6c136453e32d4441e24d0cbfba3a95", + "https://deno.land/x/hono@v3.10.1/utils/jwt/types.ts": "58ddf908f76ba18d9c62ddfc2d1e40cc2e306bf987409a6169287efa81ce2546", + "https://deno.land/x/hono@v3.10.1/utils/mime.ts": "0105d2b5e8e91f07acc70f5d06b388313995d62af23c802fcfba251f5a744d95", + "https://deno.land/x/hono@v3.10.1/utils/stream.ts": "1789dcc73c5b0ede28f83d7d34e47ae432c20e680907cb3275a9c9187f293983", + "https://deno.land/x/hono@v3.10.1/utils/url.ts": "5fc3307ef3cb2e6f34ec2a03e3d7f2126c6a9f5f0eab677222df3f0e40bd7567", + "https://deno.land/x/hono@v3.10.1/validator/index.ts": "6c986e8b91dcf857ecc8164a506ae8eea8665792a4ff7215471df669c632ae7c", + "https://deno.land/x/hono@v3.10.1/validator/validator.ts": "afa5e52495e0996fbba61996736fab5c486590d72d376f809e9f9ff4e0c463e9", + "https://deno.land/x/kysely_deno_postgres@v0.4.0/deps.ts": "7970f66a52a9fa0cef607cb7ef0171212af2ccb83e73ecfa7629aabc28a38793", + "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts": "662438fd3909984bb8cbaf3fd44d2121e949d11301baf21d6c3f057ccf9887de", + "https://deno.land/x/kysely_deno_postgres@v0.4.0/src/PostgreSQLDriver.ts": "590c2fa248cff38e6e0f623050983039b5fde61e9c7131593d2922fb1f0eb921", + "https://deno.land/x/kysely_deno_postgres@v0.4.0/src/PostgreSQLDriverDatabaseConnection.ts": "83cd176ca830407dbff8495140cba870d1a34b27075c91ef1d5dbf7bbe467c40", + "https://deno.land/x/pentagon@v0.1.4/deps.ts": "486904dff4ea0275059d7fa42ad7da6025eeced0c7885befa3354da3b9522dda", + "https://deno.land/x/pentagon@v0.1.4/mod.ts": "2a3226e25d9142c24cb93bc1c88b459c06d7b031643c4fb1969fe88d4f6b63b0", + "https://deno.land/x/pentagon@v0.1.4/src/batchOperations.ts": "0c9f410623e7f881d6cc4ba28e51e650f07120f8b86c53f2532d06efdc466a87", + "https://deno.land/x/pentagon@v0.1.4/src/crud.ts": "c9e924fe5a2dd1f37c3c3716c255c5f3d591da2a1927509e4275e74708fa1c6b", + "https://deno.land/x/pentagon@v0.1.4/src/errors.ts": "2d311e95b68a42b5a53528b6171acea1a3ba27e52d7cacdab0e5c54304d97394", + "https://deno.land/x/pentagon@v0.1.4/src/keys.ts": "82bc85bbca3425e8bdb5b19dc48997dc154c173b1462f9aafd9630ab639d1762", + "https://deno.land/x/pentagon@v0.1.4/src/pentagon.ts": "d603e704c7299a207373ddb2a528718c0adfa5f4c3a2ffdbd34ed7d11f44f96d", + "https://deno.land/x/pentagon@v0.1.4/src/relation.ts": "ed6da1fb9e521c09ef9fe29170ce78682bcc788bf41f0e11d031983aa1279f49", + "https://deno.land/x/pentagon@v0.1.4/src/search.ts": "ec1bb39df1e8bd551bcd22978cbd78466522f0b1a5b0002a38f5293a25ac272f", + "https://deno.land/x/pentagon@v0.1.4/src/types.ts": "f2a16e11eb9a724627a9b4532bc95f91a27a43d3d4aade14a947fcdc8cab671a", + "https://deno.land/x/pentagon@v0.1.4/src/util.ts": "a601821f1ee32209a1dd4c094ef541a6990755424f31fc490352e340b19798e9", + "https://deno.land/x/plug@1.0.1/deps.ts": "35ea2acd5e3e11846817a429b7ef4bec47b80f2d988f5d63797147134cbd35c2", + "https://deno.land/x/plug@1.0.1/download.ts": "8d6a023ade0806a0653b48cd5f6f8b15fcfaa1dbf2aa1f4bc90fc5732d27b144", + "https://deno.land/x/plug@1.0.1/mod.ts": "5dec80ee7a3a325be45c03439558531bce7707ac118f4376cebbd6740ff24bfb", + "https://deno.land/x/plug@1.0.1/types.ts": "d8eb738fc6ed883e6abf77093442c2f0b71af9090f15c7613621d4039e410ee1", + "https://deno.land/x/plug@1.0.1/util.ts": "5ba8127b9adc36e070b9e22971fb8106869eea1741f452a87b4861e574f13481", + "https://deno.land/x/postgres@v0.17.0/client.ts": "348779c9f6a1c75ef1336db662faf08dce7d2101ff72f0d1e341ba1505c8431d", + "https://deno.land/x/postgres@v0.17.0/client/error.ts": "0817583b666fd546664ed52c1d37beccc5a9eebcc6e3c2ead20ada99b681e5f7", + "https://deno.land/x/postgres@v0.17.0/connection/auth.ts": "1070125e2ac4ca4ade36d69a4222d37001903092826d313217987583edd61ce9", + "https://deno.land/x/postgres@v0.17.0/connection/connection.ts": "428ed3efa055870db505092b5d3545ef743497b7b4b72cf8f0593e7dd4788acd", + "https://deno.land/x/postgres@v0.17.0/connection/connection_params.ts": "52bfe90e8860f584b95b1b08c254dde97c3aa763c4b6bee0c80c5930e35459e0", + "https://deno.land/x/postgres@v0.17.0/connection/message.ts": "f9257948b7f87d58bfbfe3fc6e2e08f0de3ef885655904d56a5f73655cc22c5a", + "https://deno.land/x/postgres@v0.17.0/connection/message_code.ts": "466719008b298770c366c5c63f6cf8285b7f76514dadb4b11e7d9756a8a1ddbf", + "https://deno.land/x/postgres@v0.17.0/connection/packet.ts": "050aeff1fc13c9349e89451a155ffcd0b1343dc313a51f84439e3e45f64b56c8", + "https://deno.land/x/postgres@v0.17.0/connection/scram.ts": "0c7a2551fe7b1a1c62dd856b7714731a7e7534ccca10093336782d1bfc5b2bd2", + "https://deno.land/x/postgres@v0.17.0/deps.ts": "f47ccb41f7f97eaad455d94f407ef97146ae99443dbe782894422c869fbba69e", + "https://deno.land/x/postgres@v0.17.0/mod.ts": "a1e18fd9e6fedc8bc24e5aeec3ae6de45e2274be1411fb66e9081420c5e81d7d", + "https://deno.land/x/postgres@v0.17.0/pool.ts": "892db7b5e1787988babecc994a151ebbd7d017f080905cbe9c3d7b44a73032a9", + "https://deno.land/x/postgres@v0.17.0/query/array_parser.ts": "f8a229d82c3801de8266fa2cc4afe12e94fef8d0c479e73655c86ed3667ef33f", + "https://deno.land/x/postgres@v0.17.0/query/decode.ts": "44a4a6cbcf494ed91a4fecae38a57dce63a7b519166f02c702791d9717371419", + "https://deno.land/x/postgres@v0.17.0/query/decoders.ts": "16cb0e60227d86692931e315421b15768c78526e3aeb84e25fcc4111096de9fd", + "https://deno.land/x/postgres@v0.17.0/query/encode.ts": "5f1418a2932b7c2231556e4a5f5f56efef48728014070cfafe7656963f342933", + "https://deno.land/x/postgres@v0.17.0/query/oid.ts": "8c33e1325f34e4ca9f11a48b8066c8cfcace5f64bc1eb17ad7247af4936999e1", + "https://deno.land/x/postgres@v0.17.0/query/query.ts": "edb473cbcfeff2ee1c631272afb25d079d06b66b5853f42492725b03ffa742b6", + "https://deno.land/x/postgres@v0.17.0/query/transaction.ts": "8e75c3ce0aca97da7fe126e68f8e6c08d640e5c8d2016e62cee5c254bebe7fe8", + "https://deno.land/x/postgres@v0.17.0/query/types.ts": "a6dc8024867fe7ccb0ba4b4fa403ee5d474c7742174128c8e689c3b5e5eaa933", + "https://deno.land/x/postgres@v0.17.0/utils/deferred.ts": "dd94f2a57355355c47812b061a51b55263f72d24e9cb3fdb474c7519f4d61083", + "https://deno.land/x/postgres@v0.17.0/utils/utils.ts": "19c3527ddd5c6c4c49ae36397120274c7f41f9d3cbf479cb36065d23329e9f90", + "https://deno.land/x/s3_lite_client@0.6.1/client.ts": "d4c93fe2dbd19d0c570c8661e1971051a4e3a5f74c30122fc1ed5ee44cadaac4", + "https://deno.land/x/s3_lite_client@0.6.1/deps.ts": "cfa4510116af915b090db6789035b89fbd34fd8a6ff6b1389650401a1d794962", + "https://deno.land/x/s3_lite_client@0.6.1/errors.ts": "3dd431b0e96f346104d7be6c09e1659b5c360992e6487e35bacb881f10c5a5bf", + "https://deno.land/x/s3_lite_client@0.6.1/helpers.ts": "6ba450312f54873805390cc7a11e61a7886dc00633f2ed20d941606568527332", + "https://deno.land/x/s3_lite_client@0.6.1/mod.ts": "4a896cad948ae36e35a5025eff92a97366059fe8e01bb109df3889666c88bd1d", + "https://deno.land/x/s3_lite_client@0.6.1/object-uploader.ts": "b4bad0d771d79b2bb23b8cab0e6f7be85a2390e18957c612fd5cda11c39f55b0", + "https://deno.land/x/s3_lite_client@0.6.1/signing.ts": "2ba77aac07a7c94267e83d285bbd33fdb3253dfa32b035df62479d6b224bb748", + "https://deno.land/x/s3_lite_client@0.6.1/transform-chunk-sizes.ts": "cecc1167ba366d086a13c754be6ed86717d6b0b27c779c4c766621435a697045", + "https://deno.land/x/s3_lite_client@0.6.1/xml-parser.ts": "de925493369718cab6f26413fbbada18eec74aa6eaf0598d77c7296f5fdfd8a9", + "https://deno.land/x/scoped_performance@v2.0.0/mod.ts": "c874aa244e9b2c585759d716b86735bd78fbd82e0e0b94df0a3f5856bbcacb73", + "https://deno.land/x/scoped_performance@v2.0.0/src/scoped-performance.ts": "c0194251ff4a758bf9af29edef64d00926b14e8e51f6a279a83e005428c21eb3", + "https://deno.land/x/sentry@7.112.2/index.mjs": "04382d5c2f4e233ba389611db46f77943b2a7f6efbeaaf31193f6e586f4366ef", + "https://deno.land/x/sqlite3@0.9.1/deno.json": "50895b0bb0c13ae38b93413d7f9f62652f6e7076cd99b9876f6b3b7f6c488dca", + "https://deno.land/x/sqlite3@0.9.1/deps.ts": "f6035f0884a730c0d55b0cdce68846f13bbfc14e8afbf0b3cd4f12a52b4107b7", + "https://deno.land/x/sqlite3@0.9.1/mod.ts": "d41b8b30e1b20b537ef4d78cae98d90f6bd65c727b64aa1a18bffbb28f7d6ec3", + "https://deno.land/x/sqlite3@0.9.1/src/blob.ts": "3681353b3c97bc43f9b02f8d1c3269c0dc4eb9cb5d3af16c7ce4d1e1ec7507c4", + "https://deno.land/x/sqlite3@0.9.1/src/constants.ts": "85fd27aa6e199093f25f5f437052e16fd0e0870b96ca9b24a98e04ddc8b7d006", + "https://deno.land/x/sqlite3@0.9.1/src/database.ts": "c326446463955f276dcbe18547ede4b19ea3085bef0980548c0a58d830b3b5d9", + "https://deno.land/x/sqlite3@0.9.1/src/ffi.ts": "b83f6d16179be7a97a298d6e8172941dbf532058e7c2b3df3a708beefe285c90", + "https://deno.land/x/sqlite3@0.9.1/src/statement.ts": "4773bc8699a9084b93e65126cd5f9219c248de1fce447270bdae2c3630637150", + "https://deno.land/x/sqlite3@0.9.1/src/util.ts": "3892904eb057271d4072215c3e7ffe57a9e59e4df78ac575046eb278ca6239cd", + "https://deno.land/x/zod@v3.21.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea", + "https://deno.land/x/zod@v3.21.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", + "https://deno.land/x/zod@v3.21.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", + "https://deno.land/x/zod@v3.21.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", + "https://deno.land/x/zod@v3.21.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", + "https://deno.land/x/zod@v3.21.4/helpers/parseUtil.ts": "51a76c126ee212be86013d53a9d07f87e9ae04bb1496f2558e61b62cb74a6aa8", + "https://deno.land/x/zod@v3.21.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", + "https://deno.land/x/zod@v3.21.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", + "https://deno.land/x/zod@v3.21.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774", + "https://deno.land/x/zod@v3.21.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", + "https://deno.land/x/zod@v3.21.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", + "https://deno.land/x/zod@v3.21.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", + "https://deno.land/x/zod@v3.21.4/types.ts": "b5d061babea250de14fc63764df5b3afa24f2b088a1d797fc060ba49a0ddff28", + "https://esm.sh/kysely@0.17.1/dist/esm/index-nodeless.js": "9c23bfd307118e3ccd3a9f0ec1261fc3451fb5301aa34aa6f28e05156818755a", + "https://esm.sh/lodash@4.17.21": "cf3544d5159a7648b25ad21fcf8dbf08a1fbfb1415b70b4163da646ef83eec4a", + "https://esm.sh/v135/kysely@0.17.1/denonext/dist/esm/index-nodeless.js": "6f73bbf2d73bc7e96cdabf941c4ae8c12f58fd7b441031edec44c029aed9532b", + "https://esm.sh/v135/lodash@4.17.21/denonext/lodash.mjs": "f04a5db09228738fd8cd06b6d1eaf3463b1b639d1529cf11673c3ac7bda1b1a8", + "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts": "3f74ab08cf97d4a3e6994cb79422e9b0069495e017416858121d5ff8ae04ac2a", + "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/mod.ts": "5f505cd265aefbcb687cde6f98c79344d3292ee1dd978e85e5ffa84a617c6682", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/mod.ts": "de0b11782a0c461ccd2722a1a67e5495186438f09be36f5d849ef13698c1725b", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/generate.ts": "222496a4617271d86d5d8b9de88471b82637c2787f4f8df2a19b44e71d5db63e", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/http-signing-cavage.ts": "86c0b286bf492246b0e934909fc8d3ecbb95f69a0df02efcdece306bac4f576c", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/httpsigjs/parser.ts": "cd8b233265f23aa8921243b33a069cc49e29a047522d861efd3b21868287c219", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/httpsigjs/utils.ts": "c60195568ee8ac807a7f58aad1bad090bc20456650c0ce6c0c48e6ec6891bbe9", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/pem.ts": "88d621412c124390d4badfda24af0b82a73ad3431391883904c810847c60b6eb", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/sign.ts": "d68479a738b7f41c2988f8f0222c1f1b9289656e0d7f0731da2f78e0f96fa71f", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/utils.ts": "434ed995db2f9ed99a25b711fc87fab5372b3cfd0ced984034b5994288a718dc", + "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/verify.ts": "26ce6ea17a50814239381a71eab844e0dbc165143af8db2a5c31d29552dbcb89", + "https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/event.ts": "15e748e4561c7530437359748db56edfb5c239e8c226258440d721ffc6570b74", + "https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts": "7dd491c62fb1c4a5af089278b0a938700dc03b58fd32f1ce57e73903120caded", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts": "fabd0c320882413cdbcc3477809df842857285ae7bc3677d47b03c1e35f451e6", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/async-socket.ts": "e925b05ba1dff46bd89f680acbc0323ddb819ad01549a83498a6bdca18743a3e", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/backoff.ts": "26e6c5f4433f90a11339d9039d3230eb1cd30448b5726874fe7c13301519166f", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/deps.ts": "1f6f7cfdce91a8db807e2f22c4c4d8b3e821db3cd3b11f4bee9be7186ccc190c", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/machina.ts": "8fcda3f9c8d786ef7aab6e13790a61416fe0ea417533d7a65177b970e59dd9e8", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/nice-relay.ts": "ced298b02357d401e557234fbc29ae81c434499bdaaefe159288912b0c901c9c", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/pubsub.ts": "a36060eed7a5f1b356ee617464426f0e8c99fa352c7c52b6967026b5aa30f077", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/reanimate.ts": "9df3833a254b7c677707fdba85773a753d74c22e3528b8ef9ac6e21b13c5888f", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/relay.ts": "0b040f3336a1427d01c7726736a5d02c314eaaae96273871971703e2c1ef5558", + "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/schema.ts": "b2a38e800443dcc297e64bb30b870a06c2c026bbccf4d215c4e8158a07392119", + "https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts": "77aa427debd9b796bab1cc37e46ff7e9d81ce8eef24dbe370f33254d87a74cd5", + "https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/colors.json": "0ebfe52aa82aaaaebbe2991500959bee57d8aaa8819c9841f968d4dbac58bcc0", + "https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/debug.ts": "282bdde3f10431dbfb7660d00f02a630f7e4dba0da03dc86bf661991cb8d5e53", + "https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/mod.ts": "c2d24f7c2973f7876b55c351ee8971b80e2884508334414ae4bde657eaee4ded", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/build/sqlite.js": "423a53b12ad3e068a4f02e6dba2cb64ee761afe281d61d80a997ed15f6715232", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/build/vfs.js": "08533cc78fb29b9d9bd62f6bb93e5ef333407013fed185776808f11223ba0e70", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts": "e09fc79d8065fe222578114b109b1fd60077bff1bb75448532077f784f4d6a83", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/constants.ts": "90f3be047ec0a89bcb5d6fc30db121685fc82cb00b1c476124ff47a4b0472aa9", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/db.ts": "03d0c860957496eadedd86e51a6e650670764630e64f56df0092e86c90752401", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/function.ts": "e4c83b8ec64bf88bafad2407376b0c6a3b54e777593c70336fb40d43a79865f2", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad", + "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487", + "https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js": "a336e5c58b1e6946ae8943eb4fef21b810dc2a5a233438cff92b883673e29c96" + }, + "workspace": { + "dependencies": [ + "jsr:@nostrify/nostrify@^0.15.0", + "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", + "jsr:@std/cli@^0.223.0", + "jsr:@std/crypto@^0.224.0", + "jsr:@std/encoding@^0.224.0", + "jsr:@std/json@^0.223.0", + "jsr:@std/media-types@^0.224.0", + "jsr:@std/streams@^0.223.0", + "npm:kysely@^0.27.3", + "npm:nostr-tools@^2.5.1", + "npm:nostr-wasm@^0.1.0", + "npm:zod@^3.23.4" + ] + } +} From 58ed1b111f09083728bbc340ff1bf5ab55834db3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 15:55:56 -0500 Subject: [PATCH 029/252] Comlink alias --- deno.json | 1 + deno.lock | 7 +++++++ src/deps.ts | 1 - src/workers/fetch.ts | 2 +- src/workers/fetch.worker.ts | 4 +++- src/workers/handlers/abortsignal.ts | 2 +- src/workers/sqlite.ts | 5 ++--- src/workers/sqlite.worker.ts | 3 ++- src/workers/trends.ts | 2 +- src/workers/trends.worker.ts | 4 +++- src/workers/verify.ts | 2 +- src/workers/verify.worker.ts | 2 +- 12 files changed, 23 insertions(+), 12 deletions(-) diff --git a/deno.json b/deno.json index 3eae6324..aa6c4758 100644 --- a/deno.json +++ b/deno.json @@ -24,6 +24,7 @@ "@std/json": "jsr:@std/json@^0.223.0", "@std/media-types": "jsr:@std/media-types@^0.224.0", "@std/streams": "jsr:@std/streams@^0.223.0", + "comlink": "npm:comlink@^4.4.1", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", "kysely": "npm:kysely@^0.27.3", diff --git a/deno.lock b/deno.lock index f984b36f..22586540 100644 --- a/deno.lock +++ b/deno.lock @@ -1313,15 +1313,18 @@ "https://deno.land/x/hono@v3.10.1/adapter/deno/serve-static.ts": "ba10cf6aaf39da942b0d49c3b9877ddba69d41d414c6551d890beb1085f58eea", "https://deno.land/x/hono@v3.10.1/client/client.ts": "ff340f58041203879972dd368b011ed130c66914f789826610869a90603406bf", "https://deno.land/x/hono@v3.10.1/client/index.ts": "3ff4cf246f3543f827a85a2c84d66a025ac350ee927613629bda47e854bfb7ba", + "https://deno.land/x/hono@v3.10.1/client/types.ts": "52c66cbe74540e1811259a48c30622ac915666196eb978092d166435cbc15213", "https://deno.land/x/hono@v3.10.1/client/utils.ts": "053273c002963b549d38268a1b423ac8ca211a8028bdab1ed0b781a62aa5e661", "https://deno.land/x/hono@v3.10.1/compose.ts": "e8ab4b345aa367f2dd65f221c9fe829dd885326a613f4215b654f93a4066bb5c", "https://deno.land/x/hono@v3.10.1/context.ts": "261cc8b8b1e8f04b98beab1cca6692f317b7dc6d2b75b4f84c982e54cf1db730", + "https://deno.land/x/hono@v3.10.1/helper/adapter/index.ts": "eea9b4caedbfa3a3b4a020bf46c88c0171a00008cd6c10708cd85a3e39d86e62", "https://deno.land/x/hono@v3.10.1/helper/cookie/index.ts": "55ccd20bbd8d9a8bb2ecd998e90845c1d306c19027f54b3d1b89a5be35968b80", "https://deno.land/x/hono@v3.10.1/helper/html/index.ts": "aba19e8d29f217c7fffa5719cf606c4e259b540d51296e82bbea3c992e2ecbc6", "https://deno.land/x/hono@v3.10.1/hono-base.ts": "cc55e0a4c63a7bdf44df3e804ea4737d5399eeb6606b45d102f8e48c3ff1e925", "https://deno.land/x/hono@v3.10.1/hono.ts": "2cc4c292e541463a4d6f83edbcea58048d203e9564ae62ec430a3d466b49a865", "https://deno.land/x/hono@v3.10.1/http-exception.ts": "6071df078b5f76d279684d52fe82a590f447a64ffe1b75eb5064d0c8a8d2d676", "https://deno.land/x/hono@v3.10.1/jsx/index.ts": "019512d3a9b3897b879e87fa5fb179cd34f3d326f8ff8b93379c2bb707ec168a", + "https://deno.land/x/hono@v3.10.1/jsx/intrinsic-elements.ts": "03250beb610bda1c72017bc0912c2505ff764b7a8d869e7e4add40eb4cfec096", "https://deno.land/x/hono@v3.10.1/jsx/streaming.ts": "5d03b4d02eaa396c8f0f33c3f6e8c7ed3afb7598283c2d4a7ddea0ada8c212a7", "https://deno.land/x/hono@v3.10.1/middleware.ts": "57b2047c4b9d775a052a9c44a3b805802c1d1cb477ab9c4bb6185d27382d1b96", "https://deno.land/x/hono@v3.10.1/middleware/basic-auth/index.ts": "5505288ccf9364f56f7be2dfac841543b72e20656e54ac646a1a73a0aa853261", @@ -1353,6 +1356,7 @@ "https://deno.land/x/hono@v3.10.1/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41", "https://deno.land/x/hono@v3.10.1/router/trie-router/node.ts": "3af15fa9c9994a8664a2b7a7c11233504b5bb9d4fcf7bb34cf30d7199052c39f", "https://deno.land/x/hono@v3.10.1/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d", + "https://deno.land/x/hono@v3.10.1/types.ts": "edc414a92383f9deb82f5f7a09e95bcf76f6100c23457c27d041986768f5345c", "https://deno.land/x/hono@v3.10.1/utils/body.ts": "7a16a6656331a96bcae57642f8d5e3912bd361cbbcc2c0d2157ecc3f218f7a92", "https://deno.land/x/hono@v3.10.1/utils/buffer.ts": "9066a973e64498cb262c7e932f47eed525a51677b17f90893862b7279dc0773e", "https://deno.land/x/hono@v3.10.1/utils/cookie.ts": "19920ba6756944aae1ad8585c3ddeaa9df479733f59d05359db096f7361e5e4b", @@ -1360,11 +1364,13 @@ "https://deno.land/x/hono@v3.10.1/utils/encode.ts": "3b7c7d736123b5073542b34321700d4dbf5ff129c138f434bb2144a4d425ee89", "https://deno.land/x/hono@v3.10.1/utils/filepath.ts": "18461b055a914d6da85077f453051b516281bb17cf64fa74bf5ef604dc9d2861", "https://deno.land/x/hono@v3.10.1/utils/html.ts": "01c1520a4256f899da1954357cf63ae11c348eda141a505f72d7090cf5481aba", + "https://deno.land/x/hono@v3.10.1/utils/http-status.ts": "e0c4343ea7717c314dc600131e16b636c29d61cfdaf9df93b267258d1729d1a0", "https://deno.land/x/hono@v3.10.1/utils/jwt/index.ts": "5e4b82a42eb3603351dfce726cd781ca41cb57437395409d227131aec348d2d5", "https://deno.land/x/hono@v3.10.1/utils/jwt/jwt.ts": "02ff7bbf1298ffcc7a40266842f8eac44b6c136453e32d4441e24d0cbfba3a95", "https://deno.land/x/hono@v3.10.1/utils/jwt/types.ts": "58ddf908f76ba18d9c62ddfc2d1e40cc2e306bf987409a6169287efa81ce2546", "https://deno.land/x/hono@v3.10.1/utils/mime.ts": "0105d2b5e8e91f07acc70f5d06b388313995d62af23c802fcfba251f5a744d95", "https://deno.land/x/hono@v3.10.1/utils/stream.ts": "1789dcc73c5b0ede28f83d7d34e47ae432c20e680907cb3275a9c9187f293983", + "https://deno.land/x/hono@v3.10.1/utils/types.ts": "ddff055e6d35066232efdfbd42c8954e855c04279c27dcd735d929b6b4f319b3", "https://deno.land/x/hono@v3.10.1/utils/url.ts": "5fc3307ef3cb2e6f34ec2a03e3d7f2126c6a9f5f0eab677222df3f0e40bd7567", "https://deno.land/x/hono@v3.10.1/validator/index.ts": "6c986e8b91dcf857ecc8164a506ae8eea8665792a4ff7215471df669c632ae7c", "https://deno.land/x/hono@v3.10.1/validator/validator.ts": "afa5e52495e0996fbba61996736fab5c486590d72d376f809e9f9ff4e0c463e9", @@ -1496,6 +1502,7 @@ "jsr:@std/json@^0.223.0", "jsr:@std/media-types@^0.224.0", "jsr:@std/streams@^0.223.0", + "npm:comlink@^4.4.1", "npm:kysely@^0.27.3", "npm:nostr-tools@^2.5.1", "npm:nostr-wasm@^0.1.0", diff --git a/src/deps.ts b/src/deps.ts index 2c84d9e3..2111f0f6 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -36,7 +36,6 @@ export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; export { default as uuid62 } from 'npm:uuid62@^1.0.2'; export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts'; -export * as Comlink from 'npm:comlink@^4.4.1'; export { EventEmitter } from 'npm:tseep@^1.1.3'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; export { default as Debug } from 'https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/debug.ts'; diff --git a/src/workers/fetch.ts b/src/workers/fetch.ts index 510d806f..f0bece58 100644 --- a/src/workers/fetch.ts +++ b/src/workers/fetch.ts @@ -1,4 +1,4 @@ -import { Comlink } from '@/deps.ts'; +import * as Comlink from 'comlink'; import './handlers/abortsignal.ts'; diff --git a/src/workers/fetch.worker.ts b/src/workers/fetch.worker.ts index 8e79465f..c3b4c72c 100644 --- a/src/workers/fetch.worker.ts +++ b/src/workers/fetch.worker.ts @@ -1,4 +1,6 @@ -import { Comlink, Debug } from '@/deps.ts'; +import * as Comlink from 'comlink'; + +import { Debug } from '@/deps.ts'; import './handlers/abortsignal.ts'; diff --git a/src/workers/handlers/abortsignal.ts b/src/workers/handlers/abortsignal.ts index c4c6a3e9..14cf9f41 100644 --- a/src/workers/handlers/abortsignal.ts +++ b/src/workers/handlers/abortsignal.ts @@ -1,4 +1,4 @@ -import { Comlink } from '@/deps.ts'; +import * as Comlink from 'comlink'; const signalFinalizers = new FinalizationRegistry((port: MessagePort) => { port.postMessage(null); diff --git a/src/workers/sqlite.ts b/src/workers/sqlite.ts index 1d29d4a7..c6f2d2e2 100644 --- a/src/workers/sqlite.ts +++ b/src/workers/sqlite.ts @@ -1,6 +1,5 @@ -import type { CompiledQuery, QueryResult } from 'kysely'; - -import { Comlink } from '@/deps.ts'; +import * as Comlink from 'comlink'; +import { CompiledQuery, QueryResult } from 'kysely'; import type { SqliteWorker as _SqliteWorker } from './sqlite.worker.ts'; diff --git a/src/workers/sqlite.worker.ts b/src/workers/sqlite.worker.ts index 4739638a..753e7aea 100644 --- a/src/workers/sqlite.worker.ts +++ b/src/workers/sqlite.worker.ts @@ -1,8 +1,9 @@ /// import { ScopedPerformance } from 'https://deno.land/x/scoped_performance@v2.0.0/mod.ts'; +import * as Comlink from 'comlink'; import { CompiledQuery, QueryResult } from 'kysely'; -import { Comlink, DenoSqlite3, Stickynotes } from '@/deps.ts'; +import { DenoSqlite3, Stickynotes } from '@/deps.ts'; import '@/sentry.ts'; let db: DenoSqlite3 | undefined; diff --git a/src/workers/trends.ts b/src/workers/trends.ts index b4552835..31db3818 100644 --- a/src/workers/trends.ts +++ b/src/workers/trends.ts @@ -1,4 +1,4 @@ -import { Comlink } from '@/deps.ts'; +import * as Comlink from 'comlink'; import type { TrendsWorker as _TrendsWorker } from '@/workers/trends.worker.ts'; diff --git a/src/workers/trends.worker.ts b/src/workers/trends.worker.ts index df06fbb6..819883ff 100644 --- a/src/workers/trends.worker.ts +++ b/src/workers/trends.worker.ts @@ -1,4 +1,6 @@ -import { Comlink, Sqlite } from '@/deps.ts'; +import * as Comlink from 'comlink'; + +import { Sqlite } from '@/deps.ts'; import { hashtagSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; import { generateDateRange, Time } from '@/utils/time.ts'; diff --git a/src/workers/verify.ts b/src/workers/verify.ts index 0dde872d..15ad783a 100644 --- a/src/workers/verify.ts +++ b/src/workers/verify.ts @@ -1,5 +1,5 @@ import { NostrEvent } from '@nostrify/nostrify'; -import { Comlink } from '@/deps.ts'; +import * as Comlink from 'comlink'; import type { VerifyWorker } from './verify.worker.ts'; diff --git a/src/workers/verify.worker.ts b/src/workers/verify.worker.ts index d72387a2..e218474e 100644 --- a/src/workers/verify.worker.ts +++ b/src/workers/verify.worker.ts @@ -1,7 +1,7 @@ import { NostrEvent } from '@nostrify/nostrify'; +import * as Comlink from 'comlink'; import { VerifiedEvent, verifyEvent } from 'nostr-tools'; -import { Comlink } from '@/deps.ts'; import '@/nostr-wasm.ts'; export const VerifyWorker = { From 9d0be2de0d142579014bb38c4440b9bffa2a3d40 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 15:57:28 -0500 Subject: [PATCH 030/252] nostr-relaypool alias --- deno.json | 1 + deno.lock | 1 + src/deps.ts | 1 - src/pool.ts | 3 ++- src/storages/pool-store.ts | 3 ++- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index aa6c4758..e3cd02f0 100644 --- a/deno.json +++ b/deno.json @@ -29,6 +29,7 @@ "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", "kysely": "npm:kysely@^0.27.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", + "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", "zod": "npm:zod@^3.23.4", diff --git a/deno.lock b/deno.lock index 22586540..508e7fb9 100644 --- a/deno.lock +++ b/deno.lock @@ -1504,6 +1504,7 @@ "jsr:@std/streams@^0.223.0", "npm:comlink@^4.4.1", "npm:kysely@^0.27.3", + "npm:nostr-relaypool2@0.6.34", "npm:nostr-tools@^2.5.1", "npm:nostr-wasm@^0.1.0", "npm:zod@^3.23.4" diff --git a/src/deps.ts b/src/deps.ts index 2111f0f6..da98d647 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,5 +1,4 @@ import 'https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts'; -export { RelayPoolWorker } from 'npm:nostr-relaypool2@0.6.34'; export { parseFormData } from 'npm:formdata-helper@^0.3.0'; // @deno-types="npm:@types/lodash@4.14.194" export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; diff --git a/src/pool.ts b/src/pool.ts index 06c251e9..48d5e1fa 100644 --- a/src/pool.ts +++ b/src/pool.ts @@ -1,5 +1,6 @@ +import { RelayPoolWorker } from 'nostr-relaypool'; + import { getActiveRelays } from '@/db/relays.ts'; -import { RelayPoolWorker } from '@/deps.ts'; const activeRelays = await getActiveRelays(); diff --git a/src/storages/pool-store.ts b/src/storages/pool-store.ts index 93ca24e9..a8bd09aa 100644 --- a/src/storages/pool-store.ts +++ b/src/storages/pool-store.ts @@ -1,7 +1,8 @@ import { NostrEvent, NostrFilter, NSet, NStore } from '@nostrify/nostrify'; +import { RelayPoolWorker } from 'nostr-relaypool'; import { matchFilters } from 'nostr-tools'; -import { Debug, type RelayPoolWorker } from '@/deps.ts'; +import { Debug } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { abortError } from '@/utils/abort.ts'; From 3513206de1670b59369504a5b36443ebdf0eea18 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:14:50 -0500 Subject: [PATCH 031/252] stickynotes alias --- deno.json | 1 + deno.lock | 5 +++++ src/app.ts | 2 +- src/controllers/api/streaming.ts | 2 +- src/db/users.ts | 3 ++- src/deps.ts | 2 -- src/firehose.ts | 3 ++- src/middleware/cache.ts | 3 ++- src/pipeline.ts | 2 +- src/queries.ts | 3 ++- src/stats.ts | 2 +- src/storages/events-db.ts | 2 +- src/storages/optimizer.ts | 3 ++- src/storages/pool-store.ts | 2 +- src/storages/reqmeister.ts | 4 +++- src/storages/search-store.ts | 3 ++- src/utils/api.ts | 3 ++- src/utils/lnurl.ts | 3 ++- src/utils/nip05.ts | 2 +- src/utils/unfurl.ts | 4 +++- src/workers/fetch.worker.ts | 3 +-- src/workers/sqlite.worker.ts | 3 ++- 22 files changed, 38 insertions(+), 22 deletions(-) diff --git a/deno.json b/deno.json index e3cd02f0..3b6c0420 100644 --- a/deno.json +++ b/deno.json @@ -18,6 +18,7 @@ "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", + "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", diff --git a/deno.lock b/deno.lock index 508e7fb9..9c302b18 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,7 @@ "specifiers": { "jsr:@nostrify/nostrify@^0.15.0": "jsr:@nostrify/nostrify@0.15.0", "jsr:@soapbox/kysely-deno-sqlite@^2.0.2": "jsr:@soapbox/kysely-deno-sqlite@2.0.2", + "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.0", @@ -68,6 +69,9 @@ "npm:kysely@^0.27.2" ] }, + "@soapbox/stickynotes@0.4.0": { + "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" + }, "@std/assert@0.224.0": { "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" }, @@ -1496,6 +1500,7 @@ "dependencies": [ "jsr:@nostrify/nostrify@^0.15.0", "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", + "jsr:@soapbox/stickynotes@^0.4.0", "jsr:@std/cli@^0.223.0", "jsr:@std/crypto@^0.224.0", "jsr:@std/encoding@^0.224.0", diff --git a/src/app.ts b/src/app.ts index 370abc5a..7e91a013 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,9 +1,9 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; import { type User } from '@/db/users.ts'; -import { Debug } from '@/deps.ts'; import '@/firehose.ts'; import { Time } from '@/utils.ts'; diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 4bbc627b..965855c3 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -1,9 +1,9 @@ import { NostrFilter } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { Debug } from '@/deps.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; diff --git a/src/db/users.ts b/src/db/users.ts index 841e981f..c7659e43 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -1,6 +1,7 @@ import { NostrFilter } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; + import { Conf } from '@/config.ts'; -import { Debug } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; diff --git a/src/deps.ts b/src/deps.ts index da98d647..2878bc6f 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -37,7 +37,5 @@ export { default as uuid62 } from 'npm:uuid62@^1.0.2'; export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts'; export { EventEmitter } from 'npm:tseep@^1.1.3'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; -export { default as Debug } from 'https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/debug.ts'; -export { Stickynotes } from 'https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/mod.ts'; export type * as TypeFest from 'npm:type-fest@^4.3.0'; diff --git a/src/firehose.ts b/src/firehose.ts index 98cb4db1..d7aaab35 100644 --- a/src/firehose.ts +++ b/src/firehose.ts @@ -1,5 +1,6 @@ import { NostrEvent } from '@nostrify/nostrify'; -import { Debug } from '@/deps.ts'; +import Debug from '@soapbox/stickynotes/debug'; + import { activeRelays, pool } from '@/pool.ts'; import { nostrNow } from '@/utils.ts'; diff --git a/src/middleware/cache.ts b/src/middleware/cache.ts index fe28c5fe..181623f6 100644 --- a/src/middleware/cache.ts +++ b/src/middleware/cache.ts @@ -1,5 +1,6 @@ +import Debug from '@soapbox/stickynotes/debug'; import { type MiddlewareHandler } from 'hono'; -import { Debug } from '@/deps.ts'; + import ExpiringCache from '@/utils/expiring-cache.ts'; const debug = Debug('ditto:middleware:cache'); diff --git a/src/pipeline.ts b/src/pipeline.ts index 5fa9f23f..f91626dd 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,12 +1,12 @@ import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; +import Debug from '@soapbox/stickynotes/debug'; import { sql } from 'kysely'; import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; import { addRelays } from '@/db/relays.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; -import { Debug } from '@/deps.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; diff --git a/src/queries.ts b/src/queries.ts index e4fdc21d..5626c45e 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -1,7 +1,8 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; + import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; -import { Debug } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoRelation } from '@/interfaces/DittoFilter.ts'; import { findReplyTag, getTagSet } from '@/tags.ts'; diff --git a/src/stats.ts b/src/stats.ts index e75b57c7..2bb3e593 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,9 +1,9 @@ import { NostrEvent } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { InsertQueryBuilder } from 'kysely'; import { db } from '@/db.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { Debug } from '@/deps.ts'; import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 22f15e11..74bcb01b 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,9 +1,9 @@ import { NIP50, NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { Kysely, type SelectQueryBuilder } from 'kysely'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { Debug } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; diff --git a/src/storages/optimizer.ts b/src/storages/optimizer.ts index 518fc15c..7b4153e9 100644 --- a/src/storages/optimizer.ts +++ b/src/storages/optimizer.ts @@ -1,5 +1,6 @@ import { NostrFilter, NSet, NStore } from '@nostrify/nostrify'; -import { Debug } from '@/deps.ts'; +import Debug from '@soapbox/stickynotes/debug'; + import { normalizeFilters } from '@/filter.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { abortError } from '@/utils/abort.ts'; diff --git a/src/storages/pool-store.ts b/src/storages/pool-store.ts index a8bd09aa..953720b0 100644 --- a/src/storages/pool-store.ts +++ b/src/storages/pool-store.ts @@ -1,8 +1,8 @@ import { NostrEvent, NostrFilter, NSet, NStore } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { RelayPoolWorker } from 'nostr-relaypool'; import { matchFilters } from 'nostr-tools'; -import { Debug } from '@/deps.ts'; import { normalizeFilters } from '@/filter.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { abortError } from '@/utils/abort.ts'; diff --git a/src/storages/reqmeister.ts b/src/storages/reqmeister.ts index 6be5a56b..a385ec9b 100644 --- a/src/storages/reqmeister.ts +++ b/src/storages/reqmeister.ts @@ -1,5 +1,7 @@ import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; -import { Debug, EventEmitter } from '@/deps.ts'; +import Debug from '@soapbox/stickynotes/debug'; + +import { EventEmitter } from '@/deps.ts'; import { eventToMicroFilter, getFilterId, isMicrofilter, type MicroFilter } from '@/filter.ts'; import { Time } from '@/utils/time.ts'; import { abortError } from '@/utils/abort.ts'; diff --git a/src/storages/search-store.ts b/src/storages/search-store.ts index 61508966..be6e2b44 100644 --- a/src/storages/search-store.ts +++ b/src/storages/search-store.ts @@ -1,5 +1,6 @@ import { NostrEvent, NostrFilter, NRelay1, NStore } from '@nostrify/nostrify'; -import { Debug } from '@/deps.ts'; +import Debug from '@soapbox/stickynotes/debug'; + import { normalizeFilters } from '@/filter.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; diff --git a/src/utils/api.ts b/src/utils/api.ts index 7b090ee6..ee0ede22 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,11 +1,12 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { type Context, HTTPException } from 'hono'; import { EventTemplate } from 'nostr-tools'; import { z } from 'zod'; import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { Debug, parseFormData, type TypeFest } from '@/deps.ts'; +import { parseFormData, type TypeFest } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; diff --git a/src/utils/lnurl.ts b/src/utils/lnurl.ts index a84d20bc..ea5ce8a6 100644 --- a/src/utils/lnurl.ts +++ b/src/utils/lnurl.ts @@ -1,5 +1,6 @@ import { LNURL, LNURLDetails } from '@nostrify/nostrify/ln'; -import { Debug } from '@/deps.ts'; +import Debug from '@soapbox/stickynotes/debug'; + import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { Time } from '@/utils/time.ts'; import { fetchWorker } from '@/workers/fetch.ts'; diff --git a/src/utils/nip05.ts b/src/utils/nip05.ts index bcef61f5..0b4c6e39 100644 --- a/src/utils/nip05.ts +++ b/src/utils/nip05.ts @@ -1,8 +1,8 @@ import { NIP05 } from '@nostrify/nostrify'; +import Debug from '@soapbox/stickynotes/debug'; import { nip19 } from 'nostr-tools'; import { Conf } from '@/config.ts'; -import { Debug } from '@/deps.ts'; import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { Time } from '@/utils/time.ts'; import { Storages } from '@/storages.ts'; diff --git a/src/utils/unfurl.ts b/src/utils/unfurl.ts index da11e843..bde55618 100644 --- a/src/utils/unfurl.ts +++ b/src/utils/unfurl.ts @@ -1,4 +1,6 @@ -import { Debug, sanitizeHtml, TTLCache, unfurl } from '@/deps.ts'; +import Debug from '@soapbox/stickynotes/debug'; + +import { sanitizeHtml, TTLCache, unfurl } from '@/deps.ts'; import { Time } from '@/utils/time.ts'; import { fetchWorker } from '@/workers/fetch.ts'; diff --git a/src/workers/fetch.worker.ts b/src/workers/fetch.worker.ts index c3b4c72c..d44e043c 100644 --- a/src/workers/fetch.worker.ts +++ b/src/workers/fetch.worker.ts @@ -1,7 +1,6 @@ +import Debug from '@soapbox/stickynotes/debug'; import * as Comlink from 'comlink'; -import { Debug } from '@/deps.ts'; - import './handlers/abortsignal.ts'; const debug = Debug('ditto:fetch.worker'); diff --git a/src/workers/sqlite.worker.ts b/src/workers/sqlite.worker.ts index 753e7aea..df9d6f01 100644 --- a/src/workers/sqlite.worker.ts +++ b/src/workers/sqlite.worker.ts @@ -1,9 +1,10 @@ /// import { ScopedPerformance } from 'https://deno.land/x/scoped_performance@v2.0.0/mod.ts'; +import { Stickynotes } from '@soapbox/stickynotes'; import * as Comlink from 'comlink'; import { CompiledQuery, QueryResult } from 'kysely'; -import { DenoSqlite3, Stickynotes } from '@/deps.ts'; +import { DenoSqlite3 } from '@/deps.ts'; import '@/sentry.ts'; let db: DenoSqlite3 | undefined; From 973791cde1755eeca3e5a0599e99c5d2e9a42e06 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:16:19 -0500 Subject: [PATCH 032/252] type-fest alias --- deno.json | 1 + deno.lock | 1 + src/deps.ts | 2 -- src/utils/api.ts | 3 ++- src/views/mastodon/attachments.ts | 3 ++- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/deno.json b/deno.json index 3b6c0420..0bc5d0bb 100644 --- a/deno.json +++ b/deno.json @@ -33,6 +33,7 @@ "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", + "type-fest": "npm:type-fest@^4.3.0", "zod": "npm:zod@^3.23.4", "~/fixtures/": "./fixtures/" }, diff --git a/deno.lock b/deno.lock index 9c302b18..eb53807a 100644 --- a/deno.lock +++ b/deno.lock @@ -1512,6 +1512,7 @@ "npm:nostr-relaypool2@0.6.34", "npm:nostr-tools@^2.5.1", "npm:nostr-wasm@^0.1.0", + "npm:type-fest@^4.3.0", "npm:zod@^3.23.4" ] } diff --git a/src/deps.ts b/src/deps.ts index 2878bc6f..78e90d01 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -37,5 +37,3 @@ export { default as uuid62 } from 'npm:uuid62@^1.0.2'; export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts'; export { EventEmitter } from 'npm:tseep@^1.1.3'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; - -export type * as TypeFest from 'npm:type-fest@^4.3.0'; diff --git a/src/utils/api.ts b/src/utils/api.ts index ee0ede22..85d57832 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -2,11 +2,12 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, HTTPException } from 'hono'; import { EventTemplate } from 'nostr-tools'; +import * as TypeFest from 'type-fest'; import { z } from 'zod'; import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { parseFormData, type TypeFest } from '@/deps.ts'; +import { parseFormData } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 38ddb376..3ea989e6 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -1,5 +1,6 @@ +import * as TypeFest from 'type-fest'; + import { UnattachedMedia } from '@/db/unattached-media.ts'; -import { type TypeFest } from '@/deps.ts'; type DittoAttachment = TypeFest.SetOptional; From c7b34ed31b895037c01432af2bd5626243a23cc1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:18:28 -0500 Subject: [PATCH 033/252] iso-639-1 alias --- deno.json | 1 + deno.lock | 1 + src/controllers/api/statuses.ts | 2 +- src/deps.ts | 2 -- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index 0bc5d0bb..526d43f8 100644 --- a/deno.json +++ b/deno.json @@ -28,6 +28,7 @@ "comlink": "npm:comlink@^4.4.1", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", + "iso-639-1": "npm:iso-639-1@2.1.15", "kysely": "npm:kysely@^0.27.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", diff --git a/deno.lock b/deno.lock index eb53807a..4a7aa616 100644 --- a/deno.lock +++ b/deno.lock @@ -1508,6 +1508,7 @@ "jsr:@std/media-types@^0.224.0", "jsr:@std/streams@^0.223.0", "npm:comlink@^4.4.1", + "npm:iso-639-1@2.1.15", "npm:kysely@^0.27.3", "npm:nostr-relaypool2@0.6.34", "npm:nostr-tools@^2.5.1", diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 42923365..2c05dbd4 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,11 +1,11 @@ import { NIP05, NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import ISO6391 from 'iso-639-1'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; -import { ISO6391 } from '@/deps.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { addTag, deleteTag } from '@/tags.ts'; diff --git a/src/deps.ts b/src/deps.ts index 78e90d01..55dc803f 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -11,7 +11,6 @@ export { unfurl } from 'npm:unfurl.js@^6.4.0'; export { default as TTLCache } from 'npm:@isaacs/ttlcache@^1.4.1'; // @deno-types="npm:@types/sanitize-html@2.9.0" export { default as sanitizeHtml } from 'npm:sanitize-html@^2.11.0'; -export { default as ISO6391 } from 'npm:iso-639-1@2.1.15'; export { createPentagon } from 'https://deno.land/x/pentagon@v0.1.4/mod.ts'; export { type ParsedSignature, @@ -34,6 +33,5 @@ export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; export { default as uuid62 } from 'npm:uuid62@^1.0.2'; -export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts'; export { EventEmitter } from 'npm:tseep@^1.1.3'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; From 8738aeb8208d6c31ee22c5c6704e35d6bc27775d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:19:53 -0500 Subject: [PATCH 034/252] tldts alias --- deno.json | 1 + src/db/relays.ts | 3 ++- src/deps.ts | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 526d43f8..e4f23437 100644 --- a/deno.json +++ b/deno.json @@ -34,6 +34,7 @@ "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", + "tldts": "npm:tldts@^6.0.14", "type-fest": "npm:type-fest@^4.3.0", "zod": "npm:zod@^3.23.4", "~/fixtures/": "./fixtures/" diff --git a/src/db/relays.ts b/src/db/relays.ts index 836f520e..da29b796 100644 --- a/src/db/relays.ts +++ b/src/db/relays.ts @@ -1,4 +1,5 @@ -import { tldts } from '@/deps.ts'; +import tldts from 'tldts'; + import { db } from '@/db.ts'; interface AddRelaysOpts { diff --git a/src/deps.ts b/src/deps.ts index 55dc803f..bdab9f46 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -28,7 +28,6 @@ export { } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; export * as dotenv from 'https://deno.land/std@0.198.0/dotenv/mod.ts'; -export { default as tldts } from 'npm:tldts@^6.0.14'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; From c4d8ad2368f6badab16ba79219acb30460edcecf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:21:18 -0500 Subject: [PATCH 035/252] uuid62 alias --- deno.json | 1 + src/db/unattached-media.ts | 3 ++- src/deps.ts | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index e4f23437..1cf44e99 100644 --- a/deno.json +++ b/deno.json @@ -36,6 +36,7 @@ "nostr-wasm": "npm:nostr-wasm@^0.1.0", "tldts": "npm:tldts@^6.0.14", "type-fest": "npm:type-fest@^4.3.0", + "uuid62": "npm:uuid62@^1.0.2", "zod": "npm:zod@^3.23.4", "~/fixtures/": "./fixtures/" }, diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 3761947b..21805ba6 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -1,5 +1,6 @@ +import { uuid62 } from 'uuid62'; + import { db } from '@/db.ts'; -import { uuid62 } from '@/deps.ts'; import { type MediaData } from '@/schemas/nostr.ts'; interface UnattachedMedia { diff --git a/src/deps.ts b/src/deps.ts index bdab9f46..3b770b10 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -31,6 +31,4 @@ export * as dotenv from 'https://deno.land/std@0.198.0/dotenv/mod.ts'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; -export { default as uuid62 } from 'npm:uuid62@^1.0.2'; -export { EventEmitter } from 'npm:tseep@^1.1.3'; export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; From ea665eed12516efd61dfc55a616537df845ec760 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:22:55 -0500 Subject: [PATCH 036/252] std/dotenv alias --- deno.json | 1 + deno.lock | 7 +++++++ src/config.ts | 3 +-- src/deps.ts | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index 1cf44e99..9bcdba98 100644 --- a/deno.json +++ b/deno.json @@ -21,6 +21,7 @@ "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", + "@std/dotenv": "jsr:@std/dotenv@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", "@std/json": "jsr:@std/json@^0.223.0", "@std/media-types": "jsr:@std/media-types@^0.224.0", diff --git a/deno.lock b/deno.lock index 4a7aa616..e948e583 100644 --- a/deno.lock +++ b/deno.lock @@ -7,6 +7,7 @@ "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", + "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.0", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.0", "jsr:@std/media-types@^0.224.0": "jsr:@std/media-types@0.224.0", "npm:@isaacs/ttlcache@^1.4.1": "npm:@isaacs/ttlcache@1.4.1", @@ -82,6 +83,9 @@ "jsr:@std/encoding@^0.224.0" ] }, + "@std/dotenv@0.224.0": { + "integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d" + }, "@std/encoding@0.224.0": { "integrity": "efb6dca97d3e9c31392bd5c8cfd9f9fc9decf5a1f4d1f78af7900a493bcf89b5" }, @@ -1503,6 +1507,7 @@ "jsr:@soapbox/stickynotes@^0.4.0", "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/json@^0.223.0", "jsr:@std/media-types@^0.224.0", @@ -1513,7 +1518,9 @@ "npm:nostr-relaypool2@0.6.34", "npm:nostr-tools@^2.5.1", "npm:nostr-wasm@^0.1.0", + "npm:tldts@^6.0.14", "npm:type-fest@^4.3.0", + "npm:uuid62@^1.0.2", "npm:zod@^3.23.4" ] } diff --git a/src/config.ts b/src/config.ts index 8331f715..2e8004f0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,10 +1,9 @@ import url from 'node:url'; +import * as dotenv from '@std/dotenv'; import { getPublicKey, nip19 } from 'nostr-tools'; import { z } from 'zod'; -import { dotenv } from '@/deps.ts'; - /** Load environment config from `.env` */ await dotenv.load({ export: true, diff --git a/src/deps.ts b/src/deps.ts index 3b770b10..d643515b 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -27,7 +27,6 @@ export { SqliteError, } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; -export * as dotenv from 'https://deno.land/std@0.198.0/dotenv/mod.ts'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; From e5c8030960a3cab32fe32ca3c958e37cf2455ca9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:29:21 -0500 Subject: [PATCH 037/252] Move more deps to aliases --- deno.json | 4 ++++ deno.lock | 33 +++++++++++++++++++++++++++++++++ src/deps.ts | 4 ---- src/filter.ts | 2 +- src/storages/reqmeister.ts | 2 +- src/uploaders/s3.ts | 2 +- src/utils/SimpleLRU.ts | 2 +- src/utils/rsa.ts | 4 +++- 8 files changed, 44 insertions(+), 9 deletions(-) diff --git a/deno.json b/deno.json index 9bcdba98..b59d2eaa 100644 --- a/deno.json +++ b/deno.json @@ -15,6 +15,7 @@ "exclude": ["./public"], "imports": { "@/": "./src/", + "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", @@ -27,15 +28,18 @@ "@std/media-types": "jsr:@std/media-types@^0.224.0", "@std/streams": "jsr:@std/streams@^0.223.0", "comlink": "npm:comlink@^4.4.1", + "fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", "iso-639-1": "npm:iso-639-1@2.1.15", "kysely": "npm:kysely@^0.27.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", + "lru-cache": "npm:lru-cache@^10.2.2", "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", "tldts": "npm:tldts@^6.0.14", + "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", "uuid62": "npm:uuid62@^1.0.2", "zod": "npm:zod@^3.23.4", diff --git a/deno.lock b/deno.lock index e948e583..bb494f5c 100644 --- a/deno.lock +++ b/deno.lock @@ -2,13 +2,17 @@ "version": "3", "packages": { "specifiers": { + "jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.4", "jsr:@nostrify/nostrify@^0.15.0": "jsr:@nostrify/nostrify@0.15.0", "jsr:@soapbox/kysely-deno-sqlite@^2.0.2": "jsr:@soapbox/kysely-deno-sqlite@2.0.2", "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", + "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", + "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.0", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.0", + "jsr:@std/io@^0.218": "jsr:@std/io@0.218.2", "jsr:@std/media-types@^0.224.0": "jsr:@std/media-types@0.224.0", "npm:@isaacs/ttlcache@^1.4.1": "npm:@isaacs/ttlcache@1.4.1", "npm:@noble/hashes@^1.4.0": "npm:@noble/hashes@1.4.0", @@ -32,6 +36,7 @@ "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:lru-cache@^10.2.0": "npm:lru-cache@10.2.0", + "npm:lru-cache@^10.2.2": "npm:lru-cache@10.2.2", "npm:mime@^3.0.0": "npm:mime@3.0.0", "npm:node-forge@^1.3.1": "npm:node-forge@1.3.1", "npm:nostr-relaypool2@0.6.34": "npm:nostr-relaypool2@0.6.34", @@ -42,6 +47,7 @@ "npm:sanitize-html@^2.11.0": "npm:sanitize-html@2.13.0", "npm:tldts@^6.0.14": "npm:tldts@6.1.18", "npm:tseep@^1.1.3": "npm:tseep@1.2.1", + "npm:tseep@^1.2.1": "npm:tseep@1.2.1", "npm:type-fest@^4.3.0": "npm:type-fest@4.15.0", "npm:unfurl.js@^6.4.0": "npm:unfurl.js@6.4.0", "npm:uuid62@^1.0.2": "npm:uuid62@1.0.2", @@ -50,6 +56,12 @@ "npm:zod@^3.23.4": "npm:zod@3.23.4" }, "jsr": { + "@bradenmacdonald/s3-lite-client@0.7.4": { + "integrity": "602666ef40d09621d35aa3ea8813e0bfd58b3558e3f0a1d20404b0e61aa0b37e", + "dependencies": [ + "jsr:@std/io@^0.218" + ] + }, "@nostrify/nostrify@0.15.0": { "integrity": "51c2fe9ac7264d22567cd1919a5bf5101a5207f651e65bc00b3de43f9038dfc8", "dependencies": [ @@ -73,9 +85,15 @@ "@soapbox/stickynotes@0.4.0": { "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" }, + "@std/assert@0.218.2": { + "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" + }, "@std/assert@0.224.0": { "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" }, + "@std/bytes@0.218.2": { + "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" + }, "@std/crypto@0.224.0": { "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", "dependencies": [ @@ -89,6 +107,13 @@ "@std/encoding@0.224.0": { "integrity": "efb6dca97d3e9c31392bd5c8cfd9f9fc9decf5a1f4d1f78af7900a493bcf89b5" }, + "@std/io@0.218.2": { + "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", + "dependencies": [ + "jsr:@std/assert@^0.218.2", + "jsr:@std/bytes@^0.218.2" + ] + }, "@std/media-types@0.224.0": { "integrity": "5ac87989393f8cb1c81bee02aef6f5d4c8289b416deabc04f9ad25dff292d0b0" } @@ -686,6 +711,10 @@ "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dependencies": {} }, + "lru-cache@10.2.2": { + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dependencies": {} + }, "lru-cache@6.0.0": { "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { @@ -1502,6 +1531,7 @@ }, "workspace": { "dependencies": [ + "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "jsr:@nostrify/nostrify@^0.15.0", "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", "jsr:@soapbox/stickynotes@^0.4.0", @@ -1513,12 +1543,15 @@ "jsr:@std/media-types@^0.224.0", "jsr:@std/streams@^0.223.0", "npm:comlink@^4.4.1", + "npm:fast-stable-stringify@^1.0.0", "npm:iso-639-1@2.1.15", "npm:kysely@^0.27.3", + "npm:lru-cache@^10.2.2", "npm:nostr-relaypool2@0.6.34", "npm:nostr-tools@^2.5.1", "npm:nostr-wasm@^0.1.0", "npm:tldts@^6.0.14", + "npm:tseep@^1.2.1", "npm:type-fest@^4.3.0", "npm:uuid62@^1.0.2", "npm:zod@^3.23.4" diff --git a/src/deps.ts b/src/deps.ts index d643515b..ae268f53 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -21,13 +21,9 @@ export { } from 'https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/mod.ts'; export { generateSeededRsa } from 'https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts'; export * as secp from 'npm:@noble/secp256k1@^2.0.0'; -export { LRUCache } from 'npm:lru-cache@^10.2.0'; export { DB as Sqlite, SqliteError, } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; -export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts'; -export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0'; -export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0'; diff --git a/src/filter.ts b/src/filter.ts index 9e99c4a7..3bb18a63 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,7 +1,7 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { stringifyStable } from 'fast-stable-stringify'; import { z } from 'zod'; -import { stringifyStable } from '@/deps.ts'; import { isReplaceableKind } from '@/kinds.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; diff --git a/src/storages/reqmeister.ts b/src/storages/reqmeister.ts index a385ec9b..eede2007 100644 --- a/src/storages/reqmeister.ts +++ b/src/storages/reqmeister.ts @@ -1,7 +1,7 @@ import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; +import { EventEmitter } from 'tseep'; -import { EventEmitter } from '@/deps.ts'; import { eventToMicroFilter, getFilterId, isMicrofilter, type MicroFilter } from '@/filter.ts'; import { Time } from '@/utils/time.ts'; import { abortError } from '@/utils/abort.ts'; diff --git a/src/uploaders/s3.ts b/src/uploaders/s3.ts index 29f3043f..267d8172 100644 --- a/src/uploaders/s3.ts +++ b/src/uploaders/s3.ts @@ -1,11 +1,11 @@ import { join } from 'node:path'; +import { S3Client } from '@bradenmacdonald/s3-lite-client'; import { crypto } from '@std/crypto'; import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; -import { S3Client } from '@/deps.ts'; import type { Uploader } from './types.ts'; diff --git a/src/utils/SimpleLRU.ts b/src/utils/SimpleLRU.ts index 26f51fc6..f1bf6512 100644 --- a/src/utils/SimpleLRU.ts +++ b/src/utils/SimpleLRU.ts @@ -1,6 +1,6 @@ // deno-lint-ignore-file ban-types -import { LRUCache } from '@/deps.ts'; +import { LRUCache } from 'lru-cache'; type FetchFn = (key: K, opts: O) => Promise; diff --git a/src/utils/rsa.ts b/src/utils/rsa.ts index 9155a729..0d3a5882 100644 --- a/src/utils/rsa.ts +++ b/src/utils/rsa.ts @@ -1,5 +1,7 @@ +import { LRUCache } from 'lru-cache'; + import { Conf } from '@/config.ts'; -import { generateSeededRsa, LRUCache, publicKeyToPem, secp } from '@/deps.ts'; +import { generateSeededRsa, publicKeyToPem, secp } from '@/deps.ts'; const opts = { bits: 2048, From 5a7a409981229b7091147fbdbe9a21e3d1f514aa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:34:16 -0500 Subject: [PATCH 038/252] Alias unfurl, linkifyjs etc --- deno.json | 5 +++++ deno.lock | 5 +++++ src/deps.ts | 6 ------ src/note.ts | 5 ++++- src/utils/unfurl.ts | 4 +++- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/deno.json b/deno.json index b59d2eaa..01996213 100644 --- a/deno.json +++ b/deno.json @@ -16,6 +16,7 @@ "imports": { "@/": "./src/", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", + "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", @@ -34,6 +35,9 @@ "iso-639-1": "npm:iso-639-1@2.1.15", "kysely": "npm:kysely@^0.27.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", + "linkify-plugin-hashtag": "npm:linkify-plugin-hashtag@^4.1.1", + "linkify-string": "npm:linkify-string@^4.1.1", + "linkifyjs": "npm:linkifyjs@^4.1.1", "lru-cache": "npm:lru-cache@^10.2.2", "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", @@ -41,6 +45,7 @@ "tldts": "npm:tldts@^6.0.14", "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", + "unfurl": "npm:unfurl.js@^6.4.0", "uuid62": "npm:uuid62@^1.0.2", "zod": "npm:zod@^3.23.4", "~/fixtures/": "./fixtures/" diff --git a/deno.lock b/deno.lock index bb494f5c..9cf26452 100644 --- a/deno.lock +++ b/deno.lock @@ -1542,10 +1542,14 @@ "jsr:@std/json@^0.223.0", "jsr:@std/media-types@^0.224.0", "jsr:@std/streams@^0.223.0", + "npm:@isaacs/ttlcache@^1.4.1", "npm:comlink@^4.4.1", "npm:fast-stable-stringify@^1.0.0", "npm:iso-639-1@2.1.15", "npm:kysely@^0.27.3", + "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-relaypool2@0.6.34", "npm:nostr-tools@^2.5.1", @@ -1553,6 +1557,7 @@ "npm:tldts@^6.0.14", "npm:tseep@^1.2.1", "npm:type-fest@^4.3.0", + "npm:unfurl.js@^6.4.0", "npm:uuid62@^1.0.2", "npm:zod@^3.23.4" ] diff --git a/src/deps.ts b/src/deps.ts index ae268f53..2eb367fa 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -2,16 +2,10 @@ import 'https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts'; export { parseFormData } from 'npm:formdata-helper@^0.3.0'; // @deno-types="npm:@types/lodash@4.14.194" export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; -export { default as linkify } from 'npm:linkifyjs@^4.1.1'; -export { default as linkifyStr } from 'npm:linkify-string@^4.1.1'; -import 'npm:linkify-plugin-hashtag@^4.1.1'; // @deno-types="npm:@types/mime@3.0.0" export { default as mime } from 'npm:mime@^3.0.0'; -export { unfurl } from 'npm:unfurl.js@^6.4.0'; -export { default as TTLCache } from 'npm:@isaacs/ttlcache@^1.4.1'; // @deno-types="npm:@types/sanitize-html@2.9.0" export { default as sanitizeHtml } from 'npm:sanitize-html@^2.11.0'; -export { createPentagon } from 'https://deno.land/x/pentagon@v0.1.4/mod.ts'; export { type ParsedSignature, pemToPublicKey, diff --git a/src/note.ts b/src/note.ts index fe03d7fc..5603c539 100644 --- a/src/note.ts +++ b/src/note.ts @@ -1,7 +1,10 @@ +import 'linkify-plugin-hashtag'; +import linkifyStr from 'linkify-string'; +import linkify from 'linkifyjs'; import { nip19, nip21 } from 'nostr-tools'; import { Conf } from '@/config.ts'; -import { linkify, linkifyStr, mime } from '@/deps.ts'; +import { mime } from '@/deps.ts'; import { type DittoAttachment } from '@/views/mastodon/attachments.ts'; linkify.registerCustomProtocol('nostr', true); diff --git a/src/utils/unfurl.ts b/src/utils/unfurl.ts index bde55618..22c69b16 100644 --- a/src/utils/unfurl.ts +++ b/src/utils/unfurl.ts @@ -1,6 +1,8 @@ +import TTLCache from '@isaacs/ttlcache'; import Debug from '@soapbox/stickynotes/debug'; +import { unfurl } from 'unfurl'; -import { sanitizeHtml, TTLCache, unfurl } from '@/deps.ts'; +import { sanitizeHtml } from '@/deps.ts'; import { Time } from '@/utils/time.ts'; import { fetchWorker } from '@/workers/fetch.ts'; From d1f643d7ad9bf7936641242d7afe1e16dfcc38f7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:35:39 -0500 Subject: [PATCH 039/252] secp256k1 alias --- deno.json | 1 + deno.lock | 1 + src/deps.ts | 1 - src/utils/rsa.ts | 3 ++- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 01996213..9ea8d73c 100644 --- a/deno.json +++ b/deno.json @@ -17,6 +17,7 @@ "@/": "./src/", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", + "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", diff --git a/deno.lock b/deno.lock index 9cf26452..e568a59e 100644 --- a/deno.lock +++ b/deno.lock @@ -1543,6 +1543,7 @@ "jsr:@std/media-types@^0.224.0", "jsr:@std/streams@^0.223.0", "npm:@isaacs/ttlcache@^1.4.1", + "npm:@noble/secp256k1@^2.0.0", "npm:comlink@^4.4.1", "npm:fast-stable-stringify@^1.0.0", "npm:iso-639-1@2.1.15", diff --git a/src/deps.ts b/src/deps.ts index 2eb367fa..09eb5b5d 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -14,7 +14,6 @@ export { verifyRequest, } from 'https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/mod.ts'; export { generateSeededRsa } from 'https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts'; -export * as secp from 'npm:@noble/secp256k1@^2.0.0'; export { DB as Sqlite, SqliteError, diff --git a/src/utils/rsa.ts b/src/utils/rsa.ts index 0d3a5882..6942c435 100644 --- a/src/utils/rsa.ts +++ b/src/utils/rsa.ts @@ -1,7 +1,8 @@ +import * as secp from '@noble/secp256k1'; import { LRUCache } from 'lru-cache'; import { Conf } from '@/config.ts'; -import { generateSeededRsa, publicKeyToPem, secp } from '@/deps.ts'; +import { generateSeededRsa, publicKeyToPem } from '@/deps.ts'; const opts = { bits: 2048, From 08ed52a57b5d37bc5a336a146dc8f7287eaddefc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:37:01 -0500 Subject: [PATCH 040/252] formdata-helper alias --- deno.json | 1 + deno.lock | 1 + src/deps.ts | 2 -- src/utils/api.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index 9ea8d73c..24bc364e 100644 --- a/deno.json +++ b/deno.json @@ -31,6 +31,7 @@ "@std/streams": "jsr:@std/streams@^0.223.0", "comlink": "npm:comlink@^4.4.1", "fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0", + "formdata-helper": "npm:formdata-helper@^0.3.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", "iso-639-1": "npm:iso-639-1@2.1.15", diff --git a/deno.lock b/deno.lock index e568a59e..494d289a 100644 --- a/deno.lock +++ b/deno.lock @@ -1546,6 +1546,7 @@ "npm:@noble/secp256k1@^2.0.0", "npm:comlink@^4.4.1", "npm:fast-stable-stringify@^1.0.0", + "npm:formdata-helper@^0.3.0", "npm:iso-639-1@2.1.15", "npm:kysely@^0.27.3", "npm:linkify-plugin-hashtag@^4.1.1", diff --git a/src/deps.ts b/src/deps.ts index 09eb5b5d..16723657 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,5 +1,4 @@ import 'https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts'; -export { parseFormData } from 'npm:formdata-helper@^0.3.0'; // @deno-types="npm:@types/lodash@4.14.194" export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; // @deno-types="npm:@types/mime@3.0.0" @@ -19,4 +18,3 @@ export { SqliteError, } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; -export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; diff --git a/src/utils/api.ts b/src/utils/api.ts index 85d57832..72f4c3e5 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,13 +1,13 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, HTTPException } from 'hono'; +import { parseFormData } from 'formdata-helper'; import { EventTemplate } from 'nostr-tools'; import * as TypeFest from 'type-fest'; import { z } from 'zod'; import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { parseFormData } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; From 7de5cdc18d1397dab224b72e91bc6aa58037f19f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:40:58 -0500 Subject: [PATCH 041/252] @db/sqlite, scoped_performance aliases --- deno.json | 2 ++ src/deps.ts | 2 -- src/workers/sqlite.worker.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deno.json b/deno.json index 24bc364e..ca05ab0d 100644 --- a/deno.json +++ b/deno.json @@ -16,6 +16,7 @@ "imports": { "@/": "./src/", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", + "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", @@ -44,6 +45,7 @@ "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", + "scoped_performance" :"https://deno.land/x/scoped_performance@v2.0.0/mod.ts", "tldts": "npm:tldts@^6.0.14", "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", diff --git a/src/deps.ts b/src/deps.ts index 16723657..7a8fa9aa 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -15,6 +15,4 @@ export { export { generateSeededRsa } from 'https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts'; export { DB as Sqlite, - SqliteError, } from 'https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts'; -export { Database as DenoSqlite3 } from 'https://deno.land/x/sqlite3@0.9.1/mod.ts'; diff --git a/src/workers/sqlite.worker.ts b/src/workers/sqlite.worker.ts index df9d6f01..a3ff1d68 100644 --- a/src/workers/sqlite.worker.ts +++ b/src/workers/sqlite.worker.ts @@ -1,18 +1,18 @@ /// -import { ScopedPerformance } from 'https://deno.land/x/scoped_performance@v2.0.0/mod.ts'; +import { Database as SQLite } from '@db/sqlite'; import { Stickynotes } from '@soapbox/stickynotes'; import * as Comlink from 'comlink'; import { CompiledQuery, QueryResult } from 'kysely'; +import { ScopedPerformance } from 'scoped_performance'; -import { DenoSqlite3 } from '@/deps.ts'; import '@/sentry.ts'; -let db: DenoSqlite3 | undefined; +let db: SQLite | undefined; const console = new Stickynotes('ditto:sqlite.worker'); export const SqliteWorker = { open(path: string): void { - db = new DenoSqlite3(path); + db = new SQLite(path); }, executeQuery({ sql, parameters }: CompiledQuery): QueryResult { if (!db) throw new Error('Database not open'); From 8959f85afbc7619be9b12eaa403e043646ea1194 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:42:07 -0500 Subject: [PATCH 042/252] Fix imports of uuid62 and fast-stringify-stable --- deno.lock | 57 ++++++++++++++++++++++++++++++++++++++ src/db/unattached-media.ts | 2 +- src/filter.ts | 2 +- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/deno.lock b/deno.lock index 494d289a..554ca0b3 100644 --- a/deno.lock +++ b/deno.lock @@ -3,17 +3,26 @@ "packages": { "specifiers": { "jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.4", + "jsr:@db/sqlite@^0.11.1": "jsr:@db/sqlite@0.11.1", + "jsr:@denosaurs/plug@1": "jsr:@denosaurs/plug@1.0.6", "jsr:@nostrify/nostrify@^0.15.0": "jsr:@nostrify/nostrify@0.15.0", "jsr:@soapbox/kysely-deno-sqlite@^2.0.2": "jsr:@soapbox/kysely-deno-sqlite@2.0.2", "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", + "jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0", "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", + "jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0", "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.0", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.0", + "jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0", + "jsr:@std/fs@^0.221.0": "jsr:@std/fs@0.221.0", "jsr:@std/io@^0.218": "jsr:@std/io@0.218.2", "jsr:@std/media-types@^0.224.0": "jsr:@std/media-types@0.224.0", + "jsr:@std/path@0.217": "jsr:@std/path@0.217.0", + "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0", "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", @@ -62,6 +71,22 @@ "jsr:@std/io@^0.218" ] }, + "@db/sqlite@0.11.1": { + "integrity": "546434e7ed762db07e6ade0f963540dd5e06723b802937bf260ff855b21ef9c5", + "dependencies": [ + "jsr:@denosaurs/plug@1", + "jsr:@std/path@0.217" + ] + }, + "@denosaurs/plug@1.0.6": { + "integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/fmt@^0.221.0", + "jsr:@std/fs@^0.221.0", + "jsr:@std/path@^0.221.0" + ] + }, "@nostrify/nostrify@0.15.0": { "integrity": "51c2fe9ac7264d22567cd1919a5bf5101a5207f651e65bc00b3de43f9038dfc8", "dependencies": [ @@ -85,9 +110,15 @@ "@soapbox/stickynotes@0.4.0": { "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" }, + "@std/assert@0.217.0": { + "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" + }, "@std/assert@0.218.2": { "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" }, + "@std/assert@0.221.0": { + "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a" + }, "@std/assert@0.224.0": { "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" }, @@ -104,9 +135,22 @@ "@std/dotenv@0.224.0": { "integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d" }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, "@std/encoding@0.224.0": { "integrity": "efb6dca97d3e9c31392bd5c8cfd9f9fc9decf5a1f4d1f78af7900a493bcf89b5" }, + "@std/fmt@0.221.0": { + "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" + }, + "@std/fs@0.221.0": { + "integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286", + "dependencies": [ + "jsr:@std/assert@^0.221.0", + "jsr:@std/path@^0.221.0" + ] + }, "@std/io@0.218.2": { "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", "dependencies": [ @@ -116,6 +160,18 @@ }, "@std/media-types@0.224.0": { "integrity": "5ac87989393f8cb1c81bee02aef6f5d4c8289b416deabc04f9ad25dff292d0b0" + }, + "@std/path@0.217.0": { + "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", + "dependencies": [ + "jsr:@std/assert@^0.217.0" + ] + }, + "@std/path@0.221.0": { + "integrity": "0a36f6b17314ef653a3a1649740cc8db51b25a133ecfe838f20b79a56ebe0095", + "dependencies": [ + "jsr:@std/assert@^0.221.0" + ] } }, "npm": { @@ -1532,6 +1588,7 @@ "workspace": { "dependencies": [ "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", + "jsr:@db/sqlite@^0.11.1", "jsr:@nostrify/nostrify@^0.15.0", "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 21805ba6..415c110d 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -1,4 +1,4 @@ -import { uuid62 } from 'uuid62'; +import uuid62 from 'uuid62'; import { db } from '@/db.ts'; import { type MediaData } from '@/schemas/nostr.ts'; diff --git a/src/filter.ts b/src/filter.ts index 3bb18a63..62473785 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,5 +1,5 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; -import { stringifyStable } from 'fast-stable-stringify'; +import stringifyStable from 'fast-stable-stringify'; import { z } from 'zod'; import { isReplaceableKind } from '@/kinds.ts'; From d94f831af11b5dc974213728566524a3f1da121b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:45:47 -0500 Subject: [PATCH 043/252] Bump zod to v3.23.5 --- deno.json | 2 +- deno.lock | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index ca05ab0d..3708be36 100644 --- a/deno.json +++ b/deno.json @@ -51,7 +51,7 @@ "type-fest": "npm:type-fest@^4.3.0", "unfurl": "npm:unfurl.js@^6.4.0", "uuid62": "npm:uuid62@^1.0.2", - "zod": "npm:zod@^3.23.4", + "zod": "npm:zod@^3.23.5", "~/fixtures/": "./fixtures/" }, "lint": { diff --git a/deno.lock b/deno.lock index 554ca0b3..c3ea4d46 100644 --- a/deno.lock +++ b/deno.lock @@ -62,7 +62,8 @@ "npm:uuid62@^1.0.2": "npm:uuid62@1.0.2", "npm:websocket-ts@^2.1.5": "npm:websocket-ts@2.1.5", "npm:zod@^3.21.0": "npm:zod@3.23.4", - "npm:zod@^3.23.4": "npm:zod@3.23.4" + "npm:zod@^3.23.4": "npm:zod@3.23.4", + "npm:zod@^3.23.5": "npm:zod@3.23.5" }, "jsr": { "@bradenmacdonald/s3-lite-client@0.7.4": { @@ -1279,6 +1280,10 @@ "zod@3.23.4": { "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", "dependencies": {} + }, + "zod@3.23.5": { + "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==", + "dependencies": {} } } }, @@ -1618,7 +1623,7 @@ "npm:type-fest@^4.3.0", "npm:unfurl.js@^6.4.0", "npm:uuid62@^1.0.2", - "npm:zod@^3.23.4" + "npm:zod@^3.23.5" ] } } From 984695391a407aef7ef3757d4376889f93bd7398 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 16:59:15 -0500 Subject: [PATCH 044/252] unfurl -> unfurl.js --- deno.json | 2 +- src/utils/unfurl.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 3708be36..494c3118 100644 --- a/deno.json +++ b/deno.json @@ -49,7 +49,7 @@ "tldts": "npm:tldts@^6.0.14", "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", - "unfurl": "npm:unfurl.js@^6.4.0", + "unfurl.js": "npm:unfurl.js@^6.4.0", "uuid62": "npm:uuid62@^1.0.2", "zod": "npm:zod@^3.23.5", "~/fixtures/": "./fixtures/" diff --git a/src/utils/unfurl.ts b/src/utils/unfurl.ts index 22c69b16..b028be50 100644 --- a/src/utils/unfurl.ts +++ b/src/utils/unfurl.ts @@ -1,6 +1,6 @@ import TTLCache from '@isaacs/ttlcache'; import Debug from '@soapbox/stickynotes/debug'; -import { unfurl } from 'unfurl'; +import { unfurl } from 'unfurl.js'; import { sanitizeHtml } from '@/deps.ts'; import { Time } from '@/utils/time.ts'; From 258e81df519d4c38c74aa4125806979a95220391 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 18:29:32 -0500 Subject: [PATCH 045/252] Admin relays: use "marker" property in the API, fix PUT controller --- src/app.ts | 4 ++-- src/controllers/api/ditto.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index 7e91a013..a3b507fe 100644 --- a/src/app.ts +++ b/src/app.ts @@ -75,7 +75,7 @@ import { auth19, requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; -import { adminRelaysController } from '@/controllers/api/ditto.ts'; +import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; import { storeMiddleware } from '@/middleware/store.ts'; interface AppEnv extends HonoEnv { @@ -190,7 +190,7 @@ app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigContr app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAdminDeleteStatusController); app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); -app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); +app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index e9485932..f0f70360 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -6,10 +6,11 @@ import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; +const markerSchema = z.enum(['read', 'write']); + const relaySchema = z.object({ url: z.string().url(), - read: z.boolean(), - write: z.boolean(), + marker: markerSchema.optional(), }); type RelayEntity = z.infer; @@ -31,7 +32,7 @@ export const adminSetRelaysController: AppController = async (c) => { const event = await new AdminSigner().signEvent({ kind: 10002, - tags: relays.map(({ url, read, write }) => ['r', url, read && write ? '' : read ? 'read' : 'write']), + tags: relays.map(({ url, marker }) => marker ? ['r', url, marker] : ['r', url]), content: '', created_at: Math.floor(Date.now() / 1000), }); @@ -47,8 +48,7 @@ function renderRelays(event: NostrEvent): RelayEntity[] { if (name === 'r') { const relay: RelayEntity = { url, - read: !marker || marker === 'read', - write: !marker || marker === 'write', + marker: markerSchema.safeParse(marker).success ? marker as 'read' | 'write' : undefined, }; acc.push(relay); } From d1a4a71e31c166597f4c582a2e535121c5495567 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 18:30:12 -0500 Subject: [PATCH 046/252] Remove the lockfile --- deno.json | 1 + deno.lock | 1629 ----------------------------------------------------- 2 files changed, 1 insertion(+), 1629 deletions(-) delete mode 100644 deno.lock diff --git a/deno.json b/deno.json index 494c3118..666f7d9d 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,6 @@ { "$schema": "https://deno.land/x/deno@v1.41.0/cli/schemas/config-file.v1.json", + "lock": false, "tasks": { "start": "deno run -A src/server.ts", "dev": "deno run -A --watch src/server.ts", diff --git a/deno.lock b/deno.lock deleted file mode 100644 index c3ea4d46..00000000 --- a/deno.lock +++ /dev/null @@ -1,1629 +0,0 @@ -{ - "version": "3", - "packages": { - "specifiers": { - "jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.4", - "jsr:@db/sqlite@^0.11.1": "jsr:@db/sqlite@0.11.1", - "jsr:@denosaurs/plug@1": "jsr:@denosaurs/plug@1.0.6", - "jsr:@nostrify/nostrify@^0.15.0": "jsr:@nostrify/nostrify@0.15.0", - "jsr:@soapbox/kysely-deno-sqlite@^2.0.2": "jsr:@soapbox/kysely-deno-sqlite@2.0.2", - "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", - "jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0", - "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", - "jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0", - "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", - "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", - "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", - "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.0", - "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", - "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.0", - "jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0", - "jsr:@std/fs@^0.221.0": "jsr:@std/fs@0.221.0", - "jsr:@std/io@^0.218": "jsr:@std/io@0.218.2", - "jsr:@std/media-types@^0.224.0": "jsr:@std/media-types@0.224.0", - "jsr:@std/path@0.217": "jsr:@std/path@0.217.0", - "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0", - "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/bip39@^1.3.0": "npm:@scure/bip39@1.3.0", - "npm:@types/lodash@4.14.194": "npm:@types/lodash@4.14.194", - "npm:@types/mime@3.0.0": "npm:@types/mime@3.0.0", - "npm:@types/node": "npm:@types/node@18.16.19", - "npm:@types/node-forge@^1.3.1": "npm:@types/node-forge@1.3.11", - "npm:@types/sanitize-html@2.9.0": "npm:@types/sanitize-html@2.9.0", - "npm:comlink@^4.4.1": "npm:comlink@4.4.1", - "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:ipfs-only-hash@^4.0.0": "npm:ipfs-only-hash@4.0.0", - "npm:iso-639-1@2.1.15": "npm:iso-639-1@2.1.15", - "npm:kysely@^0.27.2": "npm:kysely@0.27.3", - "npm:kysely@^0.27.3": "npm:kysely@0.27.3", - "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:lru-cache@^10.2.0": "npm:lru-cache@10.2.0", - "npm:lru-cache@^10.2.2": "npm:lru-cache@10.2.2", - "npm:mime@^3.0.0": "npm:mime@3.0.0", - "npm:node-forge@^1.3.1": "npm:node-forge@1.3.1", - "npm:nostr-relaypool2@0.6.34": "npm:nostr-relaypool2@0.6.34", - "npm:nostr-tools@^1.14.0": "npm:nostr-tools@1.17.0", - "npm:nostr-tools@^2.5.0": "npm:nostr-tools@2.5.1", - "npm:nostr-tools@^2.5.1": "npm:nostr-tools@2.5.1", - "npm:nostr-wasm@^0.1.0": "npm:nostr-wasm@0.1.0", - "npm:sanitize-html@^2.11.0": "npm:sanitize-html@2.13.0", - "npm:tldts@^6.0.14": "npm:tldts@6.1.18", - "npm:tseep@^1.1.3": "npm:tseep@1.2.1", - "npm:tseep@^1.2.1": "npm:tseep@1.2.1", - "npm:type-fest@^4.3.0": "npm:type-fest@4.15.0", - "npm:unfurl.js@^6.4.0": "npm:unfurl.js@6.4.0", - "npm:uuid62@^1.0.2": "npm:uuid62@1.0.2", - "npm:websocket-ts@^2.1.5": "npm:websocket-ts@2.1.5", - "npm:zod@^3.21.0": "npm:zod@3.23.4", - "npm:zod@^3.23.4": "npm:zod@3.23.4", - "npm:zod@^3.23.5": "npm:zod@3.23.5" - }, - "jsr": { - "@bradenmacdonald/s3-lite-client@0.7.4": { - "integrity": "602666ef40d09621d35aa3ea8813e0bfd58b3558e3f0a1d20404b0e61aa0b37e", - "dependencies": [ - "jsr:@std/io@^0.218" - ] - }, - "@db/sqlite@0.11.1": { - "integrity": "546434e7ed762db07e6ade0f963540dd5e06723b802937bf260ff855b21ef9c5", - "dependencies": [ - "jsr:@denosaurs/plug@1", - "jsr:@std/path@0.217" - ] - }, - "@denosaurs/plug@1.0.6": { - "integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7", - "dependencies": [ - "jsr:@std/encoding@^0.221.0", - "jsr:@std/fmt@^0.221.0", - "jsr:@std/fs@^0.221.0", - "jsr:@std/path@^0.221.0" - ] - }, - "@nostrify/nostrify@0.15.0": { - "integrity": "51c2fe9ac7264d22567cd1919a5bf5101a5207f651e65bc00b3de43f9038dfc8", - "dependencies": [ - "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.4" - ] - }, - "@soapbox/kysely-deno-sqlite@2.0.2": { - "integrity": "296f1d6c258b3fa2e8ad51f59782fce0e92549d4cb34ba159a582bcebf35d5e9", - "dependencies": [ - "npm:kysely@^0.27.2" - ] - }, - "@soapbox/stickynotes@0.4.0": { - "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" - }, - "@std/assert@0.217.0": { - "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" - }, - "@std/assert@0.218.2": { - "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" - }, - "@std/assert@0.221.0": { - "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a" - }, - "@std/assert@0.224.0": { - "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" - }, - "@std/bytes@0.218.2": { - "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" - }, - "@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/encoding@0.221.0": { - "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" - }, - "@std/encoding@0.224.0": { - "integrity": "efb6dca97d3e9c31392bd5c8cfd9f9fc9decf5a1f4d1f78af7900a493bcf89b5" - }, - "@std/fmt@0.221.0": { - "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" - }, - "@std/fs@0.221.0": { - "integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286", - "dependencies": [ - "jsr:@std/assert@^0.221.0", - "jsr:@std/path@^0.221.0" - ] - }, - "@std/io@0.218.2": { - "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", - "dependencies": [ - "jsr:@std/assert@^0.218.2", - "jsr:@std/bytes@^0.218.2" - ] - }, - "@std/media-types@0.224.0": { - "integrity": "5ac87989393f8cb1c81bee02aef6f5d4c8289b416deabc04f9ad25dff292d0b0" - }, - "@std/path@0.217.0": { - "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", - "dependencies": [ - "jsr:@std/assert@^0.217.0" - ] - }, - "@std/path@0.221.0": { - "integrity": "0a36f6b17314ef653a3a1649740cc8db51b25a133ecfe838f20b79a56ebe0095", - "dependencies": [ - "jsr:@std/assert@^0.221.0" - ] - } - }, - "npm": { - "@assemblyscript/loader@0.9.4": { - "integrity": "sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA==", - "dependencies": {} - }, - "@babel/code-frame@7.24.2": { - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", - "dependencies": { - "@babel/highlight": "@babel/highlight@7.24.2", - "picocolors": "picocolors@1.0.0" - } - }, - "@babel/helper-validator-identifier@7.22.20": { - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dependencies": {} - }, - "@babel/highlight@7.24.2": { - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", - "dependencies": { - "@babel/helper-validator-identifier": "@babel/helper-validator-identifier@7.22.20", - "chalk": "chalk@2.4.2", - "js-tokens": "js-tokens@4.0.0", - "picocolors": "picocolors@1.0.0" - } - }, - "@isaacs/ttlcache@1.4.1": { - "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", - "dependencies": {} - }, - "@multiformats/base-x@4.0.1": { - "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==", - "dependencies": {} - }, - "@noble/ciphers@0.2.0": { - "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", - "dependencies": {} - }, - "@noble/ciphers@0.5.2": { - "integrity": "sha512-GADtQmZCdgbnNp+daPLc3OY3ibEtGGDV/+CzeM3MFnhiQ7ELQKlsHWYq0YbYUXx4jU3/Y1erAxU6r+hwpewqmQ==", - "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/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/secp256k1@2.1.0": { - "integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==", - "dependencies": {} - }, - "@protobufjs/aspromise@1.1.2": { - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dependencies": {} - }, - "@protobufjs/base64@1.1.2": { - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dependencies": {} - }, - "@protobufjs/codegen@2.0.4": { - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dependencies": {} - }, - "@protobufjs/eventemitter@1.1.0": { - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "dependencies": {} - }, - "@protobufjs/fetch@1.1.0": { - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "@protobufjs/aspromise@1.1.2", - "@protobufjs/inquire": "@protobufjs/inquire@1.1.0" - } - }, - "@protobufjs/float@1.0.2": { - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dependencies": {} - }, - "@protobufjs/inquire@1.1.0": { - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "dependencies": {} - }, - "@protobufjs/path@1.1.2": { - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dependencies": {} - }, - "@protobufjs/pool@1.1.0": { - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dependencies": {} - }, - "@protobufjs/utf8@1.1.0": { - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "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/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/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/lodash@4.14.194": { - "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==", - "dependencies": {} - }, - "@types/long@4.0.2": { - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "dependencies": {} - }, - "@types/mime@3.0.0": { - "integrity": "sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w==", - "dependencies": {} - }, - "@types/minimist@1.2.5": { - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dependencies": {} - }, - "@types/node-forge@1.3.11": { - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dependencies": { - "@types/node": "@types/node@18.16.19" - } - }, - "@types/node@18.16.19": { - "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", - "dependencies": {} - }, - "@types/node@20.12.7": { - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", - "dependencies": { - "undici-types": "undici-types@5.26.5" - } - }, - "@types/normalize-package-data@2.4.4": { - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dependencies": {} - }, - "@types/sanitize-html@2.9.0": { - "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", - "dependencies": { - "htmlparser2": "htmlparser2@8.0.2" - } - }, - "ansi-styles@3.2.1": { - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "color-convert@1.9.3" - } - }, - "arrify@1.0.1": { - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dependencies": {} - }, - "base-x@3.0.9": { - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "dependencies": { - "safe-buffer": "safe-buffer@5.2.1" - } - }, - "base64-js@1.5.1": { - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dependencies": {} - }, - "bl@5.1.0": { - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dependencies": { - "buffer": "buffer@6.0.3", - "inherits": "inherits@2.0.4", - "readable-stream": "readable-stream@3.6.2" - } - }, - "blakejs@1.2.1": { - "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", - "dependencies": {} - }, - "buffer@6.0.3": { - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dependencies": { - "base64-js": "base64-js@1.5.1", - "ieee754": "ieee754@1.2.1" - } - }, - "camelcase-keys@6.2.2": { - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dependencies": { - "camelcase": "camelcase@5.3.1", - "map-obj": "map-obj@4.3.0", - "quick-lru": "quick-lru@4.0.1" - } - }, - "camelcase@5.3.1": { - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dependencies": {} - }, - "chalk@2.4.2": { - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "ansi-styles@3.2.1", - "escape-string-regexp": "escape-string-regexp@1.0.5", - "supports-color": "supports-color@5.5.0" - } - }, - "cids@1.1.9": { - "integrity": "sha512-l11hWRfugIcbGuTZwAM5PwpjPPjyb6UZOGwlHSnOBV5o07XhQ4gNpBN67FbODvpjyHtd+0Xs6KNvUcGBiDRsdg==", - "dependencies": { - "multibase": "multibase@4.0.6", - "multicodec": "multicodec@3.2.1", - "multihashes": "multihashes@4.0.3", - "uint8arrays": "uint8arrays@3.1.1" - } - }, - "color-convert@1.9.3": { - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "color-name@1.1.3" - } - }, - "color-name@1.1.3": { - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dependencies": {} - }, - "comlink@4.4.1": { - "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==", - "dependencies": {} - }, - "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" - } - }, - "decamelize-keys@1.1.1": { - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dependencies": { - "decamelize": "decamelize@1.2.0", - "map-obj": "map-obj@1.0.1" - } - }, - "decamelize@1.2.0": { - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dependencies": {} - }, - "deepmerge@4.3.1": { - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dependencies": {} - }, - "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" - } - }, - "entities@4.5.0": { - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dependencies": {} - }, - "err-code@3.0.1": { - "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", - "dependencies": {} - }, - "error-ex@1.3.2": { - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "is-arrayish@0.2.1" - } - }, - "escape-string-regexp@1.0.5": { - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dependencies": {} - }, - "escape-string-regexp@4.0.0": { - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dependencies": {} - }, - "fast-stable-stringify@1.0.0": { - "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", - "dependencies": {} - }, - "find-up@4.1.0": { - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "locate-path@5.0.0", - "path-exists": "path-exists@4.0.0" - } - }, - "formdata-helper@0.3.0": { - "integrity": "sha512-QkRUFbNgWSu9lkc5TKLWri0ilTFowo950w13I5pRhj4cUxzMLuz0MIhGbE/gIRyfsZQoFeMNN0h06OCSOgfhUg==", - "dependencies": {} - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dependencies": {} - }, - "hamt-sharding@2.0.1": { - "integrity": "sha512-vnjrmdXG9dDs1m/H4iJ6z0JFI2NtgsW5keRkTcM85NGak69Mkf5PHUqBz+Xs0T4sg0ppvj9O5EGAJo40FTxmmA==", - "dependencies": { - "sparse-array": "sparse-array@1.3.2", - "uint8arrays": "uint8arrays@3.1.1" - } - }, - "hard-rejection@2.1.0": { - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dependencies": {} - }, - "has-flag@3.0.0": { - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dependencies": {} - }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "function-bind@1.1.2" - } - }, - "he@1.2.0": { - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dependencies": {} - }, - "hosted-git-info@2.8.9": { - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dependencies": {} - }, - "hosted-git-info@4.1.0": { - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dependencies": { - "lru-cache": "lru-cache@6.0.0" - } - }, - "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" - } - }, - "iconv-lite@0.4.24": { - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": "safer-buffer@2.1.2" - } - }, - "ieee754@1.2.1": { - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dependencies": {} - }, - "indent-string@4.0.0": { - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dependencies": {} - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dependencies": {} - }, - "interface-ipld-format@1.0.1": { - "integrity": "sha512-WV/ar+KQJVoQpqRDYdo7YPGYIUHJxCuOEhdvsRpzLqoOIVCqPKdMMYmsLL1nCRsF3yYNio+PAJbCKiv6drrEAg==", - "dependencies": { - "cids": "cids@1.1.9", - "multicodec": "multicodec@3.2.1", - "multihashes": "multihashes@4.0.3" - } - }, - "ipfs-only-hash@4.0.0": { - "integrity": "sha512-TE1DZCvfw8i3gcsTq3P4TFx3cKFJ3sluu/J3XINkJhIN9OwJgNMqKA+WnKx6ByCb1IoPXsTp1KM7tupElb6SyA==", - "dependencies": { - "ipfs-unixfs-importer": "ipfs-unixfs-importer@7.0.3", - "meow": "meow@9.0.0" - } - }, - "ipfs-unixfs-importer@7.0.3": { - "integrity": "sha512-qeFOlD3AQtGzr90sr5Tq1Bi8pT5Nr2tSI8z310m7R4JDYgZc6J1PEZO3XZQ8l1kuGoqlAppBZuOYmPEqaHcVQQ==", - "dependencies": { - "bl": "bl@5.1.0", - "cids": "cids@1.1.9", - "err-code": "err-code@3.0.1", - "hamt-sharding": "hamt-sharding@2.0.1", - "ipfs-unixfs": "ipfs-unixfs@4.0.3", - "ipld-dag-pb": "ipld-dag-pb@0.22.3", - "it-all": "it-all@1.0.6", - "it-batch": "it-batch@1.0.9", - "it-first": "it-first@1.0.7", - "it-parallel-batch": "it-parallel-batch@1.0.11", - "merge-options": "merge-options@3.0.4", - "multihashing-async": "multihashing-async@2.1.4", - "rabin-wasm": "rabin-wasm@0.1.5", - "uint8arrays": "uint8arrays@2.1.10" - } - }, - "ipfs-unixfs@4.0.3": { - "integrity": "sha512-hzJ3X4vlKT8FQ3Xc4M1szaFVjsc1ZydN+E4VQ91aXxfpjFn9G2wsMo1EFdAXNq/BUnN5dgqIOMP5zRYr3DTsAw==", - "dependencies": { - "err-code": "err-code@3.0.1", - "protobufjs": "protobufjs@6.11.4" - } - }, - "ipld-dag-pb@0.22.3": { - "integrity": "sha512-dfG5C5OVAR4FEP7Al2CrHWvAyIM7UhAQrjnOYOIxXGQz5NlEj6wGX0XQf6Ru6or1na6upvV3NQfstapQG8X2rg==", - "dependencies": { - "cids": "cids@1.1.9", - "interface-ipld-format": "interface-ipld-format@1.0.1", - "multicodec": "multicodec@3.2.1", - "multihashing-async": "multihashing-async@2.1.4", - "protobufjs": "protobufjs@6.11.4", - "stable": "stable@0.1.8", - "uint8arrays": "uint8arrays@2.1.10" - } - }, - "is-arrayish@0.2.1": { - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dependencies": {} - }, - "is-core-module@2.13.1": { - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dependencies": { - "hasown": "hasown@2.0.2" - } - }, - "is-plain-obj@1.1.0": { - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dependencies": {} - }, - "is-plain-obj@2.1.0": { - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dependencies": {} - }, - "is-plain-object@5.0.0": { - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dependencies": {} - }, - "iso-639-1@2.1.15": { - "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==", - "dependencies": {} - }, - "isomorphic-ws@5.0.0_ws@8.16.0": { - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "dependencies": { - "ws": "ws@8.16.0" - } - }, - "it-all@1.0.6": { - "integrity": "sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A==", - "dependencies": {} - }, - "it-batch@1.0.9": { - "integrity": "sha512-7Q7HXewMhNFltTsAMdSz6luNhyhkhEtGGbYek/8Xb/GiqYMtwUmopE1ocPSiJKKp3rM4Dt045sNFoUu+KZGNyA==", - "dependencies": {} - }, - "it-first@1.0.7": { - "integrity": "sha512-nvJKZoBpZD/6Rtde6FXqwDqDZGF1sCADmr2Zoc0hZsIvnE449gRFnGctxDf09Bzc/FWnHXAdaHVIetY6lrE0/g==", - "dependencies": {} - }, - "it-parallel-batch@1.0.11": { - "integrity": "sha512-UWsWHv/kqBpMRmyZJzlmZeoAMA0F3SZr08FBdbhtbe+MtoEBgr/ZUAKrnenhXCBrsopy76QjRH2K/V8kNdupbQ==", - "dependencies": { - "it-batch": "it-batch@1.0.9" - } - }, - "js-sha3@0.8.0": { - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dependencies": {} - }, - "js-tokens@4.0.0": { - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dependencies": {} - }, - "json-parse-even-better-errors@2.3.1": { - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dependencies": {} - }, - "kind-of@6.0.3": { - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dependencies": {} - }, - "kysely@0.27.3": { - "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", - "dependencies": {} - }, - "lines-and-columns@1.2.4": { - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "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": {} - }, - "locate-path@5.0.0": { - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "p-locate@4.1.0" - } - }, - "long@4.0.0": { - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "dependencies": {} - }, - "lru-cache@10.2.0": { - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dependencies": {} - }, - "lru-cache@10.2.2": { - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dependencies": {} - }, - "lru-cache@6.0.0": { - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "yallist@4.0.0" - } - }, - "map-obj@1.0.1": { - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dependencies": {} - }, - "map-obj@4.3.0": { - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dependencies": {} - }, - "meow@9.0.0": { - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dependencies": { - "@types/minimist": "@types/minimist@1.2.5", - "camelcase-keys": "camelcase-keys@6.2.2", - "decamelize": "decamelize@1.2.0", - "decamelize-keys": "decamelize-keys@1.1.1", - "hard-rejection": "hard-rejection@2.1.0", - "minimist-options": "minimist-options@4.1.0", - "normalize-package-data": "normalize-package-data@3.0.3", - "read-pkg-up": "read-pkg-up@7.0.1", - "redent": "redent@3.0.0", - "trim-newlines": "trim-newlines@3.0.1", - "type-fest": "type-fest@0.18.1", - "yargs-parser": "yargs-parser@20.2.9" - } - }, - "merge-options@3.0.4": { - "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", - "dependencies": { - "is-plain-obj": "is-plain-obj@2.1.0" - } - }, - "mime@3.0.0": { - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dependencies": {} - }, - "min-indent@1.0.1": { - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dependencies": {} - }, - "minimist-options@4.1.0": { - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dependencies": { - "arrify": "arrify@1.0.1", - "is-plain-obj": "is-plain-obj@1.1.0", - "kind-of": "kind-of@6.0.3" - } - }, - "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": {} - }, - "multibase@4.0.6": { - "integrity": "sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ==", - "dependencies": { - "@multiformats/base-x": "@multiformats/base-x@4.0.1" - } - }, - "multicodec@3.2.1": { - "integrity": "sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw==", - "dependencies": { - "uint8arrays": "uint8arrays@3.1.1", - "varint": "varint@6.0.0" - } - }, - "multiformats@9.9.0": { - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "dependencies": {} - }, - "multihashes@4.0.3": { - "integrity": "sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA==", - "dependencies": { - "multibase": "multibase@4.0.6", - "uint8arrays": "uint8arrays@3.1.1", - "varint": "varint@5.0.2" - } - }, - "multihashing-async@2.1.4": { - "integrity": "sha512-sB1MiQXPSBTNRVSJc2zM157PXgDtud2nMFUEIvBrsq5Wv96sUclMRK/ecjoP1T/W61UJBqt4tCTwMkUpt2Gbzg==", - "dependencies": { - "blakejs": "blakejs@1.2.1", - "err-code": "err-code@3.0.1", - "js-sha3": "js-sha3@0.8.0", - "multihashes": "multihashes@4.0.3", - "murmurhash3js-revisited": "murmurhash3js-revisited@3.0.0", - "uint8arrays": "uint8arrays@3.1.1" - } - }, - "murmurhash3js-revisited@3.0.0": { - "integrity": "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==", - "dependencies": {} - }, - "nanoid@3.3.7": { - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dependencies": {} - }, - "node-fetch@2.7.0": { - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "whatwg-url@5.0.0" - } - }, - "node-forge@1.3.1": { - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dependencies": {} - }, - "normalize-package-data@2.5.0": { - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "hosted-git-info@2.8.9", - "resolve": "resolve@1.22.8", - "semver": "semver@5.7.2", - "validate-npm-package-license": "validate-npm-package-license@3.0.4" - } - }, - "normalize-package-data@3.0.3": { - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dependencies": { - "hosted-git-info": "hosted-git-info@4.1.0", - "is-core-module": "is-core-module@2.13.1", - "semver": "semver@7.6.0", - "validate-npm-package-license": "validate-npm-package-license@3.0.4" - } - }, - "nostr-relaypool2@0.6.34": { - "integrity": "sha512-e3FDh9w/wQkY513mvoJps1Hc/Y5wiWXeBM6MD+YKSyAg+px+/8uHSSHAuHhlavw7oOEOvEsIGlMDMc57DG3MOA==", - "dependencies": { - "isomorphic-ws": "isomorphic-ws@5.0.0_ws@8.16.0", - "nostr-tools": "nostr-tools@1.17.0", - "safe-stable-stringify": "safe-stable-stringify@2.4.3" - } - }, - "nostr-tools@1.17.0": { - "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", - "dependencies": { - "@noble/ciphers": "@noble/ciphers@0.2.0", - "@noble/curves": "@noble/curves@1.1.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-tools@2.5.1": { - "integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==", - "dependencies": { - "@noble/ciphers": "@noble/ciphers@0.5.2", - "@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": {} - }, - "p-limit@2.3.0": { - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "p-try@2.2.0" - } - }, - "p-locate@4.1.0": { - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "p-limit@2.3.0" - } - }, - "p-try@2.2.0": { - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dependencies": {} - }, - "parse-json@5.2.0": { - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "@babel/code-frame@7.24.2", - "error-ex": "error-ex@1.3.2", - "json-parse-even-better-errors": "json-parse-even-better-errors@2.3.1", - "lines-and-columns": "lines-and-columns@1.2.4" - } - }, - "parse-srcset@1.0.2": { - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "dependencies": {} - }, - "path-exists@4.0.0": { - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dependencies": {} - }, - "path-parse@1.0.7": { - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dependencies": {} - }, - "picocolors@1.0.0": { - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dependencies": {} - }, - "postcss@8.4.38": { - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dependencies": { - "nanoid": "nanoid@3.3.7", - "picocolors": "picocolors@1.0.0", - "source-map-js": "source-map-js@1.2.0" - } - }, - "protobufjs@6.11.4": { - "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", - "dependencies": { - "@protobufjs/aspromise": "@protobufjs/aspromise@1.1.2", - "@protobufjs/base64": "@protobufjs/base64@1.1.2", - "@protobufjs/codegen": "@protobufjs/codegen@2.0.4", - "@protobufjs/eventemitter": "@protobufjs/eventemitter@1.1.0", - "@protobufjs/fetch": "@protobufjs/fetch@1.1.0", - "@protobufjs/float": "@protobufjs/float@1.0.2", - "@protobufjs/inquire": "@protobufjs/inquire@1.1.0", - "@protobufjs/path": "@protobufjs/path@1.1.2", - "@protobufjs/pool": "@protobufjs/pool@1.1.0", - "@protobufjs/utf8": "@protobufjs/utf8@1.1.0", - "@types/long": "@types/long@4.0.2", - "@types/node": "@types/node@20.12.7", - "long": "long@4.0.0" - } - }, - "quick-lru@4.0.1": { - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dependencies": {} - }, - "rabin-wasm@0.1.5": { - "integrity": "sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA==", - "dependencies": { - "@assemblyscript/loader": "@assemblyscript/loader@0.9.4", - "bl": "bl@5.1.0", - "debug": "debug@4.3.4", - "minimist": "minimist@1.2.8", - "node-fetch": "node-fetch@2.7.0", - "readable-stream": "readable-stream@3.6.2" - } - }, - "read-pkg-up@7.0.1": { - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dependencies": { - "find-up": "find-up@4.1.0", - "read-pkg": "read-pkg@5.2.0", - "type-fest": "type-fest@0.8.1" - } - }, - "read-pkg@5.2.0": { - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dependencies": { - "@types/normalize-package-data": "@types/normalize-package-data@2.4.4", - "normalize-package-data": "normalize-package-data@2.5.0", - "parse-json": "parse-json@5.2.0", - "type-fest": "type-fest@0.6.0" - } - }, - "readable-stream@3.6.2": { - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "inherits@2.0.4", - "string_decoder": "string_decoder@1.3.0", - "util-deprecate": "util-deprecate@1.0.2" - } - }, - "redent@3.0.0": { - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dependencies": { - "indent-string": "indent-string@4.0.0", - "strip-indent": "strip-indent@3.0.0" - } - }, - "resolve@1.22.8": { - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "is-core-module@2.13.1", - "path-parse": "path-parse@1.0.7", - "supports-preserve-symlinks-flag": "supports-preserve-symlinks-flag@1.0.0" - } - }, - "safe-buffer@5.2.1": { - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dependencies": {} - }, - "safe-stable-stringify@2.4.3": { - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "dependencies": {} - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dependencies": {} - }, - "sanitize-html@2.13.0": { - "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", - "dependencies": { - "deepmerge": "deepmerge@4.3.1", - "escape-string-regexp": "escape-string-regexp@4.0.0", - "htmlparser2": "htmlparser2@8.0.2", - "is-plain-object": "is-plain-object@5.0.0", - "parse-srcset": "parse-srcset@1.0.2", - "postcss": "postcss@8.4.38" - } - }, - "semver@5.7.2": { - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dependencies": {} - }, - "semver@7.6.0": { - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "lru-cache@6.0.0" - } - }, - "source-map-js@1.2.0": { - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dependencies": {} - }, - "sparse-array@1.3.2": { - "integrity": "sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==", - "dependencies": {} - }, - "spdx-correct@3.2.0": { - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dependencies": { - "spdx-expression-parse": "spdx-expression-parse@3.0.1", - "spdx-license-ids": "spdx-license-ids@3.0.17" - } - }, - "spdx-exceptions@2.5.0": { - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dependencies": {} - }, - "spdx-expression-parse@3.0.1": { - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "spdx-exceptions@2.5.0", - "spdx-license-ids": "spdx-license-ids@3.0.17" - } - }, - "spdx-license-ids@3.0.17": { - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "dependencies": {} - }, - "stable@0.1.8": { - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dependencies": {} - }, - "string_decoder@1.3.0": { - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "safe-buffer@5.2.1" - } - }, - "strip-indent@3.0.0": { - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dependencies": { - "min-indent": "min-indent@1.0.1" - } - }, - "supports-color@5.5.0": { - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "has-flag@3.0.0" - } - }, - "supports-preserve-symlinks-flag@1.0.0": { - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dependencies": {} - }, - "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" - } - }, - "tr46@0.0.3": { - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dependencies": {} - }, - "trim-newlines@3.0.1": { - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dependencies": {} - }, - "tseep@1.2.1": { - "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==", - "dependencies": {} - }, - "type-fest@0.18.1": { - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dependencies": {} - }, - "type-fest@0.6.0": { - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dependencies": {} - }, - "type-fest@0.8.1": { - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dependencies": {} - }, - "type-fest@4.15.0": { - "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==", - "dependencies": {} - }, - "uint8arrays@2.1.10": { - "integrity": "sha512-Q9/hhJa2836nQfEJSZTmr+pg9+cDJS9XEAp7N2Vg5MzL3bK/mkMVfjscRGYruP9jNda6MAdf4QD/y78gSzkp6A==", - "dependencies": { - "multiformats": "multiformats@9.9.0" - } - }, - "uint8arrays@3.1.1": { - "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", - "dependencies": { - "multiformats": "multiformats@9.9.0" - } - }, - "undici-types@5.26.5": { - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "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" - } - }, - "util-deprecate@1.0.2": { - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dependencies": {} - }, - "uuid62@1.0.2": { - "integrity": "sha512-vI7jxJboVd6eFRpyZn5ONx5DAQgu7hO0TcE6Qy+riw/XSw8A8+qc3SplJPZ9+nKqlAuN7RMriSn2ehMWeIPCiA==", - "dependencies": { - "base-x": "base-x@3.0.9", - "buffer": "buffer@6.0.3", - "uuid": "uuid@8.3.2" - } - }, - "uuid@8.3.2": { - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dependencies": {} - }, - "validate-npm-package-license@3.0.4": { - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "spdx-correct@3.2.0", - "spdx-expression-parse": "spdx-expression-parse@3.0.1" - } - }, - "varint@5.0.2": { - "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==", - "dependencies": {} - }, - "varint@6.0.0": { - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "dependencies": {} - }, - "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" - } - }, - "ws@8.16.0": { - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "dependencies": {} - }, - "yallist@4.0.0": { - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dependencies": {} - }, - "yargs-parser@20.2.9": { - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dependencies": {} - }, - "zod@3.23.4": { - "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", - "dependencies": {} - }, - "zod@3.23.5": { - "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==", - "dependencies": {} - } - } - }, - "redirects": { - "https://esm.sh/v135/@types/lodash@4.17.0/index": "https://esm.sh/v135/@types/lodash@4.17.0/index~.d.ts", - "https://esm.sh/v135/@types/lodash@~4.17/index.d.ts": "https://esm.sh/v135/@types/lodash@4.17.0/index.d.ts" - }, - "remote": { - "https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", - "https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", - "https://deno.land/std@0.160.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", - "https://deno.land/std@0.160.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", - "https://deno.land/std@0.160.0/async/debounce.ts": "dc8b92d4a4fe7eac32c924f2b8d3e62112530db70cadce27042689d82970b350", - "https://deno.land/std@0.160.0/async/deferred.ts": "d8fb253ffde2a056e4889ef7e90f3928f28be9f9294b6505773d33f136aab4e6", - "https://deno.land/std@0.160.0/async/delay.ts": "0419dfc993752849692d1f9647edf13407c7facc3509b099381be99ffbc9d699", - "https://deno.land/std@0.160.0/async/mod.ts": "dd0a8ed4f3984ffabe2fcca7c9f466b7932d57b1864ffee148a5d5388316db6b", - "https://deno.land/std@0.160.0/async/mux_async_iterator.ts": "3447b28a2a582224a3d4d3596bccbba6e85040da3b97ed64012f7decce98d093", - "https://deno.land/std@0.160.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239", - "https://deno.land/std@0.160.0/async/tee.ts": "9af3a3e7612af75861308b52249e167f5ebc3dcfc8a1a4d45462d96606ee2b70", - "https://deno.land/std@0.160.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4", - "https://deno.land/std@0.160.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a", - "https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179", - "https://deno.land/std@0.160.0/crypto/_fnv/fnv32.ts": "aa9bddead8c6345087d3abd4ef35fb9655622afc333fc41fff382b36e64280b5", - "https://deno.land/std@0.160.0/crypto/_fnv/fnv64.ts": "625d7e7505b6cb2e9801b5fd6ed0a89256bac12b2bbb3e4664b85a88b0ec5bef", - "https://deno.land/std@0.160.0/crypto/_fnv/index.ts": "a8f6a361b4c6d54e5e89c16098f99b6962a1dd6ad1307dbc97fa1ecac5d7060a", - "https://deno.land/std@0.160.0/crypto/_fnv/util.ts": "4848313bed7f00f55be3cb080aa0583fc007812ba965b03e4009665bde614ce3", - "https://deno.land/std@0.160.0/crypto/_wasm_crypto/lib/deno_std_wasm_crypto.generated.mjs": "258b484c2da27578bec61c01d4b62c21f72268d928d03c968c4eb590cb3bd830", - "https://deno.land/std@0.160.0/crypto/_wasm_crypto/mod.ts": "6c60d332716147ded0eece0861780678d51b560f533b27db2e15c64a4ef83665", - "https://deno.land/std@0.160.0/crypto/keystack.ts": "e481eed28007395e554a435e880fee83a5c73b9259ed8a135a75e4b1e4f381f7", - "https://deno.land/std@0.160.0/crypto/mod.ts": "fadedc013b4a86fda6305f1adc6d1c02225834d53cff5d95cc05f62b25127517", - "https://deno.land/std@0.160.0/crypto/timing_safe_equal.ts": "82a29b737bc8932d75d7a20c404136089d5d23629e94ba14efa98a8cc066c73e", - "https://deno.land/std@0.160.0/datetime/formatter.ts": "7c8e6d16a0950f400aef41b9f1eb9168249869776ec520265dfda785d746589e", - "https://deno.land/std@0.160.0/datetime/mod.ts": "ea927ca96dfb28c7b9a5eed5bdc7ac46bb9db38038c4922631895cea342fea87", - "https://deno.land/std@0.160.0/datetime/tokenizer.ts": "7381e28f6ab51cb504c7e132be31773d73ef2f3e1e50a812736962b9df1e8c47", - "https://deno.land/std@0.160.0/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2", - "https://deno.land/std@0.160.0/encoding/base64url.ts": "a5f82a9fa703bd85a5eb8e7c1296bc6529e601ebd9642cc2b5eaa6b38fa9e05a", - "https://deno.land/std@0.160.0/encoding/hex.ts": "4cc5324417cbb4ac9b828453d35aed45b9cc29506fad658f1f138d981ae33795", - "https://deno.land/std@0.160.0/fmt/colors.ts": "9e36a716611dcd2e4865adea9c4bec916b5c60caad4cdcdc630d4974e6bb8bd4", - "https://deno.land/std@0.160.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289", - "https://deno.land/std@0.160.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", - "https://deno.land/std@0.160.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", - "https://deno.land/std@0.160.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", - "https://deno.land/std@0.160.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", - "https://deno.land/std@0.160.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", - "https://deno.land/std@0.160.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac", - "https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24", - "https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", - "https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d", - "https://deno.land/std@0.160.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", - "https://deno.land/std@0.160.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", - "https://deno.land/std@0.160.0/testing/asserts.ts": "1e340c589853e82e0807629ba31a43c84ebdcdeca910c4a9705715dfdb0f5ce8", - "https://deno.land/std@0.176.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", - "https://deno.land/std@0.176.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", - "https://deno.land/std@0.176.0/encoding/hex.ts": "50f8c95b52eae24395d3dfcb5ec1ced37c5fe7610ef6fffdcc8b0fdc38e3b32f", - "https://deno.land/std@0.176.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471", - "https://deno.land/std@0.176.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", - "https://deno.land/std@0.176.0/fs/copy.ts": "14214efd94fc3aa6db1e4af2b4b9578e50f7362b7f3725d5a14ad259a5df26c8", - "https://deno.land/std@0.176.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", - "https://deno.land/std@0.176.0/fs/ensure_dir.ts": "724209875497a6b4628dfb256116e5651c4f7816741368d6c44aab2531a1e603", - "https://deno.land/std@0.176.0/fs/ensure_file.ts": "c38602670bfaf259d86ca824a94e6cb9e5eb73757fefa4ebf43a90dd017d53d9", - "https://deno.land/std@0.176.0/fs/ensure_link.ts": "c0f5b2f0ec094ed52b9128eccb1ee23362a617457aa0f699b145d4883f5b2fb4", - "https://deno.land/std@0.176.0/fs/ensure_symlink.ts": "2955cc8332aeca9bdfefd05d8d3976b94e282b0f353392a71684808ed2ffdd41", - "https://deno.land/std@0.176.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", - "https://deno.land/std@0.176.0/fs/exists.ts": "b8c8a457b71e9d7f29b9d2f87aad8dba2739cbe637e8926d6ba6e92567875f8e", - "https://deno.land/std@0.176.0/fs/expand_glob.ts": "45d17e89796a24bd6002e4354eda67b4301bb8ba67d2cac8453cdabccf1d9ab0", - "https://deno.land/std@0.176.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", - "https://deno.land/std@0.176.0/fs/move.ts": "4cb47f880e3f0582c55e71c9f8b1e5e8cfaacb5e84f7390781dd563b7298ec19", - "https://deno.land/std@0.176.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", - "https://deno.land/std@0.176.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.176.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.176.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", - "https://deno.land/std@0.176.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", - "https://deno.land/std@0.176.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", - "https://deno.land/std@0.176.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", - "https://deno.land/std@0.176.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", - "https://deno.land/std@0.176.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", - "https://deno.land/std@0.176.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", - "https://deno.land/std@0.179.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", - "https://deno.land/std@0.179.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", - "https://deno.land/std@0.179.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.179.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.179.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", - "https://deno.land/std@0.179.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", - "https://deno.land/std@0.179.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", - "https://deno.land/std@0.179.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", - "https://deno.land/std@0.179.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", - "https://deno.land/std@0.179.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", - "https://deno.land/std@0.179.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", - "https://deno.land/std@0.190.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", - "https://deno.land/std@0.190.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", - "https://deno.land/std@0.190.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab", - "https://deno.land/std@0.190.0/streams/readable_stream_from_iterable.ts": "cd4bb9e9bf6dbe84c213beb1f5085c326624421671473e410cfaecad15f01865", - "https://deno.land/std@0.198.0/_util/diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", - "https://deno.land/std@0.198.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", - "https://deno.land/std@0.198.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", - "https://deno.land/std@0.198.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.198.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", - "https://deno.land/std@0.198.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", - "https://deno.land/std@0.198.0/assert/assert_equals.ts": "a0ee60574e437bcab2dcb79af9d48dc88845f8fd559468d9c21b15fd638ef943", - "https://deno.land/std@0.198.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", - "https://deno.land/std@0.198.0/assert/assert_false.ts": "a9962749f4bf5844e3fa494257f1de73d69e4fe0e82c34d0099287552163a2dc", - "https://deno.land/std@0.198.0/assert/assert_instance_of.ts": "09fd297352a5b5bbb16da2b5e1a0d8c6c44da5447772648622dcc7df7af1ddb8", - "https://deno.land/std@0.198.0/assert/assert_is_error.ts": "b4eae4e5d182272efc172bf28e2e30b86bb1650cd88aea059e5d2586d4160fb9", - "https://deno.land/std@0.198.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", - "https://deno.land/std@0.198.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", - "https://deno.land/std@0.198.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", - "https://deno.land/std@0.198.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", - "https://deno.land/std@0.198.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", - "https://deno.land/std@0.198.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", - "https://deno.land/std@0.198.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", - "https://deno.land/std@0.198.0/assert/assert_strict_equals.ts": "5cf29b38b3f8dece95287325723272aa04e04dbf158d886d662fa594fddc9ed3", - "https://deno.land/std@0.198.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", - "https://deno.land/std@0.198.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", - "https://deno.land/std@0.198.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", - "https://deno.land/std@0.198.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", - "https://deno.land/std@0.198.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", - "https://deno.land/std@0.198.0/assert/mod.ts": "08d55a652c22c5da0215054b21085cec25a5da47ce4a6f9de7d9ad36df35bdee", - "https://deno.land/std@0.198.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", - "https://deno.land/std@0.198.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", - "https://deno.land/std@0.198.0/collections/filter_values.ts": "16e1fc456a7969e770ec5b89edf5ac97b295ca534b47c1a83f061b409aad7814", - "https://deno.land/std@0.198.0/collections/without_all.ts": "1e3cccb1ed0659455b473c0766d9414b7710d8cef48862c899f445178f66b779", - "https://deno.land/std@0.198.0/dotenv/mod.ts": "ff7acf1c97ba57af512ecb6f9094fa96e1f63cca1960a7687616fa86bab7e356", - "https://deno.land/std@0.198.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc", - "https://deno.land/x/deno_cron@v1.0.0/cron.ts": "7f984d0c4c7ac4fb1ad3cd241d457e7808a9362735d910abb02dc689883ee3ef", - "https://deno.land/x/hono@v3.10.1/adapter/deno/serve-static.ts": "ba10cf6aaf39da942b0d49c3b9877ddba69d41d414c6551d890beb1085f58eea", - "https://deno.land/x/hono@v3.10.1/client/client.ts": "ff340f58041203879972dd368b011ed130c66914f789826610869a90603406bf", - "https://deno.land/x/hono@v3.10.1/client/index.ts": "3ff4cf246f3543f827a85a2c84d66a025ac350ee927613629bda47e854bfb7ba", - "https://deno.land/x/hono@v3.10.1/client/types.ts": "52c66cbe74540e1811259a48c30622ac915666196eb978092d166435cbc15213", - "https://deno.land/x/hono@v3.10.1/client/utils.ts": "053273c002963b549d38268a1b423ac8ca211a8028bdab1ed0b781a62aa5e661", - "https://deno.land/x/hono@v3.10.1/compose.ts": "e8ab4b345aa367f2dd65f221c9fe829dd885326a613f4215b654f93a4066bb5c", - "https://deno.land/x/hono@v3.10.1/context.ts": "261cc8b8b1e8f04b98beab1cca6692f317b7dc6d2b75b4f84c982e54cf1db730", - "https://deno.land/x/hono@v3.10.1/helper/adapter/index.ts": "eea9b4caedbfa3a3b4a020bf46c88c0171a00008cd6c10708cd85a3e39d86e62", - "https://deno.land/x/hono@v3.10.1/helper/cookie/index.ts": "55ccd20bbd8d9a8bb2ecd998e90845c1d306c19027f54b3d1b89a5be35968b80", - "https://deno.land/x/hono@v3.10.1/helper/html/index.ts": "aba19e8d29f217c7fffa5719cf606c4e259b540d51296e82bbea3c992e2ecbc6", - "https://deno.land/x/hono@v3.10.1/hono-base.ts": "cc55e0a4c63a7bdf44df3e804ea4737d5399eeb6606b45d102f8e48c3ff1e925", - "https://deno.land/x/hono@v3.10.1/hono.ts": "2cc4c292e541463a4d6f83edbcea58048d203e9564ae62ec430a3d466b49a865", - "https://deno.land/x/hono@v3.10.1/http-exception.ts": "6071df078b5f76d279684d52fe82a590f447a64ffe1b75eb5064d0c8a8d2d676", - "https://deno.land/x/hono@v3.10.1/jsx/index.ts": "019512d3a9b3897b879e87fa5fb179cd34f3d326f8ff8b93379c2bb707ec168a", - "https://deno.land/x/hono@v3.10.1/jsx/intrinsic-elements.ts": "03250beb610bda1c72017bc0912c2505ff764b7a8d869e7e4add40eb4cfec096", - "https://deno.land/x/hono@v3.10.1/jsx/streaming.ts": "5d03b4d02eaa396c8f0f33c3f6e8c7ed3afb7598283c2d4a7ddea0ada8c212a7", - "https://deno.land/x/hono@v3.10.1/middleware.ts": "57b2047c4b9d775a052a9c44a3b805802c1d1cb477ab9c4bb6185d27382d1b96", - "https://deno.land/x/hono@v3.10.1/middleware/basic-auth/index.ts": "5505288ccf9364f56f7be2dfac841543b72e20656e54ac646a1a73a0aa853261", - "https://deno.land/x/hono@v3.10.1/middleware/bearer-auth/index.ts": "d11fe14e0a3006f6d35c391e455fe20d8ece9561e48b6a5580e4b87dd491cd90", - "https://deno.land/x/hono@v3.10.1/middleware/cache/index.ts": "9e5d31d33206bb5dba46dde16ed606dd2cb361d75c26b02e02c72bd1fb6fe53e", - "https://deno.land/x/hono@v3.10.1/middleware/compress/index.ts": "85d315c9a942d7758e5c524dc94b736124646a56752e56c6e4284f3989b4692a", - "https://deno.land/x/hono@v3.10.1/middleware/cors/index.ts": "d481eba7e05d3448cd326d3dca8b9c7e16ecf0d27a37fd7d700485834123ae5e", - "https://deno.land/x/hono@v3.10.1/middleware/etag/index.ts": "4ad675e108dc98dccca0e9e35cd903701669a1aea676b8b51266c3b602e4d54c", - "https://deno.land/x/hono@v3.10.1/middleware/jsx-renderer/index.ts": "5352d6dda872d419ebafbd4d6b408f66ad473fc3d395d82327850c1e786d7344", - "https://deno.land/x/hono@v3.10.1/middleware/jwt/index.ts": "c6e02a94a3911299d21392b3b1f8710bda7cacf0d60db59c0e2f0d9fa8ff1a70", - "https://deno.land/x/hono@v3.10.1/middleware/logger/index.ts": "c139f372f482baeffbad68b14bef990e011fe8df578dcee71fb612ffad7fe748", - "https://deno.land/x/hono@v3.10.1/middleware/powered-by/index.ts": "c36b7a3d1322c6a37f3d1510f7ff04a85aa6cacfac2173e5f1913eb16c3cc869", - "https://deno.land/x/hono@v3.10.1/middleware/pretty-json/index.ts": "f6967ceecdb42c95ddd5e2e7bc8545d3e8bda111fa659f3f1336b2e6fe6b0bb0", - "https://deno.land/x/hono@v3.10.1/middleware/secure-headers/index.ts": "d2b8a7978e3d201ead5ac8fd22e3adc9094189aebcba0d9cd51b98773927a5d5", - "https://deno.land/x/hono@v3.10.1/middleware/timing/index.ts": "d6976a07d9d51a7b26dae1311fe51d0744f7d234498bac3fe024ec7088c0ca47", - "https://deno.land/x/hono@v3.10.1/mod.ts": "90114a97be9111b348129ad0143e764a64921f60dd03b8f3da529db98a0d3a82", - "https://deno.land/x/hono@v3.10.1/request.ts": "52330303dd7a3bf4f580fde0463ba608bc4c88a8b7b5edd7c1327064c7cf65ce", - "https://deno.land/x/hono@v3.10.1/router.ts": "39d573f48baee429810cd583c931dd44274273c30804d538c86967d310ea4ab5", - "https://deno.land/x/hono@v3.10.1/router/linear-router/index.ts": "8a2a7144c50b1f4a92d9ee99c2c396716af144c868e10608255f969695efccd0", - "https://deno.land/x/hono@v3.10.1/router/linear-router/router.ts": "bc63e8b5bc1dabc815306d50bebd1bb5877ffa3936ba2ad7550d093c95ee6bd1", - "https://deno.land/x/hono@v3.10.1/router/pattern-router/index.ts": "304a66c50e243872037ed41c7dd79ed0c89d815e17e172e7ad7cdc4bc07d3383", - "https://deno.land/x/hono@v3.10.1/router/pattern-router/router.ts": "a9a5a2a182cce8c3ae82139892cc0502be7dd8f579f31e76d0302b19b338e548", - "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db", - "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/node.ts": "5b3fb80411db04c65df066e69fedb2c8c0844753c2633d703336de84d569252c", - "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/router.ts": "fbe8917aa24fe25d0208bfa82ce7f49ba0507f9ae158d4d0c177f6a061b0a561", - "https://deno.land/x/hono@v3.10.1/router/reg-exp-router/trie.ts": "852ce7207e6701e47fa30889a0d2b8bfcd56d8862c97e7bc9831e0a64bd8835f", - "https://deno.land/x/hono@v3.10.1/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef", - "https://deno.land/x/hono@v3.10.1/router/smart-router/router.ts": "71979c06b32b093960a6e8efc4c185e558f280bff18846b8b1cdc757ade6ff99", - "https://deno.land/x/hono@v3.10.1/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41", - "https://deno.land/x/hono@v3.10.1/router/trie-router/node.ts": "3af15fa9c9994a8664a2b7a7c11233504b5bb9d4fcf7bb34cf30d7199052c39f", - "https://deno.land/x/hono@v3.10.1/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d", - "https://deno.land/x/hono@v3.10.1/types.ts": "edc414a92383f9deb82f5f7a09e95bcf76f6100c23457c27d041986768f5345c", - "https://deno.land/x/hono@v3.10.1/utils/body.ts": "7a16a6656331a96bcae57642f8d5e3912bd361cbbcc2c0d2157ecc3f218f7a92", - "https://deno.land/x/hono@v3.10.1/utils/buffer.ts": "9066a973e64498cb262c7e932f47eed525a51677b17f90893862b7279dc0773e", - "https://deno.land/x/hono@v3.10.1/utils/cookie.ts": "19920ba6756944aae1ad8585c3ddeaa9df479733f59d05359db096f7361e5e4b", - "https://deno.land/x/hono@v3.10.1/utils/crypto.ts": "bda0e141bbe46d3a4a20f8fbcb6380d473b617123d9fdfa93e4499410b537acc", - "https://deno.land/x/hono@v3.10.1/utils/encode.ts": "3b7c7d736123b5073542b34321700d4dbf5ff129c138f434bb2144a4d425ee89", - "https://deno.land/x/hono@v3.10.1/utils/filepath.ts": "18461b055a914d6da85077f453051b516281bb17cf64fa74bf5ef604dc9d2861", - "https://deno.land/x/hono@v3.10.1/utils/html.ts": "01c1520a4256f899da1954357cf63ae11c348eda141a505f72d7090cf5481aba", - "https://deno.land/x/hono@v3.10.1/utils/http-status.ts": "e0c4343ea7717c314dc600131e16b636c29d61cfdaf9df93b267258d1729d1a0", - "https://deno.land/x/hono@v3.10.1/utils/jwt/index.ts": "5e4b82a42eb3603351dfce726cd781ca41cb57437395409d227131aec348d2d5", - "https://deno.land/x/hono@v3.10.1/utils/jwt/jwt.ts": "02ff7bbf1298ffcc7a40266842f8eac44b6c136453e32d4441e24d0cbfba3a95", - "https://deno.land/x/hono@v3.10.1/utils/jwt/types.ts": "58ddf908f76ba18d9c62ddfc2d1e40cc2e306bf987409a6169287efa81ce2546", - "https://deno.land/x/hono@v3.10.1/utils/mime.ts": "0105d2b5e8e91f07acc70f5d06b388313995d62af23c802fcfba251f5a744d95", - "https://deno.land/x/hono@v3.10.1/utils/stream.ts": "1789dcc73c5b0ede28f83d7d34e47ae432c20e680907cb3275a9c9187f293983", - "https://deno.land/x/hono@v3.10.1/utils/types.ts": "ddff055e6d35066232efdfbd42c8954e855c04279c27dcd735d929b6b4f319b3", - "https://deno.land/x/hono@v3.10.1/utils/url.ts": "5fc3307ef3cb2e6f34ec2a03e3d7f2126c6a9f5f0eab677222df3f0e40bd7567", - "https://deno.land/x/hono@v3.10.1/validator/index.ts": "6c986e8b91dcf857ecc8164a506ae8eea8665792a4ff7215471df669c632ae7c", - "https://deno.land/x/hono@v3.10.1/validator/validator.ts": "afa5e52495e0996fbba61996736fab5c486590d72d376f809e9f9ff4e0c463e9", - "https://deno.land/x/kysely_deno_postgres@v0.4.0/deps.ts": "7970f66a52a9fa0cef607cb7ef0171212af2ccb83e73ecfa7629aabc28a38793", - "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts": "662438fd3909984bb8cbaf3fd44d2121e949d11301baf21d6c3f057ccf9887de", - "https://deno.land/x/kysely_deno_postgres@v0.4.0/src/PostgreSQLDriver.ts": "590c2fa248cff38e6e0f623050983039b5fde61e9c7131593d2922fb1f0eb921", - "https://deno.land/x/kysely_deno_postgres@v0.4.0/src/PostgreSQLDriverDatabaseConnection.ts": "83cd176ca830407dbff8495140cba870d1a34b27075c91ef1d5dbf7bbe467c40", - "https://deno.land/x/pentagon@v0.1.4/deps.ts": "486904dff4ea0275059d7fa42ad7da6025eeced0c7885befa3354da3b9522dda", - "https://deno.land/x/pentagon@v0.1.4/mod.ts": "2a3226e25d9142c24cb93bc1c88b459c06d7b031643c4fb1969fe88d4f6b63b0", - "https://deno.land/x/pentagon@v0.1.4/src/batchOperations.ts": "0c9f410623e7f881d6cc4ba28e51e650f07120f8b86c53f2532d06efdc466a87", - "https://deno.land/x/pentagon@v0.1.4/src/crud.ts": "c9e924fe5a2dd1f37c3c3716c255c5f3d591da2a1927509e4275e74708fa1c6b", - "https://deno.land/x/pentagon@v0.1.4/src/errors.ts": "2d311e95b68a42b5a53528b6171acea1a3ba27e52d7cacdab0e5c54304d97394", - "https://deno.land/x/pentagon@v0.1.4/src/keys.ts": "82bc85bbca3425e8bdb5b19dc48997dc154c173b1462f9aafd9630ab639d1762", - "https://deno.land/x/pentagon@v0.1.4/src/pentagon.ts": "d603e704c7299a207373ddb2a528718c0adfa5f4c3a2ffdbd34ed7d11f44f96d", - "https://deno.land/x/pentagon@v0.1.4/src/relation.ts": "ed6da1fb9e521c09ef9fe29170ce78682bcc788bf41f0e11d031983aa1279f49", - "https://deno.land/x/pentagon@v0.1.4/src/search.ts": "ec1bb39df1e8bd551bcd22978cbd78466522f0b1a5b0002a38f5293a25ac272f", - "https://deno.land/x/pentagon@v0.1.4/src/types.ts": "f2a16e11eb9a724627a9b4532bc95f91a27a43d3d4aade14a947fcdc8cab671a", - "https://deno.land/x/pentagon@v0.1.4/src/util.ts": "a601821f1ee32209a1dd4c094ef541a6990755424f31fc490352e340b19798e9", - "https://deno.land/x/plug@1.0.1/deps.ts": "35ea2acd5e3e11846817a429b7ef4bec47b80f2d988f5d63797147134cbd35c2", - "https://deno.land/x/plug@1.0.1/download.ts": "8d6a023ade0806a0653b48cd5f6f8b15fcfaa1dbf2aa1f4bc90fc5732d27b144", - "https://deno.land/x/plug@1.0.1/mod.ts": "5dec80ee7a3a325be45c03439558531bce7707ac118f4376cebbd6740ff24bfb", - "https://deno.land/x/plug@1.0.1/types.ts": "d8eb738fc6ed883e6abf77093442c2f0b71af9090f15c7613621d4039e410ee1", - "https://deno.land/x/plug@1.0.1/util.ts": "5ba8127b9adc36e070b9e22971fb8106869eea1741f452a87b4861e574f13481", - "https://deno.land/x/postgres@v0.17.0/client.ts": "348779c9f6a1c75ef1336db662faf08dce7d2101ff72f0d1e341ba1505c8431d", - "https://deno.land/x/postgres@v0.17.0/client/error.ts": "0817583b666fd546664ed52c1d37beccc5a9eebcc6e3c2ead20ada99b681e5f7", - "https://deno.land/x/postgres@v0.17.0/connection/auth.ts": "1070125e2ac4ca4ade36d69a4222d37001903092826d313217987583edd61ce9", - "https://deno.land/x/postgres@v0.17.0/connection/connection.ts": "428ed3efa055870db505092b5d3545ef743497b7b4b72cf8f0593e7dd4788acd", - "https://deno.land/x/postgres@v0.17.0/connection/connection_params.ts": "52bfe90e8860f584b95b1b08c254dde97c3aa763c4b6bee0c80c5930e35459e0", - "https://deno.land/x/postgres@v0.17.0/connection/message.ts": "f9257948b7f87d58bfbfe3fc6e2e08f0de3ef885655904d56a5f73655cc22c5a", - "https://deno.land/x/postgres@v0.17.0/connection/message_code.ts": "466719008b298770c366c5c63f6cf8285b7f76514dadb4b11e7d9756a8a1ddbf", - "https://deno.land/x/postgres@v0.17.0/connection/packet.ts": "050aeff1fc13c9349e89451a155ffcd0b1343dc313a51f84439e3e45f64b56c8", - "https://deno.land/x/postgres@v0.17.0/connection/scram.ts": "0c7a2551fe7b1a1c62dd856b7714731a7e7534ccca10093336782d1bfc5b2bd2", - "https://deno.land/x/postgres@v0.17.0/deps.ts": "f47ccb41f7f97eaad455d94f407ef97146ae99443dbe782894422c869fbba69e", - "https://deno.land/x/postgres@v0.17.0/mod.ts": "a1e18fd9e6fedc8bc24e5aeec3ae6de45e2274be1411fb66e9081420c5e81d7d", - "https://deno.land/x/postgres@v0.17.0/pool.ts": "892db7b5e1787988babecc994a151ebbd7d017f080905cbe9c3d7b44a73032a9", - "https://deno.land/x/postgres@v0.17.0/query/array_parser.ts": "f8a229d82c3801de8266fa2cc4afe12e94fef8d0c479e73655c86ed3667ef33f", - "https://deno.land/x/postgres@v0.17.0/query/decode.ts": "44a4a6cbcf494ed91a4fecae38a57dce63a7b519166f02c702791d9717371419", - "https://deno.land/x/postgres@v0.17.0/query/decoders.ts": "16cb0e60227d86692931e315421b15768c78526e3aeb84e25fcc4111096de9fd", - "https://deno.land/x/postgres@v0.17.0/query/encode.ts": "5f1418a2932b7c2231556e4a5f5f56efef48728014070cfafe7656963f342933", - "https://deno.land/x/postgres@v0.17.0/query/oid.ts": "8c33e1325f34e4ca9f11a48b8066c8cfcace5f64bc1eb17ad7247af4936999e1", - "https://deno.land/x/postgres@v0.17.0/query/query.ts": "edb473cbcfeff2ee1c631272afb25d079d06b66b5853f42492725b03ffa742b6", - "https://deno.land/x/postgres@v0.17.0/query/transaction.ts": "8e75c3ce0aca97da7fe126e68f8e6c08d640e5c8d2016e62cee5c254bebe7fe8", - "https://deno.land/x/postgres@v0.17.0/query/types.ts": "a6dc8024867fe7ccb0ba4b4fa403ee5d474c7742174128c8e689c3b5e5eaa933", - "https://deno.land/x/postgres@v0.17.0/utils/deferred.ts": "dd94f2a57355355c47812b061a51b55263f72d24e9cb3fdb474c7519f4d61083", - "https://deno.land/x/postgres@v0.17.0/utils/utils.ts": "19c3527ddd5c6c4c49ae36397120274c7f41f9d3cbf479cb36065d23329e9f90", - "https://deno.land/x/s3_lite_client@0.6.1/client.ts": "d4c93fe2dbd19d0c570c8661e1971051a4e3a5f74c30122fc1ed5ee44cadaac4", - "https://deno.land/x/s3_lite_client@0.6.1/deps.ts": "cfa4510116af915b090db6789035b89fbd34fd8a6ff6b1389650401a1d794962", - "https://deno.land/x/s3_lite_client@0.6.1/errors.ts": "3dd431b0e96f346104d7be6c09e1659b5c360992e6487e35bacb881f10c5a5bf", - "https://deno.land/x/s3_lite_client@0.6.1/helpers.ts": "6ba450312f54873805390cc7a11e61a7886dc00633f2ed20d941606568527332", - "https://deno.land/x/s3_lite_client@0.6.1/mod.ts": "4a896cad948ae36e35a5025eff92a97366059fe8e01bb109df3889666c88bd1d", - "https://deno.land/x/s3_lite_client@0.6.1/object-uploader.ts": "b4bad0d771d79b2bb23b8cab0e6f7be85a2390e18957c612fd5cda11c39f55b0", - "https://deno.land/x/s3_lite_client@0.6.1/signing.ts": "2ba77aac07a7c94267e83d285bbd33fdb3253dfa32b035df62479d6b224bb748", - "https://deno.land/x/s3_lite_client@0.6.1/transform-chunk-sizes.ts": "cecc1167ba366d086a13c754be6ed86717d6b0b27c779c4c766621435a697045", - "https://deno.land/x/s3_lite_client@0.6.1/xml-parser.ts": "de925493369718cab6f26413fbbada18eec74aa6eaf0598d77c7296f5fdfd8a9", - "https://deno.land/x/scoped_performance@v2.0.0/mod.ts": "c874aa244e9b2c585759d716b86735bd78fbd82e0e0b94df0a3f5856bbcacb73", - "https://deno.land/x/scoped_performance@v2.0.0/src/scoped-performance.ts": "c0194251ff4a758bf9af29edef64d00926b14e8e51f6a279a83e005428c21eb3", - "https://deno.land/x/sentry@7.112.2/index.mjs": "04382d5c2f4e233ba389611db46f77943b2a7f6efbeaaf31193f6e586f4366ef", - "https://deno.land/x/sqlite3@0.9.1/deno.json": "50895b0bb0c13ae38b93413d7f9f62652f6e7076cd99b9876f6b3b7f6c488dca", - "https://deno.land/x/sqlite3@0.9.1/deps.ts": "f6035f0884a730c0d55b0cdce68846f13bbfc14e8afbf0b3cd4f12a52b4107b7", - "https://deno.land/x/sqlite3@0.9.1/mod.ts": "d41b8b30e1b20b537ef4d78cae98d90f6bd65c727b64aa1a18bffbb28f7d6ec3", - "https://deno.land/x/sqlite3@0.9.1/src/blob.ts": "3681353b3c97bc43f9b02f8d1c3269c0dc4eb9cb5d3af16c7ce4d1e1ec7507c4", - "https://deno.land/x/sqlite3@0.9.1/src/constants.ts": "85fd27aa6e199093f25f5f437052e16fd0e0870b96ca9b24a98e04ddc8b7d006", - "https://deno.land/x/sqlite3@0.9.1/src/database.ts": "c326446463955f276dcbe18547ede4b19ea3085bef0980548c0a58d830b3b5d9", - "https://deno.land/x/sqlite3@0.9.1/src/ffi.ts": "b83f6d16179be7a97a298d6e8172941dbf532058e7c2b3df3a708beefe285c90", - "https://deno.land/x/sqlite3@0.9.1/src/statement.ts": "4773bc8699a9084b93e65126cd5f9219c248de1fce447270bdae2c3630637150", - "https://deno.land/x/sqlite3@0.9.1/src/util.ts": "3892904eb057271d4072215c3e7ffe57a9e59e4df78ac575046eb278ca6239cd", - "https://deno.land/x/zod@v3.21.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea", - "https://deno.land/x/zod@v3.21.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", - "https://deno.land/x/zod@v3.21.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", - "https://deno.land/x/zod@v3.21.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", - "https://deno.land/x/zod@v3.21.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", - "https://deno.land/x/zod@v3.21.4/helpers/parseUtil.ts": "51a76c126ee212be86013d53a9d07f87e9ae04bb1496f2558e61b62cb74a6aa8", - "https://deno.land/x/zod@v3.21.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", - "https://deno.land/x/zod@v3.21.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", - "https://deno.land/x/zod@v3.21.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774", - "https://deno.land/x/zod@v3.21.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", - "https://deno.land/x/zod@v3.21.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", - "https://deno.land/x/zod@v3.21.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", - "https://deno.land/x/zod@v3.21.4/types.ts": "b5d061babea250de14fc63764df5b3afa24f2b088a1d797fc060ba49a0ddff28", - "https://esm.sh/kysely@0.17.1/dist/esm/index-nodeless.js": "9c23bfd307118e3ccd3a9f0ec1261fc3451fb5301aa34aa6f28e05156818755a", - "https://esm.sh/lodash@4.17.21": "cf3544d5159a7648b25ad21fcf8dbf08a1fbfb1415b70b4163da646ef83eec4a", - "https://esm.sh/v135/kysely@0.17.1/denonext/dist/esm/index-nodeless.js": "6f73bbf2d73bc7e96cdabf941c4ae8c12f58fd7b441031edec44c029aed9532b", - "https://esm.sh/v135/lodash@4.17.21/denonext/lodash.mjs": "f04a5db09228738fd8cd06b6d1eaf3463b1b639d1529cf11673c3ac7bda1b1a8", - "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts": "3f74ab08cf97d4a3e6994cb79422e9b0069495e017416858121d5ff8ae04ac2a", - "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/mod.ts": "5f505cd265aefbcb687cde6f98c79344d3292ee1dd978e85e5ffa84a617c6682", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/mod.ts": "de0b11782a0c461ccd2722a1a67e5495186438f09be36f5d849ef13698c1725b", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/generate.ts": "222496a4617271d86d5d8b9de88471b82637c2787f4f8df2a19b44e71d5db63e", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/http-signing-cavage.ts": "86c0b286bf492246b0e934909fc8d3ecbb95f69a0df02efcdece306bac4f576c", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/httpsigjs/parser.ts": "cd8b233265f23aa8921243b33a069cc49e29a047522d861efd3b21868287c219", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/httpsigjs/utils.ts": "c60195568ee8ac807a7f58aad1bad090bc20456650c0ce6c0c48e6ec6891bbe9", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/pem.ts": "88d621412c124390d4badfda24af0b82a73ad3431391883904c810847c60b6eb", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/sign.ts": "d68479a738b7f41c2988f8f0222c1f1b9289656e0d7f0731da2f78e0f96fa71f", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/utils.ts": "434ed995db2f9ed99a25b711fc87fab5372b3cfd0ced984034b5994288a718dc", - "https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/src/verify.ts": "26ce6ea17a50814239381a71eab844e0dbc165143af8db2a5c31d29552dbcb89", - "https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/event.ts": "15e748e4561c7530437359748db56edfb5c239e8c226258440d721ffc6570b74", - "https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts": "7dd491c62fb1c4a5af089278b0a938700dc03b58fd32f1ce57e73903120caded", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts": "fabd0c320882413cdbcc3477809df842857285ae7bc3677d47b03c1e35f451e6", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/async-socket.ts": "e925b05ba1dff46bd89f680acbc0323ddb819ad01549a83498a6bdca18743a3e", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/backoff.ts": "26e6c5f4433f90a11339d9039d3230eb1cd30448b5726874fe7c13301519166f", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/deps.ts": "1f6f7cfdce91a8db807e2f22c4c4d8b3e821db3cd3b11f4bee9be7186ccc190c", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/machina.ts": "8fcda3f9c8d786ef7aab6e13790a61416fe0ea417533d7a65177b970e59dd9e8", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/nice-relay.ts": "ced298b02357d401e557234fbc29ae81c434499bdaaefe159288912b0c901c9c", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/pubsub.ts": "a36060eed7a5f1b356ee617464426f0e8c99fa352c7c52b6967026b5aa30f077", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/reanimate.ts": "9df3833a254b7c677707fdba85773a753d74c22e3528b8ef9ac6e21b13c5888f", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/relay.ts": "0b040f3336a1427d01c7726736a5d02c314eaaae96273871971703e2c1ef5558", - "https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/src/schema.ts": "b2a38e800443dcc297e64bb30b870a06c2c026bbccf4d215c4e8158a07392119", - "https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts": "77aa427debd9b796bab1cc37e46ff7e9d81ce8eef24dbe370f33254d87a74cd5", - "https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/colors.json": "0ebfe52aa82aaaaebbe2991500959bee57d8aaa8819c9841f968d4dbac58bcc0", - "https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/debug.ts": "282bdde3f10431dbfb7660d00f02a630f7e4dba0da03dc86bf661991cb8d5e53", - "https://gitlab.com/soapbox-pub/stickynotes/-/raw/v0.3.0/mod.ts": "c2d24f7c2973f7876b55c351ee8971b80e2884508334414ae4bde657eaee4ded", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/build/sqlite.js": "423a53b12ad3e068a4f02e6dba2cb64ee761afe281d61d80a997ed15f6715232", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/build/vfs.js": "08533cc78fb29b9d9bd62f6bb93e5ef333407013fed185776808f11223ba0e70", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts": "e09fc79d8065fe222578114b109b1fd60077bff1bb75448532077f784f4d6a83", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/constants.ts": "90f3be047ec0a89bcb5d6fc30db121685fc82cb00b1c476124ff47a4b0472aa9", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/db.ts": "03d0c860957496eadedd86e51a6e650670764630e64f56df0092e86c90752401", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/function.ts": "e4c83b8ec64bf88bafad2407376b0c6a3b54e777593c70336fb40d43a79865f2", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad", - "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487", - "https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js": "a336e5c58b1e6946ae8943eb4fef21b810dc2a5a233438cff92b883673e29c96" - }, - "workspace": { - "dependencies": [ - "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", - "jsr:@db/sqlite@^0.11.1", - "jsr:@nostrify/nostrify@^0.15.0", - "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", - "jsr:@soapbox/stickynotes@^0.4.0", - "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/json@^0.223.0", - "jsr:@std/media-types@^0.224.0", - "jsr:@std/streams@^0.223.0", - "npm:@isaacs/ttlcache@^1.4.1", - "npm:@noble/secp256k1@^2.0.0", - "npm:comlink@^4.4.1", - "npm:fast-stable-stringify@^1.0.0", - "npm:formdata-helper@^0.3.0", - "npm:iso-639-1@2.1.15", - "npm:kysely@^0.27.3", - "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-relaypool2@0.6.34", - "npm:nostr-tools@^2.5.1", - "npm:nostr-wasm@^0.1.0", - "npm:tldts@^6.0.14", - "npm:tseep@^1.2.1", - "npm:type-fest@^4.3.0", - "npm:unfurl.js@^6.4.0", - "npm:uuid62@^1.0.2", - "npm:zod@^3.23.5" - ] - } -} From e8088c9eedf01c9cc7cc8483ade320143c82fd57 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 1 May 2024 20:38:30 -0300 Subject: [PATCH 047/252] feat: define reports endpoint --- src/app.ts | 3 +++ src/controllers/api/reports.ts | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 src/controllers/api/reports.ts diff --git a/src/app.ts b/src/app.ts index 7e91a013..1f580634 100644 --- a/src/app.ts +++ b/src/app.ts @@ -77,6 +77,7 @@ import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; import { adminRelaysController } from '@/controllers/api/ditto.ts'; import { storeMiddleware } from '@/middleware/store.ts'; +import { reportsController } from '@/controllers/api/reports.ts'; interface AppEnv extends HonoEnv { Variables: { @@ -192,6 +193,8 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); +app.post('/api/v1/reports', requirePubkey, reportsController); + // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); app.get('/api/v1/filters', emptyArrayController); diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts new file mode 100644 index 00000000..24d8f459 --- /dev/null +++ b/src/controllers/api/reports.ts @@ -0,0 +1,7 @@ +import { type AppController } from '@/app.ts'; + +const reportsController: AppController = (c) => { + return c.json('Reports endpoint'); +}; + +export { reportsController }; From 444a6efd7d7af4c5858feda00517183f950b79d6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 18:40:04 -0500 Subject: [PATCH 048/252] Upgrade kysely-deno-sqlite, fix the type --- deno.json | 2 +- src/workers/sqlite.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 666f7d9d..167176ff 100644 --- a/deno.json +++ b/deno.json @@ -22,7 +22,7 @@ "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", - "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.0.2", + "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", diff --git a/src/workers/sqlite.ts b/src/workers/sqlite.ts index c6f2d2e2..37c33b43 100644 --- a/src/workers/sqlite.ts +++ b/src/workers/sqlite.ts @@ -33,6 +33,10 @@ class SqliteWorker { return this.#client.executeQuery(query) as Promise>; } + streamQuery(): AsyncIterableIterator { + throw new Error('Streaming queries are not supported in the web worker'); + } + destroy(): Promise { return this.#client.destroy(); } From 87264eeef10ac091e4ca222e79c62074fb3e6bcd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 19:15:20 -0500 Subject: [PATCH 049/252] Remove `relays` table from the database, track them with a NIP-65 admin event --- deno.json | 1 - scripts/relays.ts | 23 ------------------- src/db/DittoTables.ts | 7 ------ src/db/migrations/017_rm_relays.ts | 14 +++++++++++ src/db/relays.ts | 37 ------------------------------ src/pipeline.ts | 20 +--------------- src/pool.ts | 16 +++++++++++-- src/utils.ts | 8 ------- 8 files changed, 29 insertions(+), 97 deletions(-) delete mode 100644 scripts/relays.ts create mode 100644 src/db/migrations/017_rm_relays.ts delete mode 100644 src/db/relays.ts diff --git a/deno.json b/deno.json index 167176ff..b4e834a0 100644 --- a/deno.json +++ b/deno.json @@ -7,7 +7,6 @@ "debug": "deno run -A --inspect src/server.ts", "test": "DATABASE_URL=\"sqlite://:memory:\" deno test -A", "check": "deno check src/server.ts", - "relays:sync": "deno run -A scripts/relays.ts sync", "nsec": "deno run scripts/nsec.ts", "admin:event": "deno run -A scripts/admin-event.ts", "admin:role": "deno run -A scripts/admin-role.ts" diff --git a/scripts/relays.ts b/scripts/relays.ts deleted file mode 100644 index 84f8a7e6..00000000 --- a/scripts/relays.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { addRelays } from '@/db/relays.ts'; -import { filteredArray } from '@/schema.ts'; -import { relaySchema } from '@/utils.ts'; - -switch (Deno.args[0]) { - case 'sync': - await sync(Deno.args.slice(1)); - break; - default: - console.log('Usage: deno run -A scripts/relays.ts sync '); -} - -async function sync([url]: string[]) { - if (!url) { - console.error('Error: please provide a URL'); - Deno.exit(1); - } - const response = await fetch(url); - const data = await response.json(); - const values = filteredArray(relaySchema).parse(data) as `wss://${string}`[]; - await addRelays(values, { active: true }); - console.log(`Done: added ${values.length} relays.`); -} diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index 79fec5d4..d71f48ad 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -2,7 +2,6 @@ export interface DittoTables { events: EventRow; events_fts: EventFTSRow; tags: TagRow; - relays: RelayRow; unattached_media: UnattachedMediaRow; author_stats: AuthorStatsRow; event_stats: EventStatsRow; @@ -45,12 +44,6 @@ interface TagRow { event_id: string; } -interface RelayRow { - url: string; - domain: string; - active: boolean; -} - interface UnattachedMediaRow { id: string; pubkey: string; diff --git a/src/db/migrations/017_rm_relays.ts b/src/db/migrations/017_rm_relays.ts new file mode 100644 index 00000000..70a274d0 --- /dev/null +++ b/src/db/migrations/017_rm_relays.ts @@ -0,0 +1,14 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.schema.dropTable('relays').execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema + .createTable('relays') + .addColumn('url', 'text', (col) => col.primaryKey()) + .addColumn('domain', 'text', (col) => col.notNull()) + .addColumn('active', 'boolean', (col) => col.notNull()) + .execute(); +} diff --git a/src/db/relays.ts b/src/db/relays.ts deleted file mode 100644 index da29b796..00000000 --- a/src/db/relays.ts +++ /dev/null @@ -1,37 +0,0 @@ -import tldts from 'tldts'; - -import { db } from '@/db.ts'; - -interface AddRelaysOpts { - active?: boolean; -} - -/** Inserts relays into the database, skipping duplicates. */ -function addRelays(relays: `wss://${string}`[], opts: AddRelaysOpts = {}) { - if (!relays.length) return Promise.resolve(); - const { active = false } = opts; - - const values = relays.map((url) => ({ - url: new URL(url).toString(), - domain: tldts.getDomain(url)!, - active, - })); - - return db.insertInto('relays') - .values(values) - .onConflict((oc) => oc.column('url').doNothing()) - .execute(); -} - -/** Get a list of all known active relay URLs. */ -async function getActiveRelays(): Promise { - const rows = await db - .selectFrom('relays') - .select('relays.url') - .where('relays.active', '=', true) - .execute(); - - return rows.map((row) => row.url); -} - -export { addRelays, getActiveRelays }; diff --git a/src/pipeline.ts b/src/pipeline.ts index f91626dd..16876f25 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -5,7 +5,6 @@ import { sql } from 'kysely'; import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; -import { addRelays } from '@/db/relays.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isEphemeralKind } from '@/kinds.ts'; @@ -14,7 +13,7 @@ import { updateStats } from '@/stats.ts'; import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; -import { eventAge, isRelay, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; +import { eventAge, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; import { fetchWorker } from '@/workers/fetch.ts'; import { TrendsWorker } from '@/workers/trends.ts'; import { verifyEventWorker } from '@/workers/verify.ts'; @@ -59,7 +58,6 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { } } -/** Tracks known relays in the database. */ -function trackRelays(event: NostrEvent) { - const relays = new Set<`wss://${string}`>(); - - event.tags.forEach((tag) => { - if (['p', 'e', 'a'].includes(tag[0]) && isRelay(tag[2])) { - relays.add(tag[2]); - } - if (event.kind === 10002 && tag[0] === 'r' && isRelay(tag[1])) { - relays.add(tag[1]); - } - }); - - return addRelays([...relays]); -} - /** Queue related events to fetch. */ async function fetchRelatedEvents(event: DittoEvent, signal: AbortSignal) { if (!event.user) { diff --git a/src/pool.ts b/src/pool.ts index 48d5e1fa..3ac1a1db 100644 --- a/src/pool.ts +++ b/src/pool.ts @@ -1,8 +1,20 @@ import { RelayPoolWorker } from 'nostr-relaypool'; -import { getActiveRelays } from '@/db/relays.ts'; +import { Storages } from '@/storages.ts'; +import { Conf } from '@/config.ts'; -const activeRelays = await getActiveRelays(); +const [relayList] = await Storages.db.query([ + { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, +]); + +const tags = relayList?.tags ?? []; + +const activeRelays = tags.reduce((acc, [name, url, marker]) => { + if (name === 'r' && !marker) { + acc.push(url); + } + return acc; +}, []); console.log(`pool: connecting to ${activeRelays.length} relays.`); diff --git a/src/utils.ts b/src/utils.ts index 085d8af6..65e4d590 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -82,12 +82,6 @@ async function sha256(message: string): Promise { return hashHex; } -/** Schema to parse a relay URL. */ -const relaySchema = z.string().max(255).startsWith('wss://').url(); - -/** Check whether the value is a valid relay URL. */ -const isRelay = (relay: string): relay is `wss://${string}` => relaySchema.safeParse(relay).success; - /** Deduplicate events by ID. */ function dedupeEvents(events: NostrEvent[]): NostrEvent[] { return [...new Map(events.map((event) => [event.id, event])).values()]; @@ -143,13 +137,11 @@ export { eventMatchesTemplate, findTag, isNostrId, - isRelay, isURL, type Nip05, nostrDate, nostrNow, parseNip05, - relaySchema, sha256, }; From 439dfca311fae8559b8cb4ce39d76ec906bbf458 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 19:16:12 -0500 Subject: [PATCH 050/252] Fix kysely imports in migrations --- src/db/migrations/000_create_events.ts | 2 +- src/db/migrations/001_add_relays.ts | 2 +- src/db/migrations/003_events_admin.ts | 2 +- src/db/migrations/004_add_user_indexes.ts | 2 +- src/db/migrations/006_pragma.ts | 2 +- src/db/migrations/007_unattached_media.ts | 2 +- src/db/migrations/008_wal.ts | 2 +- src/db/migrations/009_add_stats.ts | 2 +- src/db/migrations/010_drop_users.ts | 2 +- src/db/migrations/011_kind_author_index.ts | 2 +- src/db/migrations/012_tags_composite_index.ts | 2 +- src/db/migrations/013_soft_deletion.ts | 2 +- src/db/migrations/014_stats_indexes.ts.ts | 2 +- src/db/migrations/015_add_pubkey_domains.ts | 2 +- src/db/migrations/016_pubkey_domains_updated_at.ts | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/db/migrations/000_create_events.ts b/src/db/migrations/000_create_events.ts index 158551ba..f08a614e 100644 --- a/src/db/migrations/000_create_events.ts +++ b/src/db/migrations/000_create_events.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema diff --git a/src/db/migrations/001_add_relays.ts b/src/db/migrations/001_add_relays.ts index 1415f5f7..11c68844 100644 --- a/src/db/migrations/001_add_relays.ts +++ b/src/db/migrations/001_add_relays.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema diff --git a/src/db/migrations/003_events_admin.ts b/src/db/migrations/003_events_admin.ts index 8469fc24..388a3a47 100644 --- a/src/db/migrations/003_events_admin.ts +++ b/src/db/migrations/003_events_admin.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(_db: Kysely): Promise { } diff --git a/src/db/migrations/004_add_user_indexes.ts b/src/db/migrations/004_add_user_indexes.ts index 929181c9..fca9c5f3 100644 --- a/src/db/migrations/004_add_user_indexes.ts +++ b/src/db/migrations/004_add_user_indexes.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(_db: Kysely): Promise { } diff --git a/src/db/migrations/006_pragma.ts b/src/db/migrations/006_pragma.ts index 2639e817..f20ee9bd 100644 --- a/src/db/migrations/006_pragma.ts +++ b/src/db/migrations/006_pragma.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(_db: Kysely): Promise { } diff --git a/src/db/migrations/007_unattached_media.ts b/src/db/migrations/007_unattached_media.ts index 18871112..a36c5d35 100644 --- a/src/db/migrations/007_unattached_media.ts +++ b/src/db/migrations/007_unattached_media.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema diff --git a/src/db/migrations/008_wal.ts b/src/db/migrations/008_wal.ts index 2639e817..f20ee9bd 100644 --- a/src/db/migrations/008_wal.ts +++ b/src/db/migrations/008_wal.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(_db: Kysely): Promise { } diff --git a/src/db/migrations/009_add_stats.ts b/src/db/migrations/009_add_stats.ts index 60d9447b..ef1c4438 100644 --- a/src/db/migrations/009_add_stats.ts +++ b/src/db/migrations/009_add_stats.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema diff --git a/src/db/migrations/010_drop_users.ts b/src/db/migrations/010_drop_users.ts index 6cd83c01..c36f2fa9 100644 --- a/src/db/migrations/010_drop_users.ts +++ b/src/db/migrations/010_drop_users.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema.dropTable('users').ifExists().execute(); diff --git a/src/db/migrations/011_kind_author_index.ts b/src/db/migrations/011_kind_author_index.ts index da21988d..3e7d010c 100644 --- a/src/db/migrations/011_kind_author_index.ts +++ b/src/db/migrations/011_kind_author_index.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema diff --git a/src/db/migrations/012_tags_composite_index.ts b/src/db/migrations/012_tags_composite_index.ts index 8769289f..412fa599 100644 --- a/src/db/migrations/012_tags_composite_index.ts +++ b/src/db/migrations/012_tags_composite_index.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema.dropIndex('idx_tags_tag').execute(); diff --git a/src/db/migrations/013_soft_deletion.ts b/src/db/migrations/013_soft_deletion.ts index 3856ca02..df19da50 100644 --- a/src/db/migrations/013_soft_deletion.ts +++ b/src/db/migrations/013_soft_deletion.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema.alterTable('events').addColumn('deleted_at', 'integer').execute(); diff --git a/src/db/migrations/014_stats_indexes.ts.ts b/src/db/migrations/014_stats_indexes.ts.ts index d9071c62..0f27a7fa 100644 --- a/src/db/migrations/014_stats_indexes.ts.ts +++ b/src/db/migrations/014_stats_indexes.ts.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema.createIndex('idx_author_stats_pubkey').on('author_stats').column('pubkey').execute(); diff --git a/src/db/migrations/015_add_pubkey_domains.ts b/src/db/migrations/015_add_pubkey_domains.ts index 0b5fe293..4b7e23c4 100644 --- a/src/db/migrations/015_add_pubkey_domains.ts +++ b/src/db/migrations/015_add_pubkey_domains.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema diff --git a/src/db/migrations/016_pubkey_domains_updated_at.ts b/src/db/migrations/016_pubkey_domains_updated_at.ts index 3a000c1d..8b1f75d0 100644 --- a/src/db/migrations/016_pubkey_domains_updated_at.ts +++ b/src/db/migrations/016_pubkey_domains_updated_at.ts @@ -1,4 +1,4 @@ -import { Kysely } from '@/deps.ts'; +import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema From 76f30f5cc76f58d923070f17fcddf56b0499fc5b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 19:19:52 -0500 Subject: [PATCH 051/252] utils: remove unused import of zod --- src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 65e4d590..c56abb8a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,5 @@ import { NostrEvent } from '@nostrify/nostrify'; import { EventTemplate, getEventHash, nip19 } from 'nostr-tools'; -import { z } from 'zod'; import { nostrIdSchema } from '@/schemas/nostr.ts'; From fc7ed8bf2498f272907dd936bc2981c8f8e44385 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 19:51:12 -0500 Subject: [PATCH 052/252] Remove zod schemas that we can get from NSchema --- src/controllers/api/accounts.ts | 5 +- src/controllers/api/instance.ts | 6 +- src/controllers/api/pleroma.ts | 4 +- src/controllers/api/search.ts | 5 +- src/controllers/api/statuses.ts | 5 +- src/controllers/nostr/relay-info.ts | 6 +- src/controllers/nostr/relay.ts | 33 ++++----- src/filter.ts | 7 +- src/schema.ts | 12 +--- src/schemas/nostr.ts | 107 ++-------------------------- src/storages/events-db.ts | 5 +- src/utils.ts | 49 ++----------- src/utils/nip98.ts | 6 +- src/views/activitypub/actor.ts | 5 +- src/views/mastodon/accounts.ts | 4 +- src/views/mastodon/statuses.ts | 6 +- src/workers/trends.worker.ts | 4 +- 17 files changed, 65 insertions(+), 204 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 96d4afa5..58b93b48 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -1,4 +1,4 @@ -import { NostrFilter } from '@nostrify/nostrify'; +import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; @@ -6,7 +6,6 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; -import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { Storages } from '@/storages.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts'; import { uploadFile } from '@/upload.ts'; @@ -198,7 +197,7 @@ const updateCredentialsController: AppController = async (c) => { } const author = await getAuthor(pubkey); - const meta = author ? jsonMetaContentSchema.parse(author.content) : {}; + const meta = author ? n.json().pipe(n.metadata()).parse(author.content) : {}; const { avatar: avatarFile, diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index d4239d31..ac9810e5 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,6 +1,8 @@ +import { NSchema as n } from '@nostrify/nostrify'; + import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; +import { serverMetaSchema } from '@/schemas/nostr.ts'; import { Storages } from '@/storages.ts'; const instanceController: AppController = async (c) => { @@ -8,7 +10,7 @@ const instanceController: AppController = async (c) => { const { signal } = c.req.raw; const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = jsonServerMetaSchema.parse(event?.content); + const meta = n.json().pipe(serverMetaSchema).parse(event?.content); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 51f48dc3..4b693c4f 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -1,3 +1,4 @@ +import { NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; @@ -6,7 +7,6 @@ import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/p import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { createAdminEvent } from '@/utils/api.ts'; -import { jsonSchema } from '@/schema.ts'; const frontendConfigController: AppController = async (c) => { const configs = await getConfigs(c.req.raw.signal); @@ -75,7 +75,7 @@ async function getConfigs(signal: AbortSignal): Promise { try { const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); - return jsonSchema.pipe(configSchema.array()).catch([]).parse(decrypted); + return n.json().pipe(configSchema.array()).catch([]).parse(decrypted); } catch (_e) { return []; } diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index f595e22f..e674d0e9 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -1,10 +1,9 @@ -import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; import { booleanParamSchema } from '@/schema.ts'; -import { nostrIdSchema } from '@/schemas/nostr.ts'; import { Storages } from '@/storages.ts'; import { dedupeEvents } from '@/utils.ts'; import { nip05Cache } from '@/utils/nip05.ts'; @@ -20,7 +19,7 @@ const searchQuerySchema = z.object({ type: z.enum(['accounts', 'statuses', 'hashtags']).optional(), resolve: booleanParamSchema.optional().transform(Boolean), following: z.boolean().default(false), - account_id: nostrIdSchema.optional(), + account_id: n.id().optional(), limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)), }); diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 2c05dbd4..8421900d 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,4 +1,4 @@ -import { NIP05, NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { NIP05, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import ISO6391 from 'iso-639-1'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; @@ -7,7 +7,6 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; -import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { addTag, deleteTag } from '@/tags.ts'; import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; import { renderEventAccounts } from '@/views.ts'; @@ -406,7 +405,7 @@ const zapController: AppController = async (c) => { const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'], signal }); const author = target?.author; - const meta = jsonMetaContentSchema.parse(author?.content); + const meta = n.json().pipe(n.metadata()).parse(author?.content); const lnurl = getLnurl(meta); if (target && lnurl) { diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index 7f1ddf7f..f42c8df8 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,12 +1,14 @@ +import { NSchema as n } from '@nostrify/nostrify'; + import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; +import { serverMetaSchema } from '@/schemas/nostr.ts'; import { Storages } from '@/storages.ts'; const relayInfoController: AppController = async (c) => { const { signal } = c.req.raw; const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = jsonServerMetaSchema.parse(event?.content); + const meta = n.json().pipe(serverMetaSchema).parse(event?.content); return c.json({ name: meta.name ?? 'Ditto', diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 2fe8f921..7d70ad9f 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -1,14 +1,15 @@ -import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { + NostrClientCLOSE, + NostrClientCOUNT, + NostrClientEVENT, + NostrClientMsg, + NostrClientREQ, + NostrEvent, + NostrFilter, + NSchema as n, +} from '@nostrify/nostrify'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import * as pipeline from '@/pipeline.ts'; -import { - type ClientCLOSE, - type ClientCOUNT, - type ClientEVENT, - type ClientMsg, - clientMsgSchema, - type ClientREQ, -} from '@/schemas/nostr.ts'; import { Storages } from '@/storages.ts'; import type { AppController } from '@/app.ts'; @@ -30,7 +31,7 @@ function connectStream(socket: WebSocket) { const controllers = new Map(); socket.onmessage = (e) => { - const result = n.json().pipe(clientMsgSchema).safeParse(e.data); + const result = n.json().pipe(n.clientMsg()).safeParse(e.data); if (result.success) { handleMsg(result.data); } else { @@ -45,7 +46,7 @@ function connectStream(socket: WebSocket) { }; /** Handle client message. */ - function handleMsg(msg: ClientMsg) { + function handleMsg(msg: NostrClientMsg) { switch (msg[0]) { case 'REQ': handleReq(msg); @@ -63,7 +64,7 @@ function connectStream(socket: WebSocket) { } /** Handle REQ. Start a subscription. */ - async function handleReq([_, subId, ...rest]: ClientREQ): Promise { + async function handleReq([_, subId, ...rest]: NostrClientREQ): Promise { const filters = prepareFilters(rest); const controller = new AbortController(); @@ -88,7 +89,7 @@ function connectStream(socket: WebSocket) { } /** Handle EVENT. Store the event. */ - async function handleEvent([_, event]: ClientEVENT): Promise { + async function handleEvent([_, event]: NostrClientEVENT): Promise { try { // This will store it (if eligible) and run other side-effects. await pipeline.handleEvent(event, AbortSignal.timeout(1000)); @@ -104,7 +105,7 @@ function connectStream(socket: WebSocket) { } /** Handle CLOSE. Close the subscription. */ - function handleClose([_, subId]: ClientCLOSE): void { + function handleClose([_, subId]: NostrClientCLOSE): void { const controller = controllers.get(subId); if (controller) { controller.abort(); @@ -113,7 +114,7 @@ function connectStream(socket: WebSocket) { } /** Handle COUNT. Return the number of events matching the filters. */ - async function handleCount([_, subId, ...rest]: ClientCOUNT): Promise { + async function handleCount([_, subId, ...rest]: NostrClientCOUNT): Promise { const { count } = await Storages.db.count(prepareFilters(rest)); send(['COUNT', subId, { count, approximate: false }]); } @@ -127,7 +128,7 @@ function connectStream(socket: WebSocket) { } /** Enforce the filters with certain criteria. */ -function prepareFilters(filters: ClientREQ[2][]): NostrFilter[] { +function prepareFilters(filters: NostrClientREQ[2][]): NostrFilter[] { return filters.map((filter) => { const narrow = Boolean(filter.ids?.length || filter.authors?.length); const search = narrow ? filter.search : `domain:${Conf.url.host} ${filter.search ?? ''}`; diff --git a/src/filter.ts b/src/filter.ts index 62473785..59b02982 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,9 +1,8 @@ -import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import stringifyStable from 'fast-stable-stringify'; import { z } from 'zod'; import { isReplaceableKind } from '@/kinds.ts'; -import { nostrIdSchema } from '@/schemas/nostr.ts'; /** Microfilter to get one specific event by ID. */ type IdMicrofilter = { ids: [NostrEvent['id']] }; @@ -42,8 +41,8 @@ function getMicroFilters(event: NostrEvent): MicroFilter[] { /** Microfilter schema. */ const microFilterSchema = z.union([ - z.object({ ids: z.tuple([nostrIdSchema]) }).strict(), - z.object({ kinds: z.tuple([z.literal(0)]), authors: z.tuple([nostrIdSchema]) }).strict(), + z.object({ ids: z.tuple([n.id()]) }).strict(), + z.object({ kinds: z.tuple([z.literal(0)]), authors: z.tuple([n.id()]) }).strict(), ]); /** Checks whether the filter is a microfilter. */ diff --git a/src/schema.ts b/src/schema.ts index 74dc7af4..d152a0d4 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -11,16 +11,6 @@ function filteredArray(schema: T) { )); } -/** Parses a JSON string into its native type. */ -const jsonSchema = z.string().transform((value, ctx) => { - try { - return JSON.parse(value) as unknown; - } catch (_e) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid JSON' }); - return z.NEVER; - } -}); - /** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ const decode64Schema = z.string().transform((value, ctx) => { try { @@ -48,4 +38,4 @@ const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value /** Schema for `File` objects. */ const fileSchema = z.custom((value) => value instanceof File); -export { booleanParamSchema, decode64Schema, fileSchema, filteredArray, hashtagSchema, jsonSchema, safeUrlSchema }; +export { booleanParamSchema, decode64Schema, fileSchema, filteredArray, hashtagSchema, safeUrlSchema }; diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index 7f51f6c1..a42b9f07 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -1,80 +1,14 @@ +import { NSchema as n } from '@nostrify/nostrify'; import { getEventHash, verifyEvent } from 'nostr-tools'; import { z } from 'zod'; -import { jsonSchema, safeUrlSchema } from '@/schema.ts'; - -/** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */ -const nostrIdSchema = z.string().regex(/^[0-9a-f]{64}$/); -/** Nostr kinds are positive integers. */ -const kindSchema = z.number().int().nonnegative(); - -/** Nostr event schema. */ -const eventSchema = z.object({ - id: nostrIdSchema, - kind: kindSchema, - tags: z.array(z.array(z.string())), - content: z.string(), - created_at: z.number(), - pubkey: nostrIdSchema, - sig: z.string(), -}); +import { safeUrlSchema } from '@/schema.ts'; /** Nostr event schema that also verifies the event's signature. */ -const signedEventSchema = eventSchema +const signedEventSchema = n.event() .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine(verifyEvent, 'Event signature is invalid'); -/** Nostr relay filter schema. */ -const filterSchema = z.object({ - kinds: kindSchema.array().optional(), - ids: nostrIdSchema.array().optional(), - authors: nostrIdSchema.array().optional(), - since: z.number().int().nonnegative().optional(), - until: z.number().int().nonnegative().optional(), - limit: z.number().int().nonnegative().optional(), - search: z.string().optional(), -}).passthrough().and( - z.record( - z.custom<`#${string}`>((val) => typeof val === 'string' && val.startsWith('#')), - z.string().array(), - ).catch({}), -); - -const clientReqSchema = z.tuple([z.literal('REQ'), z.string().min(1)]).rest(filterSchema); -const clientEventSchema = z.tuple([z.literal('EVENT'), signedEventSchema]); -const clientCloseSchema = z.tuple([z.literal('CLOSE'), z.string().min(1)]); -const clientCountSchema = z.tuple([z.literal('COUNT'), z.string().min(1)]).rest(filterSchema); - -/** Client message to a Nostr relay. */ -const clientMsgSchema = z.union([ - clientReqSchema, - clientEventSchema, - clientCloseSchema, - clientCountSchema, -]); - -/** REQ message from client to relay. */ -type ClientREQ = z.infer; -/** EVENT message from client to relay. */ -type ClientEVENT = z.infer; -/** CLOSE message from client to relay. */ -type ClientCLOSE = z.infer; -/** COUNT message from client to relay. */ -type ClientCOUNT = z.infer; -/** Client message to a Nostr relay. */ -type ClientMsg = z.infer; - -/** Kind 0 content schema. */ -const metaContentSchema = z.object({ - name: z.string().optional().catch(undefined), - about: z.string().optional().catch(undefined), - picture: z.string().optional().catch(undefined), - banner: z.string().optional().catch(undefined), - nip05: z.string().optional().catch(undefined), - lud06: z.string().optional().catch(undefined), - lud16: z.string().optional().catch(undefined), -}).partial().passthrough(); - /** Media data schema from `"media"` tags. */ const mediaDataSchema = z.object({ blurhash: z.string().optional().catch(undefined), @@ -88,40 +22,25 @@ const mediaDataSchema = z.object({ }); /** Kind 0 content schema for the Ditto server admin user. */ -const serverMetaSchema = metaContentSchema.extend({ +const serverMetaSchema = n.metadata().and(z.object({ tagline: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined), -}); +})); /** Media data from `"media"` tags. */ type MediaData = z.infer; -/** Parses kind 0 content from a JSON string. */ -const jsonMetaContentSchema = jsonSchema.pipe(metaContentSchema).catch({}); - -/** Parses media data from a JSON string. */ -const jsonMediaDataSchema = jsonSchema.pipe(mediaDataSchema).catch({}); - -/** Parses server admin meta from a JSON string. */ -const jsonServerMetaSchema = jsonSchema.pipe(serverMetaSchema).catch({}); - /** NIP-11 Relay Information Document. */ const relayInfoDocSchema = z.object({ name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined), description: z.string().transform((val) => val.slice(0, 3000)).optional().catch(undefined), - pubkey: nostrIdSchema.optional().catch(undefined), + pubkey: n.id().optional().catch(undefined), contact: safeUrlSchema.optional().catch(undefined), supported_nips: z.number().int().nonnegative().array().optional().catch(undefined), software: safeUrlSchema.optional().catch(undefined), icon: safeUrlSchema.optional().catch(undefined), }); -/** NIP-46 signer response. */ -const connectResponseSchema = z.object({ - id: z.string(), - result: signedEventSchema, -}); - /** Parses a Nostr emoji tag. */ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]); @@ -129,23 +48,11 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url() type EmojiTag = z.infer; export { - type ClientCLOSE, - type ClientCOUNT, - type ClientEVENT, - type ClientMsg, - clientMsgSchema, - type ClientREQ, - connectResponseSchema, type EmojiTag, emojiTagSchema, - filterSchema, - jsonMediaDataSchema, - jsonMetaContentSchema, - jsonServerMetaSchema, type MediaData, mediaDataSchema, - metaContentSchema, - nostrIdSchema, relayInfoDocSchema, + serverMetaSchema, signedEventSchema, }; diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 74bcb01b..d0253f9a 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,4 +1,4 @@ -import { NIP50, NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; +import { NIP50, NostrEvent, NostrFilter, NSchema as n, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { Kysely, type SelectQueryBuilder } from 'kysely'; @@ -7,7 +7,6 @@ import { DittoTables } from '@/db/DittoTables.ts'; import { normalizeFilters } from '@/filter.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; -import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; @@ -412,7 +411,7 @@ function buildSearchContent(event: NostrEvent): string { /** Build search content for a user. */ function buildUserSearchContent(event: NostrEvent): string { - const { name, nip05, about } = jsonMetaContentSchema.parse(event.content); + const { name, nip05, about } = n.json().pipe(n.metadata()).parse(event.content); return [name, nip05, about].filter(Boolean).join('\n'); } diff --git a/src/utils.ts b/src/utils.ts index c56abb8a..e9213ed1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,16 +1,13 @@ -import { NostrEvent } from '@nostrify/nostrify'; -import { EventTemplate, getEventHash, nip19 } from 'nostr-tools'; - -import { nostrIdSchema } from '@/schemas/nostr.ts'; +import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; +import { z } from 'zod'; /** Get the current time in Nostr format. */ const nostrNow = (): number => Math.floor(Date.now() / 1000); + /** Convenience function to convert Nostr dates into native Date objects. */ const nostrDate = (seconds: number): Date => new Date(seconds * 1000); -/** Pass to sort() to sort events by date. */ -const eventDateComparator = (a: NostrEvent, b: NostrEvent): number => b.created_at - a.created_at; - /** Get pubkey from bech32 string, if applicable. */ function bech32ToPubkey(bech32: string): string | undefined { try { @@ -86,54 +83,20 @@ function dedupeEvents(events: NostrEvent[]): NostrEvent[] { return [...new Map(events.map((event) => [event.id, event])).values()]; } -/** Return a copy of the event with the given tags removed. */ -function stripTags(event: E, tags: string[] = []): E { - if (!tags.length) return event; - return { - ...event, - tags: event.tags.filter(([name]) => !tags.includes(name)), - }; -} - -/** Ensure the template and event match on their shared keys. */ -function eventMatchesTemplate(event: NostrEvent, template: EventTemplate): boolean { - const whitelist = ['nonce']; - - event = stripTags(event, whitelist); - template = stripTags(template, whitelist); - - if (template.created_at > event.created_at) { - return false; - } - - return getEventHash(event) === getEventHash({ - pubkey: event.pubkey, - ...template, - created_at: event.created_at, - }); -} - /** Test whether the value is a Nostr ID. */ function isNostrId(value: unknown): boolean { - return nostrIdSchema.safeParse(value).success; + return n.id().safeParse(value).success; } /** Test whether the value is a URL. */ function isURL(value: unknown): boolean { - try { - new URL(value as string); - return true; - } catch (_) { - return false; - } + return z.string().url().safeParse(value).success; } export { bech32ToPubkey, dedupeEvents, eventAge, - eventDateComparator, - eventMatchesTemplate, findTag, isNostrId, isURL, diff --git a/src/utils/nip98.ts b/src/utils/nip98.ts index 74e60a4f..c33da877 100644 --- a/src/utils/nip98.ts +++ b/src/utils/nip98.ts @@ -1,13 +1,13 @@ -import { NostrEvent } from '@nostrify/nostrify'; +import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { EventTemplate, nip13 } from 'nostr-tools'; -import { decode64Schema, jsonSchema } from '@/schema.ts'; +import { decode64Schema } from '@/schema.ts'; import { signedEventSchema } from '@/schemas/nostr.ts'; import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts'; import { Time } from '@/utils/time.ts'; /** Decode a Nostr event from a base64 encoded string. */ -const decode64EventSchema = decode64Schema.pipe(jsonSchema).pipe(signedEventSchema); +const decode64EventSchema = decode64Schema.pipe(n.json()).pipe(signedEventSchema); interface ParseAuthRequestOpts { /** Max event age (in ms). */ diff --git a/src/views/activitypub/actor.ts b/src/views/activitypub/actor.ts index 9ca9a277..e5d26d24 100644 --- a/src/views/activitypub/actor.ts +++ b/src/views/activitypub/actor.ts @@ -1,5 +1,6 @@ +import { NSchema as n } from '@nostrify/nostrify'; + import { Conf } from '@/config.ts'; -import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { getPublicKeyPem } from '@/utils/rsa.ts'; import type { NostrEvent } from '@nostrify/nostrify'; @@ -7,7 +8,7 @@ import type { Actor } from '@/schemas/activitypub.ts'; /** Nostr metadata event to ActivityPub actor. */ async function renderActor(event: NostrEvent, username: string): Promise { - const content = jsonMetaContentSchema.parse(event.content); + const content = n.json().pipe(n.metadata()).parse(event.content); return { type: 'Person', diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 18f3a9dc..50458bff 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -1,9 +1,9 @@ +import { NSchema as n } from '@nostrify/nostrify'; import { nip19, UnsignedEvent } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { lodash } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { getLnurl } from '@/utils/lnurl.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; @@ -28,7 +28,7 @@ async function renderAccount( about, lud06, lud16, - } = jsonMetaContentSchema.parse(event.content); + } = n.json().pipe(n.metadata()).parse(event.content); const npub = nip19.npubEncode(pubkey); const parsed05 = await parseAndVerifyNip05(nip05, pubkey); diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index f63c5010..683c667a 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -1,11 +1,10 @@ -import { NostrEvent } from '@nostrify/nostrify'; +import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; import { nip19 } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; -import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; import { nostrDate } from '@/utils.ts'; @@ -13,6 +12,7 @@ import { unfurlCardCached } from '@/utils/unfurl.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; +import { mediaDataSchema } from '@/schemas/nostr.ts'; interface statusOpts { viewerPubkey?: string; @@ -78,7 +78,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { const mediaTags: DittoAttachment[] = event.tags .filter((tag) => tag[0] === 'media') - .map(([_, url, json]) => ({ url, data: jsonMediaDataSchema.parse(json) })); + .map(([_, url, json]) => ({ url, data: n.json().pipe(mediaDataSchema).parse(json) })); const media = [...mediaLinks, ...mediaTags]; diff --git a/src/workers/trends.worker.ts b/src/workers/trends.worker.ts index 819883ff..33fd1a12 100644 --- a/src/workers/trends.worker.ts +++ b/src/workers/trends.worker.ts @@ -1,8 +1,8 @@ +import { NSchema } from '@nostrify/nostrify'; import * as Comlink from 'comlink'; import { Sqlite } from '@/deps.ts'; import { hashtagSchema } from '@/schema.ts'; -import { nostrIdSchema } from '@/schemas/nostr.ts'; import { generateDateRange, Time } from '@/utils/time.ts'; interface GetTrendingTagsOpts { @@ -102,7 +102,7 @@ export const TrendsWorker = { }, addTagUsages(pubkey: string, hashtags: string[], date = new Date()): void { - const pubkey8 = nostrIdSchema.parse(pubkey).substring(0, 8); + const pubkey8 = NSchema.id().parse(pubkey).substring(0, 8); const tags = hashtagSchema.array().min(1).parse(hashtags); db.query( From 4045a6bdfce516835741f42922a1404ecfc9b800 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 19:55:58 -0500 Subject: [PATCH 053/252] Catch metadata when parsing --- src/controllers/api/accounts.ts | 2 +- src/controllers/api/instance.ts | 2 +- src/controllers/api/statuses.ts | 2 +- src/controllers/nostr/relay-info.ts | 2 +- src/pipeline.ts | 2 +- src/storages/events-db.ts | 2 +- src/views/activitypub/actor.ts | 2 +- src/views/mastodon/accounts.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 58b93b48..473dd63f 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -197,7 +197,7 @@ const updateCredentialsController: AppController = async (c) => { } const author = await getAuthor(pubkey); - const meta = author ? n.json().pipe(n.metadata()).parse(author.content) : {}; + const meta = author ? n.json().pipe(n.metadata()).catch({}).parse(author.content) : {}; const { avatar: avatarFile, diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index ac9810e5..188d68f5 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -10,7 +10,7 @@ const instanceController: AppController = async (c) => { const { signal } = c.req.raw; const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = n.json().pipe(serverMetaSchema).parse(event?.content); + const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 8421900d..56ea38b2 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -405,7 +405,7 @@ const zapController: AppController = async (c) => { const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'], signal }); const author = target?.author; - const meta = n.json().pipe(n.metadata()).parse(author?.content); + const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content); const lnurl = getLnurl(meta); if (target && lnurl) { diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index f42c8df8..a56df51e 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -8,7 +8,7 @@ import { Storages } from '@/storages.ts'; const relayInfoController: AppController = async (c) => { const { signal } = c.req.raw; const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = n.json().pipe(serverMetaSchema).parse(event?.content); + const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content); return c.json({ name: meta.name ?? 'Ditto', diff --git a/src/pipeline.ts b/src/pipeline.ts index 16876f25..b1936173 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -111,7 +111,7 @@ async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise { - const content = n.json().pipe(n.metadata()).parse(event.content); + const content = n.json().pipe(n.metadata()).catch({}).parse(event.content); return { type: 'Person', diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 50458bff..e00856e2 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -28,7 +28,7 @@ async function renderAccount( about, lud06, lud16, - } = n.json().pipe(n.metadata()).parse(event.content); + } = n.json().pipe(n.metadata()).catch({}).parse(event.content); const npub = nip19.npubEncode(pubkey); const parsed05 = await parseAndVerifyNip05(nip05, pubkey); From b016f931ff52ca0fa31c974c4e8a5209d34642a5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 May 2024 09:54:24 -0500 Subject: [PATCH 054/252] EventsDB: always index the first P-tag of events --- src/storages/events-db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index ba34b3c8..2fcdf161 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -25,7 +25,7 @@ const tagConditions: Record = { 'L': ({ event, count }) => event.kind === 1985 || count === 0, 'l': ({ event, count }) => event.kind === 1985 || count === 0, 'media': ({ event, count, value }) => (event.user || count < 4) && isURL(value), - 'P': ({ event, count, value }) => event.kind === 9735 && count === 0 && isNostrId(value), + 'P': ({ count, value }) => count === 0 && isNostrId(value), 'p': ({ event, count, value }) => (count < 15 || event.kind === 3) && isNostrId(value), 'proxy': ({ count, value }) => count === 0 && isURL(value), 'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value), From 23f8377231124197cd140a4766ffa703b9a98b01 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 2 May 2024 16:03:15 -0300 Subject: [PATCH 055/252] feat: create reports controller --- src/controllers/api/reports.ts | 51 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 24d8f459..8008eaed 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -1,7 +1,54 @@ import { type AppController } from '@/app.ts'; +import { z } from 'zod'; +import { createEvent, parseBody } from '@/utils/api.ts'; +import { Conf } from '@/config.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; +import { renderReports } from '@/views/mastodon/reports.ts'; -const reportsController: AppController = (c) => { - return c.json('Reports endpoint'); +const reportsSchema = z.object({ + account_id: z.string(), + status_ids: z.string().array().default([]), + comment: z.string().max(1000).default(''), + forward: z.boolean().default(false), + category: z.string().default('other'), + // TODO: rules_ids[] is not implemented +}); + +/** https://docs.joinmastodon.org/methods/reports/ */ +const reportsController: AppController = async (c) => { + const store = c.get('store'); + const body = await parseBody(c.req.raw); + const result = reportsSchema.safeParse(body); + + if (!result.success) { + return c.json(result.error, 422); + } + + const { + account_id, + status_ids, + comment, + forward, + category, + } = result.data; + + const [personBeingReported] = await store.query([{ kinds: [0], authors: [account_id] }]); + if (!personBeingReported) { + return c.json({ error: 'Record not found' }, 404); + } + + await hydrateEvents({ events: [personBeingReported], storage: store }); + + const event = await createEvent({ + kind: 1984, + content: JSON.stringify({ account_id, status_ids, comment, forward, category }), + tags: [ + ['p', account_id, category], + ['P', Conf.pubkey], + ], + }, c); + + return c.json(await renderReports(event, personBeingReported, {})); }; export { reportsController }; From 226c356646e1563f5cf3718af1b64dddd8417a5b Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 2 May 2024 16:03:59 -0300 Subject: [PATCH 056/252] feat: create mastodon response for reports --- src/views/mastodon/reports.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/views/mastodon/reports.ts diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts new file mode 100644 index 00000000..67f9f6da --- /dev/null +++ b/src/views/mastodon/reports.ts @@ -0,0 +1,33 @@ +import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { renderAccount } from '@/views/mastodon/accounts.ts'; +import { nostrDate } from '@/utils.ts'; + +interface reportsOpts { + viewerPubkey?: string; +} + +/** Expects a `reportEvent` of kind 1984 and a `targetAccout` of kind 0 of the person being reported */ +async function renderReports(reportEvent: DittoEvent, targetAccout: DittoEvent, _opts: reportsOpts) { + const { + account_id, + status_ids, + comment, + forward, + category, + } = JSON.parse(reportEvent.content); + + return { + id: account_id, + action_taken: false, + action_taken_at: null, + category, + comment, + forwarded: forward, + created_at: nostrDate(reportEvent.created_at).toISOString(), + status_ids, + rules_ids: null, + target_account: await renderAccount(targetAccout), + }; +} + +export { renderReports }; From 220f16feb8d5272d71d73db6a9c99d1d56b6bcc9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 May 2024 14:36:28 -0500 Subject: [PATCH 057/252] Notifications: render notifications for kinds 1, 6, and 7 events --- src/controllers/api/notifications.ts | 42 ++++++++++---- src/interfaces/DittoEvent.ts | 1 + src/storages/hydrate.ts | 23 ++++++++ src/views/mastodon/notifications.ts | 83 ++++++++++++++++++++++++---- src/views/mastodon/statuses.ts | 6 +- 5 files changed, 130 insertions(+), 25 deletions(-) diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index 7820dd86..b2fa15ea 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -1,20 +1,40 @@ -import { type AppController } from '@/app.ts'; -import { Storages } from '@/storages.ts'; +import { NostrFilter } from '@nostrify/nostrify'; + +import { AppContext, AppController } from '@/app.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; -const notificationsController: AppController = async (c) => { +const notificationsController: AppController = (c) => { const pubkey = c.get('pubkey')!; const { since, until } = paginationSchema.parse(c.req.query()); - const { signal } = c.req.raw; - const events = await Storages.db.query( - [{ kinds: [1], '#p': [pubkey], since, until }], - { signal }, - ); - - const statuses = await Promise.all(events.map((event) => renderNotification(event, pubkey))); - return paginated(c, events, statuses); + return renderNotifications(c, [{ kinds: [1, 6, 7], '#p': [pubkey], since, until }]); }; +async function renderNotifications(c: AppContext, filters: NostrFilter[]) { + const store = c.get('store'); + const pubkey = c.get('pubkey')!; + const { signal } = c.req.raw; + + const events = await store + .query(filters, { signal }) + .then((events) => events.filter((event) => event.pubkey !== pubkey)) + .then((events) => hydrateEvents({ events, storage: store, signal })); + + if (!events.length) { + return c.json([]); + } + + const notifications = (await Promise + .all(events.map((event) => renderNotification(event, { viewerPubkey: pubkey })))) + .filter(Boolean); + + if (!notifications.length) { + return c.json([]); + } + + return paginated(c, events, notifications); +} + export { notificationsController }; diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 08879f81..2ef0bb26 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -24,4 +24,5 @@ export interface DittoEvent extends NostrEvent { user?: DittoEvent; repost?: DittoEvent; quote_repost?: DittoEvent; + reacted?: DittoEvent; } diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index dbe277ad..716f2518 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -26,6 +26,10 @@ async function hydrateEvents(opts: HydrateOpts): Promise { cache.push(event); } + for (const event of await gatherReacted({ events: cache, storage, signal })) { + cache.push(event); + } + for (const event of await gatherQuotes({ events: cache, storage, signal })) { cache.push(event); } @@ -105,6 +109,25 @@ function gatherReposts({ events, storage, signal }: HydrateOpts): Promise { + const ids = new Set(); + + for (const event of events) { + if (event.kind === 7) { + const id = event.tags.find(([name]) => name === 'e')?.[1]; + if (id) { + ids.add(id); + } + } + } + + return storage.query( + [{ ids: [...ids], limit: ids.size }], + { signal }, + ); +} + /** Collect quotes from the events. */ function gatherQuotes({ events, storage, signal }: HydrateOpts): Promise { const ids = new Set(); diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts index a1531405..266b77b0 100644 --- a/src/views/mastodon/notifications.ts +++ b/src/views/mastodon/notifications.ts @@ -1,19 +1,34 @@ -import { NostrEvent } from '@nostrify/nostrify'; -import { getAuthor } from '@/queries.ts'; +import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { nostrDate } from '@/utils.ts'; -import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -function renderNotification(event: NostrEvent, viewerPubkey?: string) { - switch (event.kind) { - case 1: - return renderNotificationMention(event, viewerPubkey); +interface RenderNotificationOpts { + viewerPubkey: string; +} + +function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) { + const mentioned = !!event.tags.find(([name, value]) => name === 'p' && value === opts.viewerPubkey); + + if (event.kind === 1 && mentioned) { + return renderMention(event, opts); + } + + if (event.kind === 6) { + return renderReblog(event, opts); + } + + if (event.kind === 7 && event.content === '+') { + return renderFavourite(event, opts); + } + + if (event.kind === 7) { + return renderReaction(event, opts); } } -async function renderNotificationMention(event: NostrEvent, viewerPubkey?: string) { - const author = await getAuthor(event.pubkey); - const status = await renderStatus({ ...event, author }, { viewerPubkey: viewerPubkey }); +async function renderMention(event: DittoEvent, opts: RenderNotificationOpts) { + const status = await renderStatus(event, opts); if (!status) return; return { @@ -25,4 +40,50 @@ async function renderNotificationMention(event: NostrEvent, viewerPubkey?: strin }; } -export { accountFromPubkey, renderNotification }; +async function renderReblog(event: DittoEvent, opts: RenderNotificationOpts) { + if (event.repost?.kind !== 1) return; + const status = await renderStatus(event.repost, opts); + if (!status) return; + const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey); + + return { + id: event.id, + type: 'reblog', + created_at: nostrDate(event.created_at).toISOString(), + account, + status, + }; +} + +async function renderFavourite(event: DittoEvent, opts: RenderNotificationOpts) { + if (event.reacted?.kind !== 1) return; + const status = await renderStatus(event.reacted, opts); + if (!status) return; + const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey); + + return { + id: event.id, + type: 'favourite', + created_at: nostrDate(event.created_at).toISOString(), + account, + status, + }; +} + +async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) { + if (event.reacted?.kind !== 1) return; + const status = await renderStatus(event.reacted, opts); + if (!status) return; + const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey); + + return { + id: event.id, + type: 'pleroma:emoji_reaction', + emoji: event.content, + created_at: nostrDate(event.created_at).toISOString(), + account, + status, + }; +} + +export { renderNotification }; diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 683c667a..21d380b3 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -14,12 +14,12 @@ import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments. import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { mediaDataSchema } from '@/schemas/nostr.ts'; -interface statusOpts { +interface RenderStatusOpts { viewerPubkey?: string; depth?: number; } -async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { +async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise { const { viewerPubkey, depth = 1 } = opts; if (depth > 2 || depth < 0) return null; @@ -117,7 +117,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { }; } -async function renderReblog(event: DittoEvent, opts: statusOpts) { +async function renderReblog(event: DittoEvent, opts: RenderStatusOpts) { const { viewerPubkey } = opts; if (!event.author) return; From ec7b3f835078e166c1646bd30be2887a3a9c23ec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 May 2024 15:02:05 -0500 Subject: [PATCH 058/252] followController: manually set `following: true` in the response --- src/controllers/api/accounts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 473dd63f..bdcd1a0d 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -240,6 +240,8 @@ const followController: AppController = async (c) => { ); const relationship = await renderRelationship(sourcePubkey, targetPubkey); + relationship.following = true; + return c.json(relationship); }; From 4c71dec6ceeece97053bf69076de1fd36a32c1a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 May 2024 15:26:46 -0500 Subject: [PATCH 059/252] Rename blocks to mutes in the API --- src/app.ts | 12 ++++++------ src/controllers/api/accounts.ts | 12 ++++++------ src/controllers/api/{blocks.ts => mutes.ts} | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) rename src/controllers/api/{blocks.ts => mutes.ts} (79%) diff --git a/src/app.ts b/src/app.ts index a3b507fe..5d29f8ca 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,25 +13,25 @@ import { accountLookupController, accountSearchController, accountStatusesController, - blockController, createAccountController, favouritesController, followController, followersController, followingController, + muteController, relationshipsController, - unblockController, unfollowController, + unmuteController, updateCredentialsController, verifyCredentialsController, } from '@/controllers/api/accounts.ts'; import { adminAccountsController } from '@/controllers/api/admin.ts'; import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts'; -import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { instanceController } from '@/controllers/api/instance.ts'; import { mediaController } from '@/controllers/api/media.ts'; +import { mutesController } from '@/controllers/api/mutes.ts'; import { notificationsController } from '@/controllers/api/notifications.ts'; import { createTokenController, oauthAuthorizeController, oauthController } from '@/controllers/api/oauth.ts'; import { @@ -139,8 +139,8 @@ app.patch('/api/v1/accounts/update_credentials', requirePubkey, updateCredential app.get('/api/v1/accounts/search', accountSearchController); app.get('/api/v1/accounts/lookup', accountLookupController); app.get('/api/v1/accounts/relationships', requirePubkey, relationshipsController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requirePubkey, blockController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requirePubkey, unblockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requirePubkey, muteController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requirePubkey, unmuteController); app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requirePubkey, followController); app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requirePubkey, unfollowController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/followers', followersController); @@ -182,7 +182,7 @@ app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }) app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/favourites', requirePubkey, favouritesController); app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); -app.get('/api/v1/blocks', requirePubkey, blocksController); +app.get('/api/v1/mutes', requirePubkey, mutesController); app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController); app.get('/api/v1/pleroma/admin/config', requireRole('admin'), configController); diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index bdcd1a0d..93455b7f 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -272,8 +272,8 @@ const followingController: AppController = async (c) => { return renderAccounts(c, pubkeys); }; -/** https://docs.joinmastodon.org/methods/accounts/#block */ -const blockController: AppController = async (c) => { +/** https://docs.joinmastodon.org/methods/accounts/#mute */ +const muteController: AppController = async (c) => { const sourcePubkey = c.get('pubkey')!; const targetPubkey = c.req.param('pubkey'); @@ -287,8 +287,8 @@ const blockController: AppController = async (c) => { return c.json(relationship); }; -/** https://docs.joinmastodon.org/methods/accounts/#unblock */ -const unblockController: AppController = async (c) => { +/** https://docs.joinmastodon.org/methods/accounts/#unmute */ +const unmuteController: AppController = async (c) => { const sourcePubkey = c.get('pubkey')!; const targetPubkey = c.req.param('pubkey'); @@ -328,15 +328,15 @@ export { accountLookupController, accountSearchController, accountStatusesController, - blockController, createAccountController, favouritesController, followController, followersController, followingController, + muteController, relationshipsController, - unblockController, unfollowController, + unmuteController, updateCredentialsController, verifyCredentialsController, }; diff --git a/src/controllers/api/blocks.ts b/src/controllers/api/mutes.ts similarity index 79% rename from src/controllers/api/blocks.ts rename to src/controllers/api/mutes.ts index 16fa5cb1..77b60e32 100644 --- a/src/controllers/api/blocks.ts +++ b/src/controllers/api/mutes.ts @@ -3,8 +3,8 @@ import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { renderAccounts } from '@/views.ts'; -/** https://docs.joinmastodon.org/methods/blocks/#get */ -const blocksController: AppController = async (c) => { +/** https://docs.joinmastodon.org/methods/mutes/#get */ +const mutesController: AppController = async (c) => { const pubkey = c.get('pubkey')!; const { signal } = c.req.raw; @@ -21,4 +21,4 @@ const blocksController: AppController = async (c) => { } }; -export { blocksController }; +export { mutesController }; From 09c596c9e4701cbdb22c578c9aeb5411a041a5b2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 May 2024 15:34:17 -0500 Subject: [PATCH 060/252] Add back block controllers, but 422 them --- src/app.ts | 6 ++++++ src/controllers/api/accounts.ts | 12 ++++++++++++ src/controllers/api/blocks.ts | 6 ++++++ 3 files changed, 24 insertions(+) create mode 100644 src/controllers/api/blocks.ts diff --git a/src/app.ts b/src/app.ts index 5d29f8ca..5fb376dc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -27,6 +27,7 @@ import { } from '@/controllers/api/accounts.ts'; import { adminAccountsController } from '@/controllers/api/admin.ts'; import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts'; +import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { instanceController } from '@/controllers/api/instance.ts'; @@ -77,6 +78,8 @@ import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; import { storeMiddleware } from '@/middleware/store.ts'; +import { blockController } from '@/controllers/api/accounts.ts'; +import { unblockController } from '@/controllers/api/accounts.ts'; interface AppEnv extends HonoEnv { Variables: { @@ -139,6 +142,8 @@ app.patch('/api/v1/accounts/update_credentials', requirePubkey, updateCredential app.get('/api/v1/accounts/search', accountSearchController); app.get('/api/v1/accounts/lookup', accountLookupController); app.get('/api/v1/accounts/relationships', requirePubkey, relationshipsController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requirePubkey, blockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requirePubkey, unblockController); app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requirePubkey, muteController); app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requirePubkey, unmuteController); app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requirePubkey, followController); @@ -182,6 +187,7 @@ app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }) app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/favourites', requirePubkey, favouritesController); app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); +app.get('/api/v1/blocks', requirePubkey, blocksController); app.get('/api/v1/mutes', requirePubkey, mutesController); app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController); diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 93455b7f..70e6c6dd 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -272,6 +272,16 @@ const followingController: AppController = async (c) => { return renderAccounts(c, pubkeys); }; +/** https://docs.joinmastodon.org/methods/accounts/#block */ +const blockController: AppController = (c) => { + return c.json({ error: 'Blocking is not supported by Nostr' }, 422); +}; + +/** https://docs.joinmastodon.org/methods/accounts/#unblock */ +const unblockController: AppController = (c) => { + return c.json({ error: 'Blocking is not supported by Nostr' }, 422); +}; + /** https://docs.joinmastodon.org/methods/accounts/#mute */ const muteController: AppController = async (c) => { const sourcePubkey = c.get('pubkey')!; @@ -328,6 +338,7 @@ export { accountLookupController, accountSearchController, accountStatusesController, + blockController, createAccountController, favouritesController, followController, @@ -335,6 +346,7 @@ export { followingController, muteController, relationshipsController, + unblockController, unfollowController, unmuteController, updateCredentialsController, diff --git a/src/controllers/api/blocks.ts b/src/controllers/api/blocks.ts new file mode 100644 index 00000000..b006a1da --- /dev/null +++ b/src/controllers/api/blocks.ts @@ -0,0 +1,6 @@ +import { AppController } from '@/app.ts'; + +/** https://docs.joinmastodon.org/methods/blocks/#get */ +export const blocksController: AppController = (c) => { + return c.json({ error: 'Blocking is not supported by Nostr' }, 422); +}; From ca5433bcc78985c055c7d9b7e29fb7500c4557e7 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 2 May 2024 20:21:58 -0300 Subject: [PATCH 061/252] refactor(reports): update code according to code review in MR 210 --- src/app.ts | 2 +- src/controllers/api/reports.ts | 19 +++++++++---------- src/views/mastodon/reports.ts | 14 +++++--------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/app.ts b/src/app.ts index 4c0e6579..ce53251e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -42,6 +42,7 @@ import { } from '@/controllers/api/pleroma.ts'; import { preferencesController } from '@/controllers/api/preferences.ts'; import { relayController } from '@/controllers/nostr/relay.ts'; +import { reportsController } from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { bookmarkController, @@ -77,7 +78,6 @@ import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; import { storeMiddleware } from '@/middleware/store.ts'; -import { reportsController } from '@/controllers/api/reports.ts'; interface AppEnv extends HonoEnv { Variables: { diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 8008eaed..9e1f9339 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -1,13 +1,14 @@ import { type AppController } from '@/app.ts'; -import { z } from 'zod'; import { createEvent, parseBody } from '@/utils/api.ts'; import { Conf } from '@/config.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { renderReports } from '@/views/mastodon/reports.ts'; +import { NSchema as n } from '@nostrify/nostrify'; +import { renderReport } from '@/views/mastodon/reports.ts'; +import { z } from 'zod'; const reportsSchema = z.object({ - account_id: z.string(), - status_ids: z.string().array().default([]), + account_id: n.id(), + status_ids: n.id().array().default([]), comment: z.string().max(1000).default(''), forward: z.boolean().default(false), category: z.string().default('other'), @@ -32,13 +33,11 @@ const reportsController: AppController = async (c) => { category, } = result.data; - const [personBeingReported] = await store.query([{ kinds: [0], authors: [account_id] }]); - if (!personBeingReported) { - return c.json({ error: 'Record not found' }, 404); + const [profile] = await store.query([{ kinds: [0], authors: [account_id] }]); + if (profile) { + await hydrateEvents({ events: [profile], storage: store }); } - await hydrateEvents({ events: [personBeingReported], storage: store }); - const event = await createEvent({ kind: 1984, content: JSON.stringify({ account_id, status_ids, comment, forward, category }), @@ -48,7 +47,7 @@ const reportsController: AppController = async (c) => { ], }, c); - return c.json(await renderReports(event, personBeingReported, {})); + return c.json(await renderReport(event, profile)); }; export { reportsController }; diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 67f9f6da..03291f7a 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -1,13 +1,9 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { renderAccount } from '@/views/mastodon/accounts.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { nostrDate } from '@/utils.ts'; -interface reportsOpts { - viewerPubkey?: string; -} - -/** Expects a `reportEvent` of kind 1984 and a `targetAccout` of kind 0 of the person being reported */ -async function renderReports(reportEvent: DittoEvent, targetAccout: DittoEvent, _opts: reportsOpts) { +/** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ +async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { const { account_id, status_ids, @@ -26,8 +22,8 @@ async function renderReports(reportEvent: DittoEvent, targetAccout: DittoEvent, created_at: nostrDate(reportEvent.created_at).toISOString(), status_ids, rules_ids: null, - target_account: await renderAccount(targetAccout), + target_account: profile ? await renderAccount(profile) : await accountFromPubkey(account_id), }; } -export { renderReports }; +export { renderReport }; From f2f0aa87419973464c683ad4bd6b7817b8911335 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 3 May 2024 09:52:25 -0300 Subject: [PATCH 062/252] fix(accountLookup): fix user not found by using 'accountFromPubkey' --- src/controllers/api/accounts.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 70e6c6dd..68edfbc9 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -17,6 +17,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; +import { bech32ToPubkey } from '@/utils.ts'; const usernameSchema = z .string().min(1).max(30) @@ -76,8 +77,13 @@ const accountLookupController: AppController = async (c) => { if (event) { return c.json(await renderAccount(event)); } - - return c.json({ error: 'Could not find user.' }, 404); + try { + const pubkey = bech32ToPubkey(decodeURIComponent(acct)) as string; + return c.json(await accountFromPubkey(pubkey)); + } catch (e) { + console.log(e); + return c.json({ error: 'Could not find user.' }, 404); + } }; const accountSearchController: AppController = async (c) => { From 705e8e7c312a7e1041697447af3527e22f8e899d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 13:23:00 -0500 Subject: [PATCH 063/252] PoolStore: implement NRelay --- deno.json | 2 +- src/pipeline.ts | 10 ++- src/storages.ts | 2 - src/storages/pool-store.ts | 124 ++++++++++++++++++++----------------- 4 files changed, 75 insertions(+), 63 deletions(-) diff --git a/deno.json b/deno.json index b4e834a0..6d611c01 100644 --- a/deno.json +++ b/deno.json @@ -19,7 +19,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.15.0", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.17.1", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/pipeline.ts b/src/pipeline.ts index b1936173..a3420fea 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -183,15 +183,19 @@ async function trackHashtags(event: NostrEvent): Promise { /** Queue related events to fetch. */ async function fetchRelatedEvents(event: DittoEvent, signal: AbortSignal) { - if (!event.user) { - Storages.reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }).catch(() => {}); + if (!event.author) { + Storages.reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }) + .then((event) => handleEvent(event, AbortSignal.timeout(1000))) + .catch(() => {}); } for (const [name, id, relay] of event.tags) { if (name === 'e') { const { count } = await Storages.cache.count([{ ids: [id] }]); if (!count) { - Storages.reqmeister.req({ ids: [id] }, { relays: [relay] }).catch(() => {}); + Storages.reqmeister.req({ ids: [id] }, { relays: [relay] }) + .then((event) => handleEvent(event, AbortSignal.timeout(1000))) + .catch(() => {}); } } } diff --git a/src/storages.ts b/src/storages.ts index 21fe9d50..a51cbd19 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -1,7 +1,6 @@ import { NCache } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; -import * as pipeline from '@/pipeline.ts'; import { activeRelays, pool } from '@/pool.ts'; import { EventsDB } from '@/storages/events-db.ts'; import { Optimizer } from '@/storages/optimizer.ts'; @@ -52,7 +51,6 @@ export class Storages { this._client = new PoolStore({ pool, relays: activeRelays, - publisher: pipeline, }); } return this._client; diff --git a/src/storages/pool-store.ts b/src/storages/pool-store.ts index 953720b0..73a10e2f 100644 --- a/src/storages/pool-store.ts +++ b/src/storages/pool-store.ts @@ -1,7 +1,16 @@ -import { NostrEvent, NostrFilter, NSet, NStore } from '@nostrify/nostrify'; +import { + NostrEvent, + NostrFilter, + NostrRelayCLOSED, + NostrRelayEOSE, + NostrRelayEVENT, + NRelay, + NSet, +} from '@nostrify/nostrify'; +import { Machina } from '@nostrify/nostrify/utils'; import Debug from '@soapbox/stickynotes/debug'; import { RelayPoolWorker } from 'nostr-relaypool'; -import { matchFilters } from 'nostr-tools'; +import { getFilterLimit, matchFilters } from 'nostr-tools'; import { normalizeFilters } from '@/filter.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; @@ -12,23 +21,16 @@ import { Conf } from '@/config.ts'; interface PoolStoreOpts { pool: InstanceType; relays: WebSocket['url'][]; - publisher: { - handleEvent(event: NostrEvent, signal: AbortSignal): Promise; - }; } -class PoolStore implements NStore { - #debug = Debug('ditto:client'); - #pool: InstanceType; - #relays: WebSocket['url'][]; - #publisher: { - handleEvent(event: NostrEvent, signal: AbortSignal): Promise; - }; +class PoolStore implements NRelay { + private debug = Debug('ditto:client'); + private pool: InstanceType; + private relays: WebSocket['url'][]; constructor(opts: PoolStoreOpts) { - this.#pool = opts.pool; - this.#relays = opts.relays; - this.#publisher = opts.publisher; + this.pool = opts.pool; + this.relays = opts.relays; } async event(event: NostrEvent, opts: { signal?: AbortSignal } = {}): Promise { @@ -40,58 +42,66 @@ class PoolStore implements NStore { const relays = [...relaySet].slice(0, 4); event = purifyEvent(event); - this.#debug('EVENT', event, relays); + this.debug('EVENT', event, relays); - this.#pool.publish(event, relays); + this.pool.publish(event, relays); return Promise.resolve(); } - query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { - if (opts.signal?.aborted) return Promise.reject(abortError()); + async *req( + filters: NostrFilter[], + opts: { signal?: AbortSignal; limit?: number } = {}, + ): AsyncIterable { + if (opts.signal?.aborted) return; filters = normalizeFilters(filters); - this.#debug('REQ', JSON.stringify(filters)); - if (!filters.length) return Promise.resolve([]); + if (!filters.length) return; - return new Promise((resolve, reject) => { - const results = new NSet(); + this.debug('REQ', JSON.stringify(filters)); - const unsub = this.#pool.subscribe( - filters, - this.#relays, - (event: NostrEvent | null) => { - if (event && matchFilters(filters, event)) { - this.#publisher.handleEvent(event, AbortSignal.timeout(1000)).catch(() => {}); - results.add({ - id: event.id, - kind: event.kind, - pubkey: event.pubkey, - content: event.content, - tags: event.tags, - created_at: event.created_at, - sig: event.sig, - }); - } - if (typeof opts.limit === 'number' && results.size >= opts.limit) { - unsub(); - resolve([...results]); - } - }, - undefined, - () => { - unsub(); - resolve([...results]); - }, - ); + const uuid = crypto.randomUUID(); + const machina = new Machina(opts.signal); - const onAbort = () => { - unsub(); - reject(abortError()); - opts.signal?.removeEventListener('abort', onAbort); - }; + const unsub = this.pool.subscribe( + filters, + this.relays, + (event: NostrEvent | null) => { + if (event && matchFilters(filters, event)) { + machina.push(['EVENT', uuid, purifyEvent(event)]); + } + }, + undefined, + () => { + machina.push(['EOSE', uuid]); + }, + ); - opts.signal?.addEventListener('abort', onAbort); - }); + try { + for await (const msg of machina) { + yield msg; + } + } finally { + unsub(); + } + } + + async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { + const events = new NSet(); + + const limit = filters.reduce((result, filter) => result + getFilterLimit(filter), 0); + if (limit === 0) return []; + + for await (const msg of this.req(filters, opts)) { + if (msg[0] === 'EOSE') break; + if (msg[0] === 'EVENT') events.add(msg[2]); + if (msg[0] === 'CLOSED') throw new Error('Subscription closed'); + + if (events.size >= limit) { + break; + } + } + + return [...events]; } } From 091392088f980e32d0e327fa7dc59f5620bcb034 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 13:25:18 -0500 Subject: [PATCH 064/252] PoolStore: simplify req --- src/storages/pool-store.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/storages/pool-store.ts b/src/storages/pool-store.ts index 73a10e2f..9f452058 100644 --- a/src/storages/pool-store.ts +++ b/src/storages/pool-store.ts @@ -12,11 +12,10 @@ import Debug from '@soapbox/stickynotes/debug'; import { RelayPoolWorker } from 'nostr-relaypool'; import { getFilterLimit, matchFilters } from 'nostr-tools'; -import { normalizeFilters } from '@/filter.ts'; +import { Conf } from '@/config.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { abortError } from '@/utils/abort.ts'; import { getRelays } from '@/utils/outbox.ts'; -import { Conf } from '@/config.ts'; interface PoolStoreOpts { pool: InstanceType; @@ -52,11 +51,6 @@ class PoolStore implements NRelay { filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}, ): AsyncIterable { - if (opts.signal?.aborted) return; - - filters = normalizeFilters(filters); - if (!filters.length) return; - this.debug('REQ', JSON.stringify(filters)); const uuid = crypto.randomUUID(); From 6b20104327e4b7f4127b89bc8fa60263dee553be Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 13:26:26 -0500 Subject: [PATCH 065/252] filter: use getFilterLimit from nostr-tools --- src/filter.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/filter.ts b/src/filter.ts index 59b02982..fd698c4e 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,9 +1,8 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import stringifyStable from 'fast-stable-stringify'; +import { getFilterLimit } from 'nostr-tools'; import { z } from 'zod'; -import { isReplaceableKind } from '@/kinds.ts'; - /** Microfilter to get one specific event by ID. */ type IdMicrofilter = { ids: [NostrEvent['id']] }; /** Microfilter to get an author. */ @@ -50,22 +49,6 @@ function isMicrofilter(filter: NostrFilter): filter is MicroFilter { return microFilterSchema.safeParse(filter).success; } -/** Calculate the intrinsic limit of a filter. */ -function getFilterLimit(filter: NostrFilter): number { - if (filter.ids && !filter.ids.length) return 0; - if (filter.kinds && !filter.kinds.length) return 0; - if (filter.authors && !filter.authors.length) return 0; - - return Math.min( - Math.max(0, filter.limit ?? Infinity), - filter.ids?.length ?? Infinity, - filter.authors?.length && - filter.kinds?.every((kind) => isReplaceableKind(kind)) - ? filter.authors.length * filter.kinds.length - : Infinity, - ); -} - /** Returns true if the filter could potentially return any stored events at all. */ function canFilter(filter: NostrFilter): boolean { return getFilterLimit(filter) > 0; From e9c5ef89ff984d33312cef7861723f35d9e5222f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 13:33:50 -0500 Subject: [PATCH 066/252] Reqmeister: improve API and fetching logic (untested) --- src/pipeline.ts | 16 +++++++++------- src/storages/reqmeister.ts | 17 ++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index a3420fea..f28b886d 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -59,7 +59,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { } /** Queue related events to fetch. */ -async function fetchRelatedEvents(event: DittoEvent, signal: AbortSignal) { +async function fetchRelatedEvents(event: DittoEvent) { if (!event.author) { - Storages.reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }) - .then((event) => handleEvent(event, AbortSignal.timeout(1000))) + const signal = AbortSignal.timeout(3000); + Storages.reqmeister.query([{ kinds: [0], authors: [event.pubkey] }], { signal }) + .then((events) => events.forEach((event) => handleEvent(event, signal))) .catch(() => {}); } - for (const [name, id, relay] of event.tags) { + for (const [name, id] of event.tags) { if (name === 'e') { const { count } = await Storages.cache.count([{ ids: [id] }]); if (!count) { - Storages.reqmeister.req({ ids: [id] }, { relays: [relay] }) - .then((event) => handleEvent(event, AbortSignal.timeout(1000))) + const signal = AbortSignal.timeout(3000); + Storages.reqmeister.query([{ ids: [id] }], { signal }) + .then((events) => events.forEach((event) => handleEvent(event, signal))) .catch(() => {}); } } diff --git a/src/storages/reqmeister.ts b/src/storages/reqmeister.ts index eede2007..e3833d37 100644 --- a/src/storages/reqmeister.ts +++ b/src/storages/reqmeister.ts @@ -82,7 +82,7 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: NostrEvent) this.#perform(); } - req(filter: MicroFilter, opts: ReqmeisterReqOpts = {}): Promise { + private fetch(filter: MicroFilter, opts: ReqmeisterReqOpts = {}): Promise { const { relays = [], signal = AbortSignal.timeout(this.#opts.timeout ?? 1000), @@ -120,12 +120,7 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: NostrEvent) return Promise.resolve(); } - isWanted(event: NostrEvent): boolean { - const filterId = getFilterId(eventToMicroFilter(event)); - return this.#queue.some(([id]) => id === filterId); - } - - query(filters: NostrFilter[], opts?: { signal?: AbortSignal }): Promise { + async query(filters: NostrFilter[], opts?: { signal?: AbortSignal }): Promise { if (opts?.signal?.aborted) return Promise.reject(abortError()); this.#debug('REQ', JSON.stringify(filters)); @@ -133,12 +128,16 @@ class Reqmeister extends EventEmitter<{ [filterId: string]: (event: NostrEvent) const promises = filters.reduce[]>((result, filter) => { if (isMicrofilter(filter)) { - result.push(this.req(filter, opts)); + result.push(this.fetch(filter, opts)); } return result; }, []); - return Promise.all(promises); + const results = await Promise.allSettled(promises); + + return results + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); } } From 2b2499849fb0116287099c56f1aa64d6492b769d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 13:52:11 -0500 Subject: [PATCH 067/252] pipeline: fix reqmeister crash, probably --- src/pipeline.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index f28b886d..3eb8913d 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -186,7 +186,7 @@ async function fetchRelatedEvents(event: DittoEvent) { if (!event.author) { const signal = AbortSignal.timeout(3000); Storages.reqmeister.query([{ kinds: [0], authors: [event.pubkey] }], { signal }) - .then((events) => events.forEach((event) => handleEvent(event, signal))) + .then((events) => Promise.allSettled(events.map((event) => handleEvent(event, signal)))) .catch(() => {}); } @@ -196,7 +196,7 @@ async function fetchRelatedEvents(event: DittoEvent) { if (!count) { const signal = AbortSignal.timeout(3000); Storages.reqmeister.query([{ ids: [id] }], { signal }) - .then((events) => events.forEach((event) => handleEvent(event, signal))) + .then((events) => Promise.allSettled(events.map((event) => handleEvent(event, signal)))) .catch(() => {}); } } From 8e178338b74f42e8d21372e4243cc8ab70e3dc7d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 16:17:36 -0500 Subject: [PATCH 068/252] Implement Markers API --- src/app.ts | 4 +++ src/controllers/api/markers.ts | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/controllers/api/markers.ts diff --git a/src/app.ts b/src/app.ts index df8919d2..d80bf37a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,6 +31,7 @@ import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { instanceController } from '@/controllers/api/instance.ts'; +import { markersController, updateMarkersController } from '@/controllers/api/markers.ts'; import { mediaController } from '@/controllers/api/media.ts'; import { mutesController } from '@/controllers/api/mutes.ts'; import { notificationsController } from '@/controllers/api/notifications.ts'; @@ -191,6 +192,9 @@ app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); app.get('/api/v1/blocks', requirePubkey, blocksController); app.get('/api/v1/mutes', requirePubkey, mutesController); +app.get('/api/v1/markers', requireProof(), markersController); +app.post('/api/v1/markers', requireProof(), updateMarkersController); + app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController); app.get('/api/v1/pleroma/admin/config', requireRole('admin'), configController); app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); diff --git a/src/controllers/api/markers.ts b/src/controllers/api/markers.ts new file mode 100644 index 00000000..cd90e334 --- /dev/null +++ b/src/controllers/api/markers.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; + +import { AppController } from '@/app.ts'; +import { parseBody } from '@/utils/api.ts'; + +const kv = await Deno.openKv(); + +interface Marker { + last_read_id: string; + version: number; + updated_at: string; +} + +export const markersController: AppController = async (c) => { + const pubkey = c.get('pubkey')!; + const timelines = c.req.queries('timeline[]') ?? []; + + const results = await kv.getMany( + timelines.map((timeline) => ['markers', pubkey, timeline]), + ); + + const marker = results.reduce>((acc, { key, value }) => { + if (value) { + const timeline = key[key.length - 1] as string; + acc[timeline] = value; + } + return acc; + }, {}); + + return c.json(marker); +}; + +const markerDataSchema = z.object({ + last_read_id: z.string(), +}); + +export const updateMarkersController: AppController = async (c) => { + const pubkey = c.get('pubkey')!; + const record = z.record(z.string(), markerDataSchema).parse(await parseBody(c.req.raw)); + const timelines = Object.keys(record); + + const markers: Record = {}; + + const entries = await kv.getMany( + timelines.map((timeline) => ['markers', pubkey, timeline]), + ); + + for (const timeline of timelines) { + const last = entries.find(({ key }) => key[key.length - 1] === timeline); + + const marker: Marker = { + last_read_id: record[timeline].last_read_id, + version: last?.value ? last.value.version + 1 : 1, + updated_at: new Date().toISOString(), + }; + + await kv.set(['markers', pubkey, timeline], marker); + markers[timeline] = marker; + } + + return c.json(markers); +}; From a2c5b5e61d048a22e55ae8237f78edb53a4ffc1d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 16:20:07 -0500 Subject: [PATCH 069/252] Markers: only allow 'home' and 'notifications' markers --- src/controllers/api/markers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/markers.ts b/src/controllers/api/markers.ts index cd90e334..ce1c4ec3 100644 --- a/src/controllers/api/markers.ts +++ b/src/controllers/api/markers.ts @@ -5,6 +5,8 @@ import { parseBody } from '@/utils/api.ts'; const kv = await Deno.openKv(); +type Timeline = 'home' | 'notifications'; + interface Marker { last_read_id: string; version: number; @@ -36,8 +38,8 @@ const markerDataSchema = z.object({ export const updateMarkersController: AppController = async (c) => { const pubkey = c.get('pubkey')!; - const record = z.record(z.string(), markerDataSchema).parse(await parseBody(c.req.raw)); - const timelines = Object.keys(record); + const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw)); + const timelines = Object.keys(record) as Timeline[]; const markers: Record = {}; @@ -49,7 +51,7 @@ export const updateMarkersController: AppController = async (c) => { const last = entries.find(({ key }) => key[key.length - 1] === timeline); const marker: Marker = { - last_read_id: record[timeline].last_read_id, + last_read_id: record[timeline]!.last_read_id, version: last?.value ? last.value.version + 1 : 1, updated_at: new Date().toISOString(), }; From 7efd5c1822ae23807ef578894f924040e4d24e95 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 17:09:20 -0500 Subject: [PATCH 070/252] Clean up "not implemented" endpoints --- src/app.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index d80bf37a..b382ceab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -208,9 +208,7 @@ app.post('/api/v1/reports', requirePubkey, reportsController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); app.get('/api/v1/filters', emptyArrayController); -app.get('/api/v1/mutes', emptyArrayController); app.get('/api/v1/domain_blocks', emptyArrayController); -app.get('/api/v1/markers', emptyObjectController); app.get('/api/v1/conversations', emptyArrayController); app.get('/api/v1/lists', emptyArrayController); From 5001567b00f7e1d39ecb0d060719c8d4920db98e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 17:20:35 -0500 Subject: [PATCH 071/252] Streaming: temporarily remove UserStore (allow blocked posts through) --- src/controllers/api/streaming.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 965855c3..8d22d5cc 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -9,7 +9,6 @@ import { bech32ToPubkey } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; -import { UserStore } from '@/storages/UserStore.ts'; const debug = Debug('ditto:streaming'); @@ -68,17 +67,14 @@ const streamingController: AppController = (c) => { const filter = await topicToFilter(stream, c.req.query(), pubkey); if (!filter) return; - const store = pubkey ? new UserStore(pubkey, Storages.admin) : Storages.admin; - try { for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { - const [event] = await store.query([{ ids: [msg[2].id] }]); - if (!event) continue; + const event = msg[2]; await hydrateEvents({ events: [event], - storage: store, + storage: Storages.admin, signal: AbortSignal.timeout(1000), }); From 0f3fbbcb28b6dcfbc474cc2096cf4c1224476103 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 18:21:40 -0500 Subject: [PATCH 072/252] Start suggestions API --- src/app.ts | 4 +++ src/controllers/api/suggestions.ts | 50 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/controllers/api/suggestions.ts diff --git a/src/app.ts b/src/app.ts index b382ceab..ddc902fc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -63,6 +63,7 @@ import { zapController, } from '@/controllers/api/statuses.ts'; import { streamingController } from '@/controllers/api/streaming.ts'; +import { suggestionsV1Controller, suggestionsV2Controller } from '@/controllers/api/suggestions.ts'; import { hashtagTimelineController, homeTimelineController, @@ -186,6 +187,9 @@ app.get('/api/pleroma/frontend_configurations', frontendConfigController); app.get('/api/v1/trends/tags', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); +app.get('/api/v1/suggestions', suggestionsV1Controller); +app.get('/api/v2/suggestions', suggestionsV2Controller); + app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/favourites', requirePubkey, favouritesController); app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts new file mode 100644 index 00000000..b85f05df --- /dev/null +++ b/src/controllers/api/suggestions.ts @@ -0,0 +1,50 @@ +import { NStore } from '@nostrify/nostrify'; + +import { AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; +import { getTagSet } from '@/tags.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; + +export const suggestionsV1Controller: AppController = async (c) => { + const store = c.get('store'); + const signal = c.req.raw.signal; + const accounts = await renderSuggestedAccounts(store, signal); + + return c.json(accounts); +}; + +export const suggestionsV2Controller: AppController = async (c) => { + const store = c.get('store'); + const signal = c.req.raw.signal; + const accounts = await renderSuggestedAccounts(store, signal); + + const suggestions = accounts.map((account) => ({ + source: 'staff', + account, + })); + + return c.json(suggestions); +}; + +async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) { + const [follows] = await store.query( + [{ kinds: [3], authors: [Conf.pubkey], limit: 1 }], + { signal }, + ); + + const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')]; + + const profiles = await store.query( + [{ kinds: [1], authors: pubkeys }], + { signal }, + ) + .then((events) => hydrateEvents({ events, storage: store, signal })); + + const accounts = await Promise.all(pubkeys.map((pubkey) => { + const profile = profiles.find((event) => event.pubkey === pubkey); + return profile ? renderAccount(profile) : accountFromPubkey(pubkey); + })); + + return accounts.filter(Boolean); +} From 4ee4266843c58ff509cf317b7b0df45d3f37b90f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 18:28:07 -0500 Subject: [PATCH 073/252] instance: add 'v2_suggestions' to features --- src/controllers/api/instance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 188d68f5..70f38e14 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -45,6 +45,7 @@ const instanceController: AppController = async (c) => { 'mastodon_api_streaming', 'exposable_reactions', 'quote_posting', + 'v2_suggestions', ], }, }, From e25372313b4c17dfea8f45c96eded8be2592d287 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 18:32:35 -0500 Subject: [PATCH 074/252] suggestions: fix profile lookup, limit to 20 items for now --- src/controllers/api/suggestions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index b85f05df..bde09165 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -33,10 +33,11 @@ async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) { { signal }, ); - const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')]; + // TODO: pagination + const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')].slice(0, 20); const profiles = await store.query( - [{ kinds: [1], authors: pubkeys }], + [{ kinds: [0], authors: pubkeys, limit: pubkeys.length }], { signal }, ) .then((events) => hydrateEvents({ events, storage: store, signal })); From 0a3be0da587425f0eafc38040ce93f3ce76df144 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 3 May 2024 21:22:53 -0500 Subject: [PATCH 075/252] Notifications: fix Favourites and EmojiReacts not being displayed --- src/storages/hydrate.ts | 7 +++++++ src/views/mastodon/notifications.ts | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 716f2518..61d82855 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -76,6 +76,13 @@ function assembleEvents( } } + if (event.kind === 7) { + const id = event.tags.find(([name]) => name === 'e')?.[1]; + if (id) { + event.reacted = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + } + } + if (event.kind === 1) { const id = event.tags.find(([name]) => name === 'q')?.[1]; if (id) { diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts index 266b77b0..5b618d79 100644 --- a/src/views/mastodon/notifications.ts +++ b/src/views/mastodon/notifications.ts @@ -2,6 +2,7 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { nostrDate } from '@/utils.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { NostrEvent } from '@nostrify/nostrify'; interface RenderNotificationOpts { viewerPubkey: string; @@ -32,7 +33,7 @@ async function renderMention(event: DittoEvent, opts: RenderNotificationOpts) { if (!status) return; return { - id: event.id, + id: notificationId(event), type: 'mention', created_at: nostrDate(event.created_at).toISOString(), account: status.account, @@ -47,7 +48,7 @@ async function renderReblog(event: DittoEvent, opts: RenderNotificationOpts) { const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey); return { - id: event.id, + id: notificationId(event), type: 'reblog', created_at: nostrDate(event.created_at).toISOString(), account, @@ -62,7 +63,7 @@ async function renderFavourite(event: DittoEvent, opts: RenderNotificationOpts) const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey); return { - id: event.id, + id: notificationId(event), type: 'favourite', created_at: nostrDate(event.created_at).toISOString(), account, @@ -77,7 +78,7 @@ async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) { const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey); return { - id: event.id, + id: notificationId(event), type: 'pleroma:emoji_reaction', emoji: event.content, created_at: nostrDate(event.created_at).toISOString(), @@ -86,4 +87,9 @@ async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) { }; } +/** This helps notifications be sorted in the correct order. */ +function notificationId({ id, created_at }: NostrEvent): string { + return `${created_at}-${id}`; +} + export { renderNotification }; From f08211e2a1240cb5731fd55094ea8f70cdb5b986 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 10:29:08 -0300 Subject: [PATCH 076/252] refactor(admin-accounts): resolve import specifier via the active import map --- src/views/mastodon/admin-accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts index 79147764..8023161e 100644 --- a/src/views/mastodon/admin-accounts.ts +++ b/src/views/mastodon/admin-accounts.ts @@ -1,7 +1,7 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { nostrDate } from '@/utils.ts'; -import { accountFromPubkey, renderAccount } from './accounts.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; async function renderAdminAccount(event: DittoEvent) { const d = event.tags.find(([name]) => name === 'd')?.[1]!; From 3770d8a0dd11833949327b290db1e3b6f7cacb17 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 4 May 2024 13:14:03 -0500 Subject: [PATCH 077/252] UnattachedMedia: return early when querying nothing --- src/db/unattached-media.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 415c110d..960abe8c 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -54,15 +54,18 @@ function deleteUnattachedMediaByUrl(url: string) { } /** Get unattached media by IDs. */ -function getUnattachedMediaByIds(ids: string[]) { +// deno-lint-ignore require-await +async function getUnattachedMediaByIds(ids: string[]) { + if (!ids.length) return []; return selectUnattachedMediaQuery() .where('id', 'in', ids) .execute(); } /** Delete rows as an event with media is being created. */ -function deleteAttachedMedia(pubkey: string, urls: string[]) { - return db.deleteFrom('unattached_media') +async function deleteAttachedMedia(pubkey: string, urls: string[]): Promise { + if (!urls.length) return; + await db.deleteFrom('unattached_media') .where('pubkey', '=', pubkey) .where('url', 'in', urls) .execute(); From b57188943fb05de562fbadeae16bacf784ab760b Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 16:29:31 -0300 Subject: [PATCH 078/252] feat: renderAdminAccount() supports both kind 0 & kind 30361 --- src/views/mastodon/admin-accounts.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts index 8023161e..90723f4c 100644 --- a/src/views/mastodon/admin-accounts.ts +++ b/src/views/mastodon/admin-accounts.ts @@ -3,9 +3,16 @@ import { nostrDate } from '@/utils.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; +/** Expects a kind 0 fully hydrated or a kind 30361 hydrated with `d_author` */ async function renderAdminAccount(event: DittoEvent) { - const d = event.tags.find(([name]) => name === 'd')?.[1]!; - const account = event.d_author ? await renderAccount({ ...event.d_author, user: event }) : await accountFromPubkey(d); + let account; + + if (event.kind === 0 && event.user) { + account = await renderAccount(event); + } else { + const d = event.tags.find(([name]) => name === 'd')?.[1]!; + account = event.d_author ? await renderAccount({ ...event.d_author, user: event }) : await accountFromPubkey(d); + } return { id: account.id, From af7b83cf8ac2c87c21f582043c92f435ab0eaefb Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 20:10:18 -0300 Subject: [PATCH 079/252] feat: create /api/v1/admin/reports endpoint & controller --- src/app.ts | 3 ++- src/controllers/api/reports.ts | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index df8919d2..73baa184 100644 --- a/src/app.ts +++ b/src/app.ts @@ -43,7 +43,7 @@ import { } from '@/controllers/api/pleroma.ts'; import { preferencesController } from '@/controllers/api/preferences.ts'; import { relayController } from '@/controllers/nostr/relay.ts'; -import { reportsController } from '@/controllers/api/reports.ts'; +import { reportsController, viewAllReportsController } from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { bookmarkController, @@ -200,6 +200,7 @@ app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysControlle app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); app.post('/api/v1/reports', requirePubkey, reportsController); +app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), viewAllReportsController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 9e1f9339..351c1c2c 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -5,6 +5,7 @@ import { hydrateEvents } from '@/storages/hydrate.ts'; import { NSchema as n } from '@nostrify/nostrify'; import { renderReport } from '@/views/mastodon/reports.ts'; import { z } from 'zod'; +import { renderAdminReport } from '@/views/mastodon/reports.ts'; const reportsSchema = z.object({ account_id: n.id(), @@ -50,4 +51,20 @@ const reportsController: AppController = async (c) => { return c.json(await renderReport(event, profile)); }; -export { reportsController }; +/** https://docs.joinmastodon.org/methods/admin/reports/#get */ +const viewAllReportsController: AppController = async (c) => { + const store = c.get('store'); + const allMastodonReports = []; + + const allReports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]); + + await hydrateEvents({ storage: store, events: allReports, signal: AbortSignal.timeout(2000) }); + + for (const report of allReports) { + allMastodonReports.push(await renderAdminReport(report, { viewerPubkey: c.get('pubkey') })); + } + + return c.json(allMastodonReports); +}; + +export { reportsController, viewAllReportsController }; From b8df95408bb665d473c3064af9724dc20b041f61 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 20:11:29 -0300 Subject: [PATCH 080/252] feat: add target_account & reported_statuses to DittoEvent type --- src/interfaces/DittoEvent.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 2ef0bb26..f6810d3a 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -25,4 +25,14 @@ export interface DittoEvent extends NostrEvent { repost?: DittoEvent; quote_repost?: DittoEvent; reacted?: DittoEvent; + /** The account being reported. + * Must be a kind 0 hydrated. + * https://github.com/nostr-protocol/nips/blob/master/56.md + */ + target_account?: DittoEvent; + /** The statuses being reported. + * Nostr only support reporting one note, the array of reported notes can be found in the `status_ids` field after JSON.parsing the `content` of a kind 1984. + * https://github.com/nostr-protocol/nips/blob/master/56.md + */ + reported_statuses?: DittoEvent[]; } From 8a7f0892d715416941a6d061a7c0031b7553e939 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 20:12:34 -0300 Subject: [PATCH 081/252] fix: check if event is not undefined in renderAdminAccount --- src/views/mastodon/admin-accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts index 90723f4c..b344608a 100644 --- a/src/views/mastodon/admin-accounts.ts +++ b/src/views/mastodon/admin-accounts.ts @@ -7,7 +7,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; async function renderAdminAccount(event: DittoEvent) { let account; - if (event.kind === 0 && event.user) { + if (event && event.kind === 0 && event.user) { account = await renderAccount(event); } else { const d = event.tags.find(([name]) => name === 'd')?.[1]!; From 4d5d4868ce2fa81d95b4d2f674d4c8eaab076f52 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 20:14:39 -0300 Subject: [PATCH 082/252] feat: create renderAdminReport() func --- src/views/mastodon/reports.ts | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 03291f7a..50fea467 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -1,6 +1,8 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { nostrDate } from '@/utils.ts'; +import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; /** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { @@ -26,4 +28,44 @@ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { }; } -export { renderReport }; +interface RenderAdminReportOpts { + viewerPubkey?: string; +} + +/** Admin-level information about a filed report. + * Expects an event of kind 1984 fully hydrated. + * https://docs.joinmastodon.org/entities/Admin_Report */ +async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminReportOpts) { + const { viewerPubkey } = opts; + + const { + comment, + forward, + category, + } = JSON.parse(reportEvent.content); + + const statuses = []; + if (reportEvent.reported_statuses) { + for (const status of reportEvent.reported_statuses) { + statuses.push(await renderStatus(status, { viewerPubkey })); + } + } + + return { + id: reportEvent.id, + action_taken: false, + action_taken_at: null, + category, + comment, + forwarded: forward, + created_at: nostrDate(reportEvent.created_at).toISOString(), + account: await renderAdminAccount(reportEvent.author as DittoEvent), + target_account: await renderAdminAccount(reportEvent.target_account as DittoEvent), + assigned_account: null, + action_taken_by_account: null, + statuses, + rule: [], + }; +} + +export { renderAdminReport, renderReport }; From 14802dd38a913e34a0a778fb55d952b0f4d2dc06 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 4 May 2024 20:18:36 -0300 Subject: [PATCH 083/252] feat(hydrate): create gatherTargetAccounts() & gatherReportedStatuses() --- src/storages/hydrate.ts | 79 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 716f2518..383a133a 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -42,6 +42,14 @@ async function hydrateEvents(opts: HydrateOpts): Promise { cache.push(event); } + for (const event of await gatherTargetAccounts({ events: cache, storage, signal })) { + cache.push(event); + } + + for (const event of await gatherReportedStatuses({ events: cache, storage, signal })) { + cache.push(event); + } + const stats = { authors: await gatherAuthorStats(cache), events: await gatherEventStats(cache), @@ -69,6 +77,13 @@ function assembleEvents( event.author = b.find((e) => matchFilter({ kinds: [0], authors: [event.pubkey] }, e)); event.user = b.find((e) => matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e)); + if (event.kind === 1) { + const id = event.tags.find(([name]) => name === 'q')?.[1]; + if (id) { + event.quote_repost = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + } + } + if (event.kind === 6) { const id = event.tags.find(([name]) => name === 'e')?.[1]; if (id) { @@ -76,10 +91,27 @@ function assembleEvents( } } - if (event.kind === 1) { - const id = event.tags.find(([name]) => name === 'q')?.[1]; - if (id) { - event.quote_repost = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + if (event.kind === 1984) { + const targetAccountId = event.tags.find(([name]) => name === 'p')?.[1]; + if (targetAccountId) { + event.target_account = b.find((e) => matchFilter({ kinds: [0], authors: [targetAccountId] }, e)); + if (event.target_account) { + event.target_account.user = b.find((e) => + matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e) + ); + } + } + const reportedEvents: DittoEvent[] = []; + + const { status_ids } = JSON.parse(event.content); + if (status_ids && Array.isArray(status_ids)) { + for (const id of status_ids) { + if (typeof id === 'string') { + const reportedEvent = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + if (reportedEvent) reportedEvents.push(reportedEvent); + } + } + event.reported_statuses = reportedEvents; } } @@ -167,6 +199,45 @@ function gatherUsers({ events, storage, signal }: HydrateOpts): Promise { + const ids = new Set(); + for (const event of events) { + if (event.kind === 1984) { + const { status_ids } = JSON.parse(event.content); + if (status_ids && Array.isArray(status_ids)) { + for (const id of status_ids) { + if (typeof id === 'string') ids.add(id); + } + } + } + } + + return storage.query( + [{ kinds: [1], ids: [...ids], limit: ids.size }], + { signal }, + ); +} + +/** Collect target accounts (the ones being reported) from the events. */ +function gatherTargetAccounts({ events, storage, signal }: HydrateOpts): Promise { + const pubkeys = new Set(); + + for (const event of events) { + if (event.kind === 1984) { + const pubkey = event.tags.find(([name]) => name === 'p')?.[1]; + if (pubkey) { + pubkeys.add(pubkey); + } + } + } + + return storage.query( + [{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }], + { signal }, + ); +} + /** Collect author stats from the events. */ function gatherAuthorStats(events: DittoEvent[]): Promise { const pubkeys = new Set( From 0e115481d936e98656b2d48f53247445e26a4e1f Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 5 May 2024 20:26:20 +0530 Subject: [PATCH 084/252] generate junit reports for GitLab CI --- .gitlab-ci.yml | 8 +++++++- deno.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5cff577a..a9dee457 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,4 +22,10 @@ test: stage: test script: deno task test variables: - DITTO_NSEC: nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz \ No newline at end of file + DITTO_NSEC: nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz + artifacts: + when: always + paths: + - deno-test.xml + reports: + junit: deno-test.xml \ No newline at end of file diff --git a/deno.json b/deno.json index 6d611c01..66263f56 100644 --- a/deno.json +++ b/deno.json @@ -5,7 +5,7 @@ "start": "deno run -A src/server.ts", "dev": "deno run -A --watch src/server.ts", "debug": "deno run -A --inspect src/server.ts", - "test": "DATABASE_URL=\"sqlite://:memory:\" deno test -A", + "test": "DATABASE_URL=\"sqlite://:memory:\" deno test -A --junit-path=./deno-test.xml", "check": "deno check src/server.ts", "nsec": "deno run scripts/nsec.ts", "admin:event": "deno run -A scripts/admin-event.ts", From 7890504adda95db8cb61aa0d2d660bab54328319 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sun, 5 May 2024 14:20:13 -0300 Subject: [PATCH 085/252] refactor: view info about all reports --- src/controllers/api/reports.ts | 25 ++++++++++--------------- src/interfaces/DittoEvent.ts | 9 ++++----- src/storages/hydrate.ts | 21 ++++++++------------- src/views/mastodon/admin-accounts.ts | 11 ++--------- src/views/mastodon/reports.ts | 6 +++--- 5 files changed, 27 insertions(+), 45 deletions(-) diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 351c1c2c..b49486c3 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -1,11 +1,12 @@ -import { type AppController } from '@/app.ts'; -import { createEvent, parseBody } from '@/utils/api.ts'; -import { Conf } from '@/config.ts'; -import { hydrateEvents } from '@/storages/hydrate.ts'; import { NSchema as n } from '@nostrify/nostrify'; -import { renderReport } from '@/views/mastodon/reports.ts'; import { z } from 'zod'; + +import { type AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; +import { createEvent, parseBody } from '@/utils/api.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderAdminReport } from '@/views/mastodon/reports.ts'; +import { renderReport } from '@/views/mastodon/reports.ts'; const reportsSchema = z.object({ account_id: n.id(), @@ -54,17 +55,11 @@ const reportsController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/admin/reports/#get */ const viewAllReportsController: AppController = async (c) => { const store = c.get('store'); - const allMastodonReports = []; + const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]) + .then((events) => hydrateEvents({ storage: store, events: events, signal: c.req.raw.signal })) + .then((events) => Promise.all(events.map((event) => renderAdminReport(event, { viewerPubkey: c.get('pubkey') })))); - const allReports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]); - - await hydrateEvents({ storage: store, events: allReports, signal: AbortSignal.timeout(2000) }); - - for (const report of allReports) { - allMastodonReports.push(await renderAdminReport(report, { viewerPubkey: c.get('pubkey') })); - } - - return c.json(allMastodonReports); + return c.json(reports); }; export { reportsController, viewAllReportsController }; diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index f6810d3a..32c6e931 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -25,14 +25,13 @@ export interface DittoEvent extends NostrEvent { repost?: DittoEvent; quote_repost?: DittoEvent; reacted?: DittoEvent; - /** The account being reported. + /** The profile being reported. * Must be a kind 0 hydrated. * https://github.com/nostr-protocol/nips/blob/master/56.md */ - target_account?: DittoEvent; - /** The statuses being reported. - * Nostr only support reporting one note, the array of reported notes can be found in the `status_ids` field after JSON.parsing the `content` of a kind 1984. + reported_profile?: DittoEvent; + /** The notes being reported. * https://github.com/nostr-protocol/nips/blob/master/56.md */ - reported_statuses?: DittoEvent[]; + reported_notes?: DittoEvent[]; } diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index ca439bd5..ced61e60 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -42,11 +42,11 @@ async function hydrateEvents(opts: HydrateOpts): Promise { cache.push(event); } - for (const event of await gatherTargetAccounts({ events: cache, storage, signal })) { + for (const event of await gatherReportedProfiles({ events: cache, storage, signal })) { cache.push(event); } - for (const event of await gatherReportedStatuses({ events: cache, storage, signal })) { + for (const event of await gatherReportedNotes({ events: cache, storage, signal })) { cache.push(event); } @@ -101,12 +101,7 @@ function assembleEvents( if (event.kind === 1984) { const targetAccountId = event.tags.find(([name]) => name === 'p')?.[1]; if (targetAccountId) { - event.target_account = b.find((e) => matchFilter({ kinds: [0], authors: [targetAccountId] }, e)); - if (event.target_account) { - event.target_account.user = b.find((e) => - matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e) - ); - } + event.reported_profile = b.find((e) => matchFilter({ kinds: [0], authors: [targetAccountId] }, e)); } const reportedEvents: DittoEvent[] = []; @@ -118,7 +113,7 @@ function assembleEvents( if (reportedEvent) reportedEvents.push(reportedEvent); } } - event.reported_statuses = reportedEvents; + event.reported_notes = reportedEvents; } } @@ -206,8 +201,8 @@ function gatherUsers({ events, storage, signal }: HydrateOpts): Promise { +/** Collect reported notes from the events. */ +function gatherReportedNotes({ events, storage, signal }: HydrateOpts): Promise { const ids = new Set(); for (const event of events) { if (event.kind === 1984) { @@ -226,8 +221,8 @@ function gatherReportedStatuses({ events, storage, signal }: HydrateOpts): Promi ); } -/** Collect target accounts (the ones being reported) from the events. */ -function gatherTargetAccounts({ events, storage, signal }: HydrateOpts): Promise { +/** Collect reported profiles from the events. */ +function gatherReportedProfiles({ events, storage, signal }: HydrateOpts): Promise { const pubkeys = new Set(); for (const event of events) { diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts index b344608a..411a6555 100644 --- a/src/views/mastodon/admin-accounts.ts +++ b/src/views/mastodon/admin-accounts.ts @@ -1,18 +1,11 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { nostrDate } from '@/utils.ts'; -import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; +import { renderAccount } from '@/views/mastodon/accounts.ts'; /** Expects a kind 0 fully hydrated or a kind 30361 hydrated with `d_author` */ async function renderAdminAccount(event: DittoEvent) { - let account; - - if (event && event.kind === 0 && event.user) { - account = await renderAccount(event); - } else { - const d = event.tags.find(([name]) => name === 'd')?.[1]!; - account = event.d_author ? await renderAccount({ ...event.d_author, user: event }) : await accountFromPubkey(d); - } + const account = await renderAccount(event); return { id: account.id, diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 50fea467..453151d6 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -45,8 +45,8 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor } = JSON.parse(reportEvent.content); const statuses = []; - if (reportEvent.reported_statuses) { - for (const status of reportEvent.reported_statuses) { + if (reportEvent.reported_notes) { + for (const status of reportEvent.reported_notes) { statuses.push(await renderStatus(status, { viewerPubkey })); } } @@ -60,7 +60,7 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor forwarded: forward, created_at: nostrDate(reportEvent.created_at).toISOString(), account: await renderAdminAccount(reportEvent.author as DittoEvent), - target_account: await renderAdminAccount(reportEvent.target_account as DittoEvent), + target_account: await renderAdminAccount(reportEvent.reported_profile as DittoEvent), assigned_account: null, action_taken_by_account: null, statuses, From 394599734f5750b80bd4dde9fcfd3eeb5e478e46 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sun, 5 May 2024 15:45:24 -0300 Subject: [PATCH 086/252] fix(reports): put notes in tag & only let comment in event.content --- src/controllers/api/reports.ts | 18 +++++++++++------- src/storages/hydrate.ts | 16 +++++++--------- src/views/mastodon/reports.ts | 32 ++++++++++++++------------------ 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index b49486c3..3486550c 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -12,7 +12,6 @@ const reportsSchema = z.object({ account_id: n.id(), status_ids: n.id().array().default([]), comment: z.string().max(1000).default(''), - forward: z.boolean().default(false), category: z.string().default('other'), // TODO: rules_ids[] is not implemented }); @@ -31,7 +30,6 @@ const reportsController: AppController = async (c) => { account_id, status_ids, comment, - forward, category, } = result.data; @@ -40,13 +38,19 @@ const reportsController: AppController = async (c) => { await hydrateEvents({ events: [profile], storage: store }); } + const tags = [ + ['p', account_id, category], + ['P', Conf.pubkey], + ]; + + for (const status of status_ids) { + tags.push(['e', status, category]); + } + const event = await createEvent({ kind: 1984, - content: JSON.stringify({ account_id, status_ids, comment, forward, category }), - tags: [ - ['p', account_id, category], - ['P', Conf.pubkey], - ], + content: comment, + tags, }, c); return c.json(await renderReport(event, profile)); diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index ced61e60..41670aa4 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -105,13 +105,11 @@ function assembleEvents( } const reportedEvents: DittoEvent[] = []; - const { status_ids } = JSON.parse(event.content); - if (status_ids && Array.isArray(status_ids)) { + const status_ids = event.tags.filter(([name]) => name === 'e').map((tag) => tag[1]); + if (status_ids.length > 0) { for (const id of status_ids) { - if (typeof id === 'string') { - const reportedEvent = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); - if (reportedEvent) reportedEvents.push(reportedEvent); - } + const reportedEvent = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + if (reportedEvent) reportedEvents.push(reportedEvent); } event.reported_notes = reportedEvents; } @@ -206,10 +204,10 @@ function gatherReportedNotes({ events, storage, signal }: HydrateOpts): Promise< const ids = new Set(); for (const event of events) { if (event.kind === 1984) { - const { status_ids } = JSON.parse(event.content); - if (status_ids && Array.isArray(status_ids)) { + const status_ids = event.tags.filter(([name]) => name === 'e').map((tag) => tag[1]); + if (status_ids.length > 0) { for (const id of status_ids) { - if (typeof id === 'string') ids.add(id); + ids.add(id); } } } diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 453151d6..7e3460d7 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -6,25 +6,24 @@ import { renderStatus } from '@/views/mastodon/statuses.ts'; /** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { - const { - account_id, - status_ids, - comment, - forward, - category, - } = JSON.parse(reportEvent.content); + // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag + const category = reportEvent.tags.find(([name]) => name === 'p')?.[2] as string; + + const status_ids = reportEvent.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? []; + + const reported_profile_pubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1] as string; return { - id: account_id, + id: reportEvent.id, action_taken: false, action_taken_at: null, category, - comment, - forwarded: forward, + comment: reportEvent.content, + forwarded: false, created_at: nostrDate(reportEvent.created_at).toISOString(), status_ids, rules_ids: null, - target_account: profile ? await renderAccount(profile) : await accountFromPubkey(account_id), + target_account: profile ? await renderAccount(profile) : await accountFromPubkey(reported_profile_pubkey), }; } @@ -38,11 +37,8 @@ interface RenderAdminReportOpts { async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminReportOpts) { const { viewerPubkey } = opts; - const { - comment, - forward, - category, - } = JSON.parse(reportEvent.content); + // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag + const category = reportEvent.tags.find(([name]) => name === 'p')?.[2] as string; const statuses = []; if (reportEvent.reported_notes) { @@ -56,8 +52,8 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor action_taken: false, action_taken_at: null, category, - comment, - forwarded: forward, + comment: reportEvent.content, + forwarded: false, created_at: nostrDate(reportEvent.created_at).toISOString(), account: await renderAdminAccount(reportEvent.author as DittoEvent), target_account: await renderAdminAccount(reportEvent.reported_profile as DittoEvent), From 24068d12d078356bdc9e7f914ba5701ee4e23a87 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 6 May 2024 11:47:05 -0300 Subject: [PATCH 087/252] refactor(reports): rename variables and remove type assertion --- src/app.ts | 4 ++-- src/controllers/api/reports.ts | 4 ++-- src/views/mastodon/reports.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app.ts b/src/app.ts index 313ad3c6..80e12885 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,7 +44,7 @@ import { } from '@/controllers/api/pleroma.ts'; import { preferencesController } from '@/controllers/api/preferences.ts'; import { relayController } from '@/controllers/nostr/relay.ts'; -import { reportsController, viewAllReportsController } from '@/controllers/api/reports.ts'; +import { adminReportsController, reportsController } from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { bookmarkController, @@ -208,7 +208,7 @@ app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysControlle app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); app.post('/api/v1/reports', requirePubkey, reportsController); -app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), viewAllReportsController); +app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 3486550c..1e276138 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -57,7 +57,7 @@ const reportsController: AppController = async (c) => { }; /** https://docs.joinmastodon.org/methods/admin/reports/#get */ -const viewAllReportsController: AppController = async (c) => { +const adminReportsController: AppController = async (c) => { const store = c.get('store'); const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]) .then((events) => hydrateEvents({ storage: store, events: events, signal: c.req.raw.signal })) @@ -66,4 +66,4 @@ const viewAllReportsController: AppController = async (c) => { return c.json(reports); }; -export { reportsController, viewAllReportsController }; +export { adminReportsController, reportsController }; diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 7e3460d7..ec0d6c7b 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -7,11 +7,11 @@ import { renderStatus } from '@/views/mastodon/statuses.ts'; /** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag - const category = reportEvent.tags.find(([name]) => name === 'p')?.[2] as string; + const category = reportEvent.tags.find(([name]) => name === 'p')?.[2]; - const status_ids = reportEvent.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? []; + const statusIds = reportEvent.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? []; - const reported_profile_pubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1] as string; + const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1]!; return { id: reportEvent.id, @@ -21,9 +21,9 @@ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { comment: reportEvent.content, forwarded: false, created_at: nostrDate(reportEvent.created_at).toISOString(), - status_ids, + status_ids: statusIds, rules_ids: null, - target_account: profile ? await renderAccount(profile) : await accountFromPubkey(reported_profile_pubkey), + target_account: profile ? await renderAccount(profile) : await accountFromPubkey(reportedPubkey), }; } @@ -38,7 +38,7 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor const { viewerPubkey } = opts; // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag - const category = reportEvent.tags.find(([name]) => name === 'p')?.[2] as string; + const category = reportEvent.tags.find(([name]) => name === 'p')?.[2]; const statuses = []; if (reportEvent.reported_notes) { From a3e54fdff4091fc7ab6becd68e6388026fcec8ee Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 6 May 2024 11:44:33 -0500 Subject: [PATCH 088/252] DittoPostgres: use pool size for number of CPU cores --- src/db/adapters/DittoPostgres.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/db/adapters/DittoPostgres.ts b/src/db/adapters/DittoPostgres.ts index f8a5112e..2d9d9f16 100644 --- a/src/db/adapters/DittoPostgres.ts +++ b/src/db/adapters/DittoPostgres.ts @@ -16,9 +16,10 @@ export class DittoPostgres { }, // @ts-ignore mismatched kysely versions probably createDriver() { - return new PostgreSQLDriver({ - connectionString: Deno.env.get('DATABASE_URL'), - }); + return new PostgreSQLDriver( + { connectionString: Deno.env.get('DATABASE_URL') }, + navigator.hardwareConcurrency, + ); }, createIntrospector(db: Kysely) { return new PostgresIntrospector(db); From 8d7621fbaf242d0821c1626125139bfb982ac664 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 6 May 2024 14:40:56 -0300 Subject: [PATCH 089/252] fix: return in case pubkey is undefined --- src/views/mastodon/reports.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index ec0d6c7b..850f86b1 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -11,7 +11,8 @@ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { const statusIds = reportEvent.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? []; - const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1]!; + const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1]; + if (!reportedPubkey) return; return { id: reportEvent.id, From 17706d3b2084c86f494bfd57388342749c0e12b7 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 7 May 2024 10:19:08 -0300 Subject: [PATCH 090/252] feat: implement view single report --- src/app.ts | 3 ++- src/controllers/api/reports.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/app.ts b/src/app.ts index 80e12885..cdd5d67b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,7 +44,7 @@ import { } from '@/controllers/api/pleroma.ts'; import { preferencesController } from '@/controllers/api/preferences.ts'; import { relayController } from '@/controllers/nostr/relay.ts'; -import { adminReportsController, reportsController } from '@/controllers/api/reports.ts'; +import { adminReportsController, reportsController, singleAdminReportsController } from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { bookmarkController, @@ -209,6 +209,7 @@ app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysContro app.post('/api/v1/reports', requirePubkey, reportsController); app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); +app.get('/api/v1/admin/reports/:id', requirePubkey, requireRole('admin'), singleAdminReportsController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 1e276138..de46fbff 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -16,7 +16,7 @@ const reportsSchema = z.object({ // TODO: rules_ids[] is not implemented }); -/** https://docs.joinmastodon.org/methods/reports/ */ +/** https://docs.joinmastodon.org/methods/reports/#post */ const reportsController: AppController = async (c) => { const store = c.get('store'); const body = await parseBody(c.req.raw); @@ -66,4 +66,26 @@ const adminReportsController: AppController = async (c) => { return c.json(reports); }; -export { adminReportsController, reportsController }; +/** https://docs.joinmastodon.org/methods/admin/reports/#get-one */ +const singleAdminReportsController: AppController = async (c) => { + const eventId = c.req.param('id'); + const { signal } = c.req.raw; + const store = c.get('store'); + const pubkey = c.get('pubkey'); + + const [event] = await store.query([{ + kinds: [1984], + ids: [eventId], + limit: 1, + }], { signal }); + + if (!event) { + return c.json({ error: 'This action is not allowed' }, 403); + } + + await hydrateEvents({ events: [event], storage: store, signal }); + + return c.json(await renderAdminReport(event, { viewerPubkey: pubkey })); +}; + +export { adminReportsController, reportsController, singleAdminReportsController }; From 53a8871a5477cd80beecf413b83e9cfb61084e12 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 7 May 2024 11:29:49 -0300 Subject: [PATCH 091/252] refactor: change 'singleAdminReportsController' to 'adminReportController' --- src/app.ts | 4 ++-- src/controllers/api/reports.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index cdd5d67b..6e5aaae3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,7 +44,7 @@ import { } from '@/controllers/api/pleroma.ts'; import { preferencesController } from '@/controllers/api/preferences.ts'; import { relayController } from '@/controllers/nostr/relay.ts'; -import { adminReportsController, reportsController, singleAdminReportsController } from '@/controllers/api/reports.ts'; +import { adminReportController, adminReportsController, reportsController } from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { bookmarkController, @@ -209,7 +209,7 @@ app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysContro app.post('/api/v1/reports', requirePubkey, reportsController); app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); -app.get('/api/v1/admin/reports/:id', requirePubkey, requireRole('admin'), singleAdminReportsController); +app.get('/api/v1/admin/reports/:id', requirePubkey, requireRole('admin'), adminReportController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index de46fbff..68450201 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -67,7 +67,7 @@ const adminReportsController: AppController = async (c) => { }; /** https://docs.joinmastodon.org/methods/admin/reports/#get-one */ -const singleAdminReportsController: AppController = async (c) => { +const adminReportController: AppController = async (c) => { const eventId = c.req.param('id'); const { signal } = c.req.raw; const store = c.get('store'); @@ -88,4 +88,4 @@ const singleAdminReportsController: AppController = async (c) => { return c.json(await renderAdminReport(event, { viewerPubkey: pubkey })); }; -export { adminReportsController, reportsController, singleAdminReportsController }; +export { adminReportController, adminReportsController, reportsController }; From cfc119f3111c4cccdb6b88de1f70c72fd894515f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 7 May 2024 14:53:32 -0300 Subject: [PATCH 092/252] feat: check for formatting errors when committing --- .hooks/pre-commit | 4 ++++ deno.json | 1 + 2 files changed, 5 insertions(+) create mode 100755 .hooks/pre-commit diff --git a/.hooks/pre-commit b/.hooks/pre-commit new file mode 100755 index 00000000..ce8d8092 --- /dev/null +++ b/.hooks/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/hook.sh" + +deno fmt --check diff --git a/deno.json b/deno.json index 66263f56..5debcfd7 100644 --- a/deno.json +++ b/deno.json @@ -4,6 +4,7 @@ "tasks": { "start": "deno run -A src/server.ts", "dev": "deno run -A --watch src/server.ts", + "hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts", "debug": "deno run -A --inspect src/server.ts", "test": "DATABASE_URL=\"sqlite://:memory:\" deno test -A --junit-path=./deno-test.xml", "check": "deno check src/server.ts", From b523f57a822b7969f48e9babc09da12751472c77 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 7 May 2024 15:04:33 -0300 Subject: [PATCH 093/252] fix: remove --check flag in deno fmt --- .hooks/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hooks/pre-commit b/.hooks/pre-commit index ce8d8092..7b9a9343 100755 --- a/.hooks/pre-commit +++ b/.hooks/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/hook.sh" -deno fmt --check +deno fmt From 6509932ab0c93eadc63ee2f86b78827e77257a83 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 May 2024 18:37:57 +0000 Subject: [PATCH 094/252] Revert "Merge branch 'deno-fmt-on-commit' into 'main'" This reverts merge request !227 --- .hooks/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hooks/pre-commit b/.hooks/pre-commit index 7b9a9343..ce8d8092 100755 --- a/.hooks/pre-commit +++ b/.hooks/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/hook.sh" -deno fmt +deno fmt --check From 7ecad73490fef8e8d1160726a6f506ef97dfbad2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 May 2024 13:45:23 -0500 Subject: [PATCH 095/252] Add lint-staged --- .hooks/pre-commit | 2 +- .lintstagedrc | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .lintstagedrc diff --git a/.hooks/pre-commit b/.hooks/pre-commit index ce8d8092..c3451eda 100755 --- a/.hooks/pre-commit +++ b/.hooks/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/hook.sh" -deno fmt --check +deno run -A npm:lint-staged diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 00000000..d3a3e554 --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,3 @@ +{ + "*.{ts,tsx,md}": "deno fmt" +} \ No newline at end of file From 57495dbd7a1bb17d7c92fdbda33bdf54a69a41aa Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 7 May 2024 20:49:42 -0300 Subject: [PATCH 096/252] feat: implement report resolve --- src/app.ts | 15 +++++++++++++-- src/controllers/api/reports.ts | 30 +++++++++++++++++++++++++++++- src/views/mastodon/reports.ts | 5 +++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index 6e5aaae3..a3f6a43b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,7 +44,12 @@ import { } from '@/controllers/api/pleroma.ts'; import { preferencesController } from '@/controllers/api/preferences.ts'; import { relayController } from '@/controllers/nostr/relay.ts'; -import { adminReportController, adminReportsController, reportsController } from '@/controllers/api/reports.ts'; +import { + adminReportController, + adminReportResolveController, + adminReportsController, + reportsController, +} from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { bookmarkController, @@ -209,7 +214,13 @@ app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysContro app.post('/api/v1/reports', requirePubkey, reportsController); app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); -app.get('/api/v1/admin/reports/:id', requirePubkey, requireRole('admin'), adminReportController); +app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requirePubkey, requireRole('admin'), adminReportController); +app.post( + '/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve', + requirePubkey, + requireRole('admin'), + adminReportResolveController, +); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 68450201..d3636303 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -88,4 +88,32 @@ const adminReportController: AppController = async (c) => { return c.json(await renderAdminReport(event, { viewerPubkey: pubkey })); }; -export { adminReportController, adminReportsController, reportsController }; +/** https://docs.joinmastodon.org/methods/admin/reports/#resolve */ +const adminReportResolveController: AppController = async (c) => { + const eventId = c.req.param('id'); + const { signal } = c.req.raw; + const store = c.get('store'); + const pubkey = c.get('pubkey'); + + const [event] = await store.query([{ + kinds: [1984], + ids: [eventId], + limit: 1, + }], { signal }); + + if (!event) { + return c.json({ error: 'This action is not allowed' }, 403); + } + + await hydrateEvents({ events: [event], storage: store, signal }); + + await createEvent({ + kind: 5, + tags: [['e', event.id]], + content: 'Report closed.', + }, c); + + return c.json(await renderAdminReport(event, { viewerPubkey: pubkey, action_taken: true })); +}; + +export { adminReportController, adminReportResolveController, adminReportsController, reportsController }; diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 850f86b1..5ddbe699 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -30,13 +30,14 @@ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { interface RenderAdminReportOpts { viewerPubkey?: string; + action_taken?: boolean; } /** Admin-level information about a filed report. * Expects an event of kind 1984 fully hydrated. * https://docs.joinmastodon.org/entities/Admin_Report */ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminReportOpts) { - const { viewerPubkey } = opts; + const { viewerPubkey, action_taken = false } = opts; // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag const category = reportEvent.tags.find(([name]) => name === 'p')?.[2]; @@ -50,7 +51,7 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor return { id: reportEvent.id, - action_taken: false, + action_taken, action_taken_at: null, category, comment: reportEvent.content, From e8b690e2626b01a8cd417d576fe4f074075b7e7a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 8 May 2024 10:32:10 -0300 Subject: [PATCH 097/252] fix(admin resolve): create admin event instead of create normal event --- src/controllers/api/reports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index d3636303..24e314d8 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { createEvent, parseBody } from '@/utils/api.ts'; +import { createAdminEvent, createEvent, parseBody } from '@/utils/api.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderAdminReport } from '@/views/mastodon/reports.ts'; import { renderReport } from '@/views/mastodon/reports.ts'; @@ -107,7 +107,7 @@ const adminReportResolveController: AppController = async (c) => { await hydrateEvents({ events: [event], storage: store, signal }); - await createEvent({ + await createAdminEvent({ kind: 5, tags: [['e', event.id]], content: 'Report closed.', From 7ad0e4d9e90bb3f3180ed36c5d04cbd752e11158 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 10:41:05 -0500 Subject: [PATCH 098/252] Relationships: fix blocks and mutes being switched --- src/views/mastodon/relationships.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/views/mastodon/relationships.ts b/src/views/mastodon/relationships.ts index 4cfbfc28..d358024f 100644 --- a/src/views/mastodon/relationships.ts +++ b/src/views/mastodon/relationships.ts @@ -6,13 +6,11 @@ async function renderRelationship(sourcePubkey: string, targetPubkey: string) { { kinds: [3], authors: [sourcePubkey], limit: 1 }, { kinds: [3], authors: [targetPubkey], limit: 1 }, { kinds: [10000], authors: [sourcePubkey], limit: 1 }, - { kinds: [10000], authors: [targetPubkey], limit: 1 }, ]); const event3 = events.find((event) => event.kind === 3 && event.pubkey === sourcePubkey); const target3 = events.find((event) => event.kind === 3 && event.pubkey === targetPubkey); const event10000 = events.find((event) => event.kind === 10000 && event.pubkey === sourcePubkey); - const target10000 = events.find((event) => event.kind === 10000 && event.pubkey === targetPubkey); return { id: targetPubkey, @@ -20,9 +18,9 @@ async function renderRelationship(sourcePubkey: string, targetPubkey: string) { showing_reblogs: true, notifying: false, followed_by: target3 ? hasTag(target3?.tags, ['p', sourcePubkey]) : false, - blocking: event10000 ? hasTag(event10000.tags, ['p', targetPubkey]) : false, - blocked_by: target10000 ? hasTag(target10000.tags, ['p', sourcePubkey]) : false, - muting: false, + blocking: false, + blocked_by: false, + muting: event10000 ? hasTag(event10000.tags, ['p', targetPubkey]) : false, muting_notifications: false, requested: false, domain_blocking: false, From 5e1cfad5cc0e51f30e4ade5c83ab7a96a9b465b6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 11:45:00 -0500 Subject: [PATCH 099/252] Add PG_POOL_SIZE environment variable --- src/config.ts | 7 +++++++ src/db/adapters/DittoPostgres.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 2e8004f0..4266033e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -208,6 +208,13 @@ class Conf { } }, }; + /** Postgres settings. */ + static pg = { + /** Number of connections to use in the pool. */ + get poolSize(): number { + return Number(Deno.env.get('PG_POOL_SIZE') ?? 10); + }, + }; } const optionalBooleanSchema = z diff --git a/src/db/adapters/DittoPostgres.ts b/src/db/adapters/DittoPostgres.ts index 2d9d9f16..541c1d73 100644 --- a/src/db/adapters/DittoPostgres.ts +++ b/src/db/adapters/DittoPostgres.ts @@ -1,6 +1,7 @@ import { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely'; import { PostgreSQLDriver } from 'kysely_deno_postgres'; +import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; export class DittoPostgres { @@ -18,7 +19,7 @@ export class DittoPostgres { createDriver() { return new PostgreSQLDriver( { connectionString: Deno.env.get('DATABASE_URL') }, - navigator.hardwareConcurrency, + Conf.pg.poolSize, ); }, createIntrospector(db: Kysely) { From 43e8f2a6987b790259619b5fb60e0c2b4c6d2706 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 12:56:42 -0500 Subject: [PATCH 100/252] Use a Kysely logger to log SQL regardless of the adapter used --- deno.json | 1 - src/db/KyselyLogger.ts | 18 ++++++++++++++++++ src/db/adapters/DittoPostgres.ts | 2 ++ src/db/adapters/DittoSQLite.ts | 2 ++ src/workers/sqlite.worker.ts | 26 +------------------------- 5 files changed, 23 insertions(+), 26 deletions(-) create mode 100644 src/db/KyselyLogger.ts diff --git a/deno.json b/deno.json index 5debcfd7..5d455889 100644 --- a/deno.json +++ b/deno.json @@ -46,7 +46,6 @@ "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@^2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", - "scoped_performance" :"https://deno.land/x/scoped_performance@v2.0.0/mod.ts", "tldts": "npm:tldts@^6.0.14", "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", diff --git a/src/db/KyselyLogger.ts b/src/db/KyselyLogger.ts new file mode 100644 index 00000000..e39cbd08 --- /dev/null +++ b/src/db/KyselyLogger.ts @@ -0,0 +1,18 @@ +import { Stickynotes } from '@soapbox/stickynotes'; +import { Logger } from 'kysely'; + +/** Log the SQL for queries. */ +export const KyselyLogger: Logger = (event) => { + if (event.level === 'query') { + const console = new Stickynotes('ditto:sql'); + + const { query, queryDurationMillis } = event; + const { sql, parameters } = query; + + console.debug( + sql, + JSON.stringify(parameters), + `\x1b[90m(${(queryDurationMillis / 1000).toFixed(2)}s)\x1b[0m`, + ); + } +}; diff --git a/src/db/adapters/DittoPostgres.ts b/src/db/adapters/DittoPostgres.ts index 541c1d73..d0abbf99 100644 --- a/src/db/adapters/DittoPostgres.ts +++ b/src/db/adapters/DittoPostgres.ts @@ -3,6 +3,7 @@ import { PostgreSQLDriver } from 'kysely_deno_postgres'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; +import { KyselyLogger } from '@/db/KyselyLogger.ts'; export class DittoPostgres { static db: Kysely | undefined; @@ -29,6 +30,7 @@ export class DittoPostgres { return new PostgresQueryCompiler(); }, }, + log: KyselyLogger, }); } diff --git a/src/db/adapters/DittoSQLite.ts b/src/db/adapters/DittoSQLite.ts index 6848aeec..fe225a20 100644 --- a/src/db/adapters/DittoSQLite.ts +++ b/src/db/adapters/DittoSQLite.ts @@ -3,6 +3,7 @@ import { Kysely, sql } from 'kysely'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; +import { KyselyLogger } from '@/db/KyselyLogger.ts'; import SqliteWorker from '@/workers/sqlite.ts'; export class DittoSQLite { @@ -17,6 +18,7 @@ export class DittoSQLite { dialect: new PolySqliteDialect({ database: sqliteWorker, }), + log: KyselyLogger, }); // Set PRAGMA values. diff --git a/src/workers/sqlite.worker.ts b/src/workers/sqlite.worker.ts index a3ff1d68..68c70d6c 100644 --- a/src/workers/sqlite.worker.ts +++ b/src/workers/sqlite.worker.ts @@ -1,14 +1,11 @@ /// import { Database as SQLite } from '@db/sqlite'; -import { Stickynotes } from '@soapbox/stickynotes'; import * as Comlink from 'comlink'; import { CompiledQuery, QueryResult } from 'kysely'; -import { ScopedPerformance } from 'scoped_performance'; import '@/sentry.ts'; let db: SQLite | undefined; -const console = new Stickynotes('ditto:sqlite.worker'); export const SqliteWorker = { open(path: string): void { @@ -17,32 +14,11 @@ export const SqliteWorker = { executeQuery({ sql, parameters }: CompiledQuery): QueryResult { if (!db) throw new Error('Database not open'); - const perf = (console.enabled && console.level >= 4) ? new ScopedPerformance() : undefined; - - if (perf) { - perf.mark('start'); - } - - const result = { + return { rows: db!.prepare(sql).all(...parameters as any[]) as R[], numAffectedRows: BigInt(db!.changes), insertId: BigInt(db!.lastInsertRowId), }; - - if (perf) { - const { duration } = perf.measure('end', 'start'); - - console.debug( - sql.replace(/\s+/g, ' '), - JSON.stringify(parameters), - `\x1b[90m(${(duration / 1000).toFixed(2)}s)\x1b[0m`, - ); - - perf.clearMarks(); - perf.clearMeasures(); - } - - return result; }, destroy() { db?.close(); From 6bc051c06bb6bf9eab3cfe06ca737e0f5c9b74bc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 13:56:37 -0500 Subject: [PATCH 101/252] EventsDB: avoid ORDER BY when querying replaceable events by author --- src/storages/events-db.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 2fcdf161..53a307c7 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,6 +1,7 @@ import { NIP50, NostrEvent, NostrFilter, NSchema as n, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { Kysely, type SelectQueryBuilder } from 'kysely'; +import { sortEvents } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; @@ -159,8 +160,17 @@ class EventsDB implements NStore { 'events.created_at', 'events.sig', ]) - .where('events.deleted_at', 'is', null) - .orderBy('events.created_at', 'desc'); + .where('events.deleted_at', 'is', null); + + /** Whether we are querying for replaceable events by author. */ + const isAddrQuery = filter.authors && + filter.kinds && + filter.kinds.every((kind) => isReplaceableKind(kind) || isParameterizedReplaceableKind(kind)); + + // Avoid ORDER BY when querying for replaceable events by author. + if (!isAddrQuery) { + query = query.orderBy('events.created_at', 'desc'); + } for (const [key, value] of Object.entries(filter)) { if (value === undefined) continue; @@ -275,7 +285,7 @@ class EventsDB implements NStore { query = query.limit(opts.limit); } - return (await query.execute()).map((row) => { + const events = (await query.execute()).map((row) => { const event: DittoEvent = { id: row.id, kind: row.kind, @@ -316,6 +326,8 @@ class EventsDB implements NStore { return event; }); + + return sortEvents(events); } /** Delete events from each table. Should be run in a transaction! */ From b16d5b749e8252395f769c0c395091bdc6b40aec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 14:34:22 -0500 Subject: [PATCH 102/252] Add a created_at, kind index for the global feed --- src/db/migrations/011_kind_author_index.ts | 2 +- .../migrations/018_events_created_at_kind_index.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/db/migrations/018_events_created_at_kind_index.ts diff --git a/src/db/migrations/011_kind_author_index.ts b/src/db/migrations/011_kind_author_index.ts index 3e7d010c..c41910b4 100644 --- a/src/db/migrations/011_kind_author_index.ts +++ b/src/db/migrations/011_kind_author_index.ts @@ -4,7 +4,7 @@ export async function up(db: Kysely): Promise { await db.schema .createIndex('idx_events_kind_pubkey_created_at') .on('events') - .columns(['kind', 'pubkey', 'created_at']) + .columns(['kind', 'pubkey', 'created_at desc']) .execute(); } diff --git a/src/db/migrations/018_events_created_at_kind_index.ts b/src/db/migrations/018_events_created_at_kind_index.ts new file mode 100644 index 00000000..8e6c67c0 --- /dev/null +++ b/src/db/migrations/018_events_created_at_kind_index.ts @@ -0,0 +1,14 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.schema + .createIndex('idx_events_created_at_kind') + .on('events') + .columns(['created_at desc', 'kind']) + .ifNotExists() + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('idx_events_created_at_kind').ifExists().execute(); +} From a82af47c679e61ddae4b57564e4a7c0c4ff244a1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 14:53:33 -0500 Subject: [PATCH 103/252] quote_repost -> quote --- src/interfaces/DittoEvent.ts | 2 +- src/storages/hydrate.test.ts | 4 ++-- src/storages/hydrate.ts | 2 +- src/views/mastodon/statuses.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 32c6e931..41847fb1 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -23,7 +23,7 @@ export interface DittoEvent extends NostrEvent { d_author?: DittoEvent; user?: DittoEvent; repost?: DittoEvent; - quote_repost?: DittoEvent; + quote?: DittoEvent; reacted?: DittoEvent; /** The profile being reported. * Must be a kind 0 hydrated. diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index c0c0a426..10e480e2 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -96,7 +96,7 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => { const expectedEvent1quoteRepost = { ...event1quoteRepostCopy, author: event0madeQuoteRepostCopy, - quote_repost: { ...event1willBeQuoteRepostedCopy, author: event0copy }, + quote: { ...event1willBeQuoteRepostedCopy, author: event0copy }, }; assertEquals(event1quoteRepostCopy, expectedEvent1quoteRepost); @@ -127,7 +127,7 @@ Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () const expectedEvent6 = { ...event6copy, author: event0copy, - repost: { ...event1quoteCopy, author: event0copy, quote_repost: { author: event0copy, ...event1copy } }, + repost: { ...event1quoteCopy, author: event0copy, quote: { author: event0copy, ...event1copy } }, }; assertEquals(event6copy, expectedEvent6); }); diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 41670aa4..e197ca8e 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -80,7 +80,7 @@ function assembleEvents( if (event.kind === 1) { const id = event.tags.find(([name]) => name === 'q')?.[1]; if (id) { - event.quote_repost = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + event.quote = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); } } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 21d380b3..c743e8b6 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -109,7 +109,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< tags: [], emojis: renderEmojis(event), poll: null, - quote: !event.quote_repost ? null : await renderStatus(event.quote_repost, { depth: depth + 1 }), + quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }), quote_id: event.tags.find(([name]) => name === 'q')?.[1] ?? null, uri: Conf.external(note), url: Conf.external(note), From f99958c40e8b755c83dc27ac8f421bd38853c66c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 14:59:30 -0500 Subject: [PATCH 104/252] reportsController -> reportController --- src/app.ts | 4 ++-- src/controllers/api/reports.ts | 8 ++++---- src/views/mastodon/reports.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app.ts b/src/app.ts index a3f6a43b..5982165f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -48,7 +48,7 @@ import { adminReportController, adminReportResolveController, adminReportsController, - reportsController, + reportController, } from '@/controllers/api/reports.ts'; import { searchController } from '@/controllers/api/search.ts'; import { @@ -212,7 +212,7 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); -app.post('/api/v1/reports', requirePubkey, reportsController); +app.post('/api/v1/reports', requirePubkey, reportController); app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requirePubkey, requireRole('admin'), adminReportController); app.post( diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 24e314d8..998054af 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -8,7 +8,7 @@ import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderAdminReport } from '@/views/mastodon/reports.ts'; import { renderReport } from '@/views/mastodon/reports.ts'; -const reportsSchema = z.object({ +const reportSchema = z.object({ account_id: n.id(), status_ids: n.id().array().default([]), comment: z.string().max(1000).default(''), @@ -17,10 +17,10 @@ const reportsSchema = z.object({ }); /** https://docs.joinmastodon.org/methods/reports/#post */ -const reportsController: AppController = async (c) => { +const reportController: AppController = async (c) => { const store = c.get('store'); const body = await parseBody(c.req.raw); - const result = reportsSchema.safeParse(body); + const result = reportSchema.safeParse(body); if (!result.success) { return c.json(result.error, 422); @@ -116,4 +116,4 @@ const adminReportResolveController: AppController = async (c) => { return c.json(await renderAdminReport(event, { viewerPubkey: pubkey, action_taken: true })); }; -export { adminReportController, adminReportResolveController, adminReportsController, reportsController }; +export { adminReportController, adminReportResolveController, adminReportsController, reportController }; diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 5ddbe699..95923075 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -30,14 +30,14 @@ async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { interface RenderAdminReportOpts { viewerPubkey?: string; - action_taken?: boolean; + actionTaken?: boolean; } /** Admin-level information about a filed report. * Expects an event of kind 1984 fully hydrated. * https://docs.joinmastodon.org/entities/Admin_Report */ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminReportOpts) { - const { viewerPubkey, action_taken = false } = opts; + const { viewerPubkey, actionTaken = false } = opts; // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag const category = reportEvent.tags.find(([name]) => name === 'p')?.[2]; @@ -51,7 +51,7 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor return { id: reportEvent.id, - action_taken, + action_taken: actionTaken, action_taken_at: null, category, comment: reportEvent.content, From 8530749192ed0b115d7bd05379785964ba55e4b5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 15:03:58 -0500 Subject: [PATCH 105/252] reportController: hydrate the report itself to get the author --- src/controllers/api/reports.ts | 10 +++------- src/views/mastodon/reports.ts | 18 ++++++++---------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 998054af..eb85bd28 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -33,11 +33,6 @@ const reportController: AppController = async (c) => { category, } = result.data; - const [profile] = await store.query([{ kinds: [0], authors: [account_id] }]); - if (profile) { - await hydrateEvents({ events: [profile], storage: store }); - } - const tags = [ ['p', account_id, category], ['P', Conf.pubkey], @@ -53,7 +48,8 @@ const reportController: AppController = async (c) => { tags, }, c); - return c.json(await renderReport(event, profile)); + await hydrateEvents({ events: [event], storage: store }); + return c.json(await renderReport(event)); }; /** https://docs.joinmastodon.org/methods/admin/reports/#get */ @@ -113,7 +109,7 @@ const adminReportResolveController: AppController = async (c) => { content: 'Report closed.', }, c); - return c.json(await renderAdminReport(event, { viewerPubkey: pubkey, action_taken: true })); + return c.json(await renderAdminReport(event, { viewerPubkey: pubkey, actionTaken: true })); }; export { adminReportController, adminReportResolveController, adminReportsController, reportController }; diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 95923075..488f7b9a 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -5,26 +5,24 @@ import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; /** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ -async function renderReport(reportEvent: DittoEvent, profile: DittoEvent) { +async function renderReport(event: DittoEvent) { // The category is present in both the 'e' and 'p' tag, however, it is possible to report a user without reporting a note, so it's better to get the category from the 'p' tag - const category = reportEvent.tags.find(([name]) => name === 'p')?.[2]; - - const statusIds = reportEvent.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? []; - - const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1]; + const category = event.tags.find(([name]) => name === 'p')?.[2]; + const statusIds = event.tags.filter(([name]) => name === 'e').map((tag) => tag[1]) ?? []; + const reportedPubkey = event.tags.find(([name]) => name === 'p')?.[1]; if (!reportedPubkey) return; return { - id: reportEvent.id, + id: event.id, action_taken: false, action_taken_at: null, category, - comment: reportEvent.content, + comment: event.content, forwarded: false, - created_at: nostrDate(reportEvent.created_at).toISOString(), + created_at: nostrDate(event.created_at).toISOString(), status_ids: statusIds, rules_ids: null, - target_account: profile ? await renderAccount(profile) : await accountFromPubkey(reportedPubkey), + target_account: event.author ? await renderAccount(event.author) : await accountFromPubkey(reportedPubkey), }; } From c7e8beebc66fcf9f5ea2cfe6aeb7ec79ebd86960 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 15:06:33 -0500 Subject: [PATCH 106/252] renderReport: whoops, event.author -> event.reported_account --- src/views/mastodon/reports.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 488f7b9a..f4908de1 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -22,7 +22,9 @@ async function renderReport(event: DittoEvent) { created_at: nostrDate(event.created_at).toISOString(), status_ids: statusIds, rules_ids: null, - target_account: event.author ? await renderAccount(event.author) : await accountFromPubkey(reportedPubkey), + target_account: event.reported_profile + ? await renderAccount(event.reported_profile) + : await accountFromPubkey(reportedPubkey), }; } From 64e49bca9d4e5c4b78f7b41d108138f00d847526 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 May 2024 17:11:48 -0500 Subject: [PATCH 107/252] EventsDB: fix postgres crash when there are no local users --- src/storages/events-db.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 53a307c7..de1ec4a2 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -235,8 +235,6 @@ class EventsDB implements NStore { /** Converts filters to more performant, simpler filters that are better for SQLite. */ async expandFilters(filters: NostrFilter[]): Promise { - filters = normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries. - for (const filter of filters) { if (filter.search) { const tokens = NIP50.parseInput(filter.search); @@ -268,7 +266,7 @@ class EventsDB implements NStore { } } - return filters; + return normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries. } /** Get events for filters from the database. */ From e4952f0c2137ed513e165314c42131f50f800f15 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 8 May 2024 20:10:09 -0300 Subject: [PATCH 108/252] feat: create updateListAdminEvent() & updateAdminEvent() --- src/utils/api.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/utils/api.ts b/src/utils/api.ts index 72f4c3e5..cba7c663 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -76,6 +76,29 @@ async function createAdminEvent(t: EventStub, c: AppContext): Promise string[][], + c: AppContext, +): Promise { + return updateAdminEvent(filter, (prev) => ({ + kind: filter.kinds[0], + content: prev?.content ?? '', + tags: fn(prev?.tags ?? []), + }), c); +} + +/** Fetch existing event, update it, then publish the new admin event. */ +async function updateAdminEvent( + filter: UpdateEventFilter, + fn: (prev: NostrEvent | undefined) => E, + c: AppContext, +): Promise { + const [prev] = await Storages.db.query([filter], { limit: 1, signal: c.req.raw.signal }); + return createAdminEvent(fn(prev), c); +} + /** Push the event through the pipeline, rethrowing any RelayError. */ async function publishEvent(event: NostrEvent, c: AppContext): Promise { debug('EVENT', event); @@ -185,5 +208,6 @@ export { paginationSchema, parseBody, updateEvent, + updateListAdminEvent, updateListEvent, }; From 9e2225873d5869722d5728cf9eb250a040d79336 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 8 May 2024 20:08:44 -0300 Subject: [PATCH 109/252] feat: implement action against an account - Action of deactivating an account by muting it in the entire server --- src/app.ts | 4 +++- src/controllers/api/admin.ts | 40 ++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/app.ts b/src/app.ts index a3f6a43b..344c52aa 100644 --- a/src/app.ts +++ b/src/app.ts @@ -25,7 +25,7 @@ import { updateCredentialsController, verifyCredentialsController, } from '@/controllers/api/accounts.ts'; -import { adminAccountsController } from '@/controllers/api/admin.ts'; +import { adminAccountAction, adminAccountsController } from '@/controllers/api/admin.ts'; import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts'; import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; @@ -222,6 +222,8 @@ app.post( adminReportResolveController, ); +app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requirePubkey, requireRole('admin'), adminAccountAction); + // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); app.get('/api/v1/filters', emptyArrayController); diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 15420564..97e28b4e 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -5,7 +5,8 @@ import { Conf } from '@/config.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; -import { paginated, paginationSchema } from '@/utils/api.ts'; +import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts'; +import { addTag } from '@/tags.ts'; const adminAccountQuerySchema = z.object({ local: booleanParamSchema.optional(), @@ -57,4 +58,39 @@ const adminAccountsController: AppController = async (c) => { return paginated(c, events, accounts); }; -export { adminAccountsController }; +const adminAccountActionSchema = z.object({ + type: z.enum(['none', 'sensitive', 'disable', 'silence', 'suspend']), +}); + +const adminAccountAction: AppController = async (c) => { + const body = await parseBody(c.req.raw); + const result = adminAccountActionSchema.safeParse(body); + const authorId = c.req.param('id'); + const store = c.get('store'); + const { signal } = c.req.raw; + + if (!result.success) { + return c.json({ error: 'This action is not allowed' }, 403); + } + + const { data } = result; + + if (data.type !== 'disable') { + return c.json({ error: 'Record invalid' }, 422); + } + + const [event] = await store.query([{ kinds: [0], authors: [authorId], limit: 1 }], { signal }); + if (!event) { + return c.json({ error: 'Record not found' }, 404); + } + + await updateListAdminEvent( + { kinds: [10000], authors: [Conf.pubkey] }, + (tags) => addTag(tags, ['p', event.pubkey]), + c, + ); + + return c.json({}, 200); +}; + +export { adminAccountAction, adminAccountsController }; From a1acc85494d1042c1f0d998fe13198eba39e95df Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 9 May 2024 09:44:05 -0300 Subject: [PATCH 110/252] feat: reference ditto docs in README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e46067a..f9994283 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Ditto is a Nostr server for building resilient communities online. With Ditto, you can create your own social network that is decentralized, customizable, and free from ads and tracking. +For more info see: https://docs.soapbox.pub/ditto/ + ⚠️ This software is a work in progress. @@ -38,7 +40,7 @@ With Ditto, you can create your own social network that is decentralized, custom ## License -© Alex Gleason & other Ditto contributors +© Alex Gleason & other Ditto contributors Ditto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by From 4fa6b96d15cfc88f37a85a8990c82db2dd88eb49 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 9 May 2024 13:44:05 -0300 Subject: [PATCH 111/252] refactor(admin action): mute account even if it doesn't have a kind 0 --- src/controllers/api/admin.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 97e28b4e..99a8e5bc 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -66,8 +66,6 @@ const adminAccountAction: AppController = async (c) => { const body = await parseBody(c.req.raw); const result = adminAccountActionSchema.safeParse(body); const authorId = c.req.param('id'); - const store = c.get('store'); - const { signal } = c.req.raw; if (!result.success) { return c.json({ error: 'This action is not allowed' }, 403); @@ -79,14 +77,9 @@ const adminAccountAction: AppController = async (c) => { return c.json({ error: 'Record invalid' }, 422); } - const [event] = await store.query([{ kinds: [0], authors: [authorId], limit: 1 }], { signal }); - if (!event) { - return c.json({ error: 'Record not found' }, 404); - } - await updateListAdminEvent( { kinds: [10000], authors: [Conf.pubkey] }, - (tags) => addTag(tags, ['p', event.pubkey]), + (tags) => addTag(tags, ['p', authorId]), c, ); From c85f31f63f1d4b89cf07afe9a14cdf76ee046432 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 10:19:15 -0300 Subject: [PATCH 112/252] feat: create MuteListPolicy class --- src/policies/MuteListPolicy.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/policies/MuteListPolicy.ts diff --git a/src/policies/MuteListPolicy.ts b/src/policies/MuteListPolicy.ts new file mode 100644 index 00000000..e2342199 --- /dev/null +++ b/src/policies/MuteListPolicy.ts @@ -0,0 +1,23 @@ +import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/nostrify'; + +import { getTagSet } from '@/tags.ts'; + +export class MuteListPolicy implements NPolicy { + constructor(private pubkey: string, private store: NStore) { + this.store = store; + this.pubkey = pubkey; + } + + async call(event: NostrEvent): Promise { + const allowEvent = ['OK', event.id, true, ''] as NostrRelayOK; + const blockEvent = ['OK', event.id, false, 'You are banned in this server.'] as NostrRelayOK; + + const [muteList] = await this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]); + if (!muteList) return allowEvent; + + const mutedPubkeys = getTagSet(muteList.tags, 'p'); + if (mutedPubkeys.has(event.pubkey)) return blockEvent; + + return allowEvent; + } +} From 0c0465f131a8c1a92f083d26f5d964cc31080708 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 10:31:34 -0300 Subject: [PATCH 113/252] refactor(UserStore): move mute logic to separate function & create isMuted() function --- src/storages/UserStore.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/storages/UserStore.ts b/src/storages/UserStore.ts index a3f0726c..b13df420 100644 --- a/src/storages/UserStore.ts +++ b/src/storages/UserStore.ts @@ -1,4 +1,5 @@ import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; + import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getTagSet } from '@/tags.ts'; @@ -16,25 +17,34 @@ export class UserStore implements NStore { } /** - * Query events that `pubkey` did not block + * Query events that `pubkey` did not mute * https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists */ async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { const allEvents = await this.store.query(filters, opts); - const mutedPubkeysEvent = await this.getMuteList(); - if (!mutedPubkeysEvent) { - return allEvents; - } - const mutedPubkeys = getTagSet(mutedPubkeysEvent.tags, 'p'); + const mutedPubkeys = await this.getMutedPubkeys(); return allEvents.filter((event) => { return event.kind === 0 || mutedPubkeys.has(event.pubkey) === false; }); } + async isMuted(pubkey: string): Promise { + const mutedPubkeys = await this.getMutedPubkeys(); + return mutedPubkeys.has(pubkey); + } + private async getMuteList(): Promise { const [muteList] = await this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]); return muteList; } + + private async getMutedPubkeys(): Promise> { + const mutedPubkeysEvent = await this.getMuteList(); + if (!mutedPubkeysEvent) { + return new Set(); + } + return getTagSet(mutedPubkeysEvent.tags, 'p'); + } } From 26dd4606ed26c7d907a9f0348862829713047af9 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 10:35:04 -0300 Subject: [PATCH 114/252] test: UserStore with 100.00% code coverage --- src/storages/UserStore.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/storages/UserStore.test.ts b/src/storages/UserStore.test.ts index 11f96cbd..42c04399 100644 --- a/src/storages/UserStore.test.ts +++ b/src/storages/UserStore.test.ts @@ -8,7 +8,7 @@ import userMe from '~/fixtures/events/event-0-makes-repost-with-quote-repost.jso import blockEvent from '~/fixtures/events/kind-10000-black-blocks-user-me.json' with { type: 'json' }; import event1authorUserMe from '~/fixtures/events/event-1-quote-repost-will-be-reposted.json' with { type: 'json' }; -Deno.test('query events of users that are not blocked', async () => { +Deno.test('query events of users that are not muted', async () => { const userBlackCopy = structuredClone(userBlack); const userMeCopy = structuredClone(userMe); const blockEventCopy = structuredClone(blockEvent); @@ -24,4 +24,19 @@ Deno.test('query events of users that are not blocked', async () => { await store.event(event1authorUserMeCopy); assertEquals(await store.query([{ kinds: [1] }], { limit: 1 }), []); + assertEquals(await store.isMuted(userMeCopy.pubkey), true); +}); + +Deno.test('user never muted anyone', async () => { + const userBlackCopy = structuredClone(userBlack); + const userMeCopy = structuredClone(userMe); + + const db = new MockRelay(); + + const store = new UserStore(userBlackCopy.pubkey, db); + + await store.event(userBlackCopy); + await store.event(userMeCopy); + + assertEquals(await store.isMuted(userMeCopy.pubkey), false); }); From ebeec2ccba1ab18c6b62a3ac0aeb6758df4604ff Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 11:26:15 -0300 Subject: [PATCH 115/252] test: MuteListPolicy with 100.00% code coverage --- src/policies/MuteListPolicy.test.ts | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/policies/MuteListPolicy.test.ts diff --git a/src/policies/MuteListPolicy.test.ts b/src/policies/MuteListPolicy.test.ts new file mode 100644 index 00000000..69561f80 --- /dev/null +++ b/src/policies/MuteListPolicy.test.ts @@ -0,0 +1,72 @@ +import { MockRelay } from '@nostrify/nostrify/test'; + +import { assertEquals } from '@/deps-test.ts'; +import { UserStore } from '@/storages/UserStore.ts'; +import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; + +import userBlack from '~/fixtures/events/kind-0-black.json' with { type: 'json' }; +import userMe from '~/fixtures/events/event-0-makes-repost-with-quote-repost.json' with { type: 'json' }; +import blockEvent from '~/fixtures/events/kind-10000-black-blocks-user-me.json' with { type: 'json' }; +import event1authorUserMe from '~/fixtures/events/event-1-quote-repost-will-be-reposted.json' with { type: 'json' }; +import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; + +Deno.test('block event: muted user cannot post', async () => { + const userBlackCopy = structuredClone(userBlack); + const userMeCopy = structuredClone(userMe); + const blockEventCopy = structuredClone(blockEvent); + const event1authorUserMeCopy = structuredClone(event1authorUserMe); + + const db = new MockRelay(); + + const store = new UserStore(userBlackCopy.pubkey, db); + const policy = new MuteListPolicy(userBlack.pubkey, db); + + await store.event(blockEventCopy); + await store.event(userBlackCopy); + await store.event(userMeCopy); + + const ok = await policy.call(event1authorUserMeCopy); + + assertEquals(ok, ['OK', event1authorUserMeCopy.id, false, 'You are banned in this server.']); +}); + +Deno.test('allow event: user is NOT muted because there is no muted event', async () => { + const userBlackCopy = structuredClone(userBlack); + const userMeCopy = structuredClone(userMe); + const event1authorUserMeCopy = structuredClone(event1authorUserMe); + + const db = new MockRelay(); + + const store = new UserStore(userBlackCopy.pubkey, db); + const policy = new MuteListPolicy(userBlack.pubkey, db); + + await store.event(userBlackCopy); + await store.event(userMeCopy); + + const ok = await policy.call(event1authorUserMeCopy); + + assertEquals(ok, ['OK', event1authorUserMeCopy.id, true, '']); +}); + +Deno.test('allow event: user is NOT muted because he is not in mute event', async () => { + const userBlackCopy = structuredClone(userBlack); + const userMeCopy = structuredClone(userMe); + const event1authorUserMeCopy = structuredClone(event1authorUserMe); + const blockEventCopy = structuredClone(blockEvent); + const event1copy = structuredClone(event1); + + const db = new MockRelay(); + + const store = new UserStore(userBlackCopy.pubkey, db); + const policy = new MuteListPolicy(userBlack.pubkey, db); + + await store.event(userBlackCopy); + await store.event(blockEventCopy); + await store.event(userMeCopy); + await store.event(event1copy); + await store.event(event1authorUserMeCopy); + + const ok = await policy.call(event1copy); + + assertEquals(ok, ['OK', event1.id, true, '']); +}); From 83adf4759e96a8976d6cdf15fddfc0c1b1ef138b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 10 May 2024 16:10:11 +0000 Subject: [PATCH 116/252] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f9994283..2f56cf83 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ For more info see: https://docs.soapbox.pub/ditto/ - [x] Profiles - [ ] Search - [ ] Moderation +- [ ] Zaps - [x] Customizable - [x] Open source - [x] Self-hosted From 86518dbac5af56799dc015227f0b19c225cf8536 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 14:34:48 -0300 Subject: [PATCH 117/252] refactor(MuteListPolicy): shorthand private constructor --- src/policies/MuteListPolicy.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/policies/MuteListPolicy.ts b/src/policies/MuteListPolicy.ts index e2342199..c0695652 100644 --- a/src/policies/MuteListPolicy.ts +++ b/src/policies/MuteListPolicy.ts @@ -3,10 +3,7 @@ import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/nostrify'; import { getTagSet } from '@/tags.ts'; export class MuteListPolicy implements NPolicy { - constructor(private pubkey: string, private store: NStore) { - this.store = store; - this.pubkey = pubkey; - } + constructor(private pubkey: string, private store: NStore) {} async call(event: NostrEvent): Promise { const allowEvent = ['OK', event.id, true, ''] as NostrRelayOK; From 4069ddc02c82d2f53df49671a4e0a8febfda3755 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 14:37:02 -0300 Subject: [PATCH 118/252] refactor(MuteListPolicy): human lint preference --- src/policies/MuteListPolicy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/policies/MuteListPolicy.ts b/src/policies/MuteListPolicy.ts index c0695652..f9f8de22 100644 --- a/src/policies/MuteListPolicy.ts +++ b/src/policies/MuteListPolicy.ts @@ -6,8 +6,8 @@ export class MuteListPolicy implements NPolicy { constructor(private pubkey: string, private store: NStore) {} async call(event: NostrEvent): Promise { - const allowEvent = ['OK', event.id, true, ''] as NostrRelayOK; - const blockEvent = ['OK', event.id, false, 'You are banned in this server.'] as NostrRelayOK; + const allowEvent: NostrRelayOK = ['OK', event.id, true, '']; + const blockEvent: NostrRelayOK = ['OK', event.id, false, 'You are banned in this server.']; const [muteList] = await this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]); if (!muteList) return allowEvent; From 282612b53c8852ae62577536864d9274257e263e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 10 May 2024 14:10:19 -0500 Subject: [PATCH 119/252] Add an eventFixture function to import fixtures in tests --- .gitignore | 3 ++- src/storages/hydrate.test.ts | 28 +++++++++++++--------------- src/test.ts | 7 +++++++ 3 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 src/test.ts diff --git a/.gitignore b/.gitignore index 17f06fa0..d816f9d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env -*.cpuprofile \ No newline at end of file +*.cpuprofile +deno-test.xml \ No newline at end of file diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index 10e480e2..4e38d8a2 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -2,13 +2,12 @@ import { assertEquals } from '@/deps-test.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { MockRelay } from '@nostrify/nostrify/test'; -import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; +import { DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { eventFixture } from '@/test.ts'; + import event0madePost from '~/fixtures/events/event-0-the-one-who-post-and-users-repost.json' with { type: 'json' }; import event0madeRepost from '~/fixtures/events/event-0-the-one-who-repost.json' with { type: 'json' }; import event0madeQuoteRepost from '~/fixtures/events/event-0-the-one-who-quote-repost.json' with { type: 'json' }; -import event0madeRepostWithQuoteRepost from '~/fixtures/events/event-0-makes-repost-with-quote-repost.json' with { - type: 'json', -}; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; import event1quoteRepost from '~/fixtures/events/event-1-quote-repost.json' with { type: 'json' }; import event1futureIsMine from '~/fixtures/events/event-1-will-be-reposted-with-quote-repost.json' with { @@ -21,16 +20,15 @@ import event1willBeQuoteReposted from '~/fixtures/events/event-1-that-will-be-qu import event1reposted from '~/fixtures/events/event-1-reposted.json' with { type: 'json' }; import event6 from '~/fixtures/events/event-6.json' with { type: 'json' }; import event6ofQuoteRepost from '~/fixtures/events/event-6-of-quote-repost.json' with { type: 'json' }; -import { DittoEvent } from '@/interfaces/DittoEvent.ts'; Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => { const db = new MockRelay(); - const event0copy = structuredClone(event0); + const event0 = await eventFixture('event-0'); const event1copy = structuredClone(event1); // Save events to database - await db.event(event0copy); + await db.event(event0); await db.event(event1copy); assertEquals((event1copy as DittoEvent).author, undefined, "Event hasn't been hydrated yet"); @@ -40,7 +38,7 @@ Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => { storage: db, }); - const expectedEvent = { ...event1copy, author: event0copy }; + const expectedEvent = { ...event1copy, author: event0 }; assertEquals(event1copy, expectedEvent); }); @@ -78,13 +76,13 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => { const db = new MockRelay(); const event0madeQuoteRepostCopy = structuredClone(event0madeQuoteRepost); - const event0copy = structuredClone(event0); + const event0 = await eventFixture('event-0'); const event1quoteRepostCopy = structuredClone(event1quoteRepost); const event1willBeQuoteRepostedCopy = structuredClone(event1willBeQuoteReposted); // Save events to database await db.event(event0madeQuoteRepostCopy); - await db.event(event0copy); + await db.event(event0); await db.event(event1quoteRepostCopy); await db.event(event1willBeQuoteRepostedCopy); @@ -96,7 +94,7 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => { const expectedEvent1quoteRepost = { ...event1quoteRepostCopy, author: event0madeQuoteRepostCopy, - quote: { ...event1willBeQuoteRepostedCopy, author: event0copy }, + quote: { ...event1willBeQuoteRepostedCopy, author: event0 }, }; assertEquals(event1quoteRepostCopy, expectedEvent1quoteRepost); @@ -105,13 +103,13 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => { Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () => { const db = new MockRelay(); - const event0copy = structuredClone(event0madeRepostWithQuoteRepost); + const author = await eventFixture('event-0-makes-repost-with-quote-repost'); const event1copy = structuredClone(event1futureIsMine); const event1quoteCopy = structuredClone(event1quoteRepostLatin); const event6copy = structuredClone(event6ofQuoteRepost); // Save events to database - await db.event(event0copy); + await db.event(author); await db.event(event1copy); await db.event(event1quoteCopy); await db.event(event6copy); @@ -126,8 +124,8 @@ Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () const expectedEvent6 = { ...event6copy, - author: event0copy, - repost: { ...event1quoteCopy, author: event0copy, quote: { author: event0copy, ...event1copy } }, + author, + repost: { ...event1quoteCopy, author, quote: { author, ...event1copy } }, }; assertEquals(event6copy, expectedEvent6); }); diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 00000000..45862828 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,7 @@ +import { NostrEvent } from '@nostrify/nostrify'; + +/** Import an event fixture by name in tests. */ +export async function eventFixture(name: string): Promise { + const result = await import(`~/fixtures/events/${name}.json`, { with: { type: 'json' } }); + return structuredClone(result.default); +} From d3b7668a1e241c1af6029d6e5b997e0f373f4d5d Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 16:51:07 -0300 Subject: [PATCH 120/252] fix: create renderAdminAccountFromPubkey and use it if reported account doesn't have a kind 0 --- src/views/mastodon/admin-accounts.ts | 38 ++++++++++++++++++++++------ src/views/mastodon/reports.ts | 10 +++++--- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts index 411a6555..4dc85699 100644 --- a/src/views/mastodon/admin-accounts.ts +++ b/src/views/mastodon/admin-accounts.ts @@ -1,23 +1,21 @@ +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { nostrDate } from '@/utils.ts'; -import { renderAccount } from '@/views/mastodon/accounts.ts'; - -/** Expects a kind 0 fully hydrated or a kind 30361 hydrated with `d_author` */ +/** Expects a kind 0 fully hydrated */ async function renderAdminAccount(event: DittoEvent) { const account = await renderAccount(event); return { id: account.id, - username: event.tags.find(([name]) => name === 'name')?.[1]!, + username: account.username, domain: account.acct.split('@')[1] || null, - created_at: nostrDate(event.created_at).toISOString(), + created_at: account.created_at, email: '', ip: null, ips: [], locale: '', invite_request: null, - role: event.tags.find(([name]) => name === 'role')?.[1] || 'user', + role: event.tags.find(([name]) => name === 'role')?.[1], confirmed: true, approved: true, disabled: false, @@ -27,4 +25,28 @@ async function renderAdminAccount(event: DittoEvent) { }; } -export { renderAdminAccount }; +/** Expects a target pubkey */ +async function renderAdminAccountFromPubkey(pubkey: string) { + const account = await accountFromPubkey(pubkey); + + return { + id: account.id, + username: account.username, + domain: account.acct.split('@')[1] || null, + created_at: account.created_at, + email: '', + ip: null, + ips: [], + locale: '', + invite_request: null, + role: 'user', + confirmed: true, + approved: true, + disabled: false, + silenced: false, + suspended: false, + account, + }; +} + +export { renderAdminAccount, renderAdminAccountFromPubkey }; diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index f4908de1..31369413 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -1,7 +1,7 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { nostrDate } from '@/utils.ts'; -import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; +import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; /** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ @@ -57,8 +57,12 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor comment: reportEvent.content, forwarded: false, created_at: nostrDate(reportEvent.created_at).toISOString(), - account: await renderAdminAccount(reportEvent.author as DittoEvent), - target_account: await renderAdminAccount(reportEvent.reported_profile as DittoEvent), + account: reportEvent.author + ? await renderAdminAccount(reportEvent.author) + : await renderAdminAccountFromPubkey(reportEvent.pubkey), + target_account: reportEvent.reported_profile + ? await renderAdminAccount(reportEvent.reported_profile) + : await renderAdminAccountFromPubkey(reportEvent.tags.find(([name]) => name === 'p')![1]), assigned_account: null, action_taken_by_account: null, statuses, From 3970bb81f76853dec4a1dda4c2fd6876dae2ec26 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 18:03:35 -0300 Subject: [PATCH 121/252] refactor(MuteListPolicy): simplify condition --- src/policies/MuteListPolicy.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/policies/MuteListPolicy.ts b/src/policies/MuteListPolicy.ts index f9f8de22..1db85566 100644 --- a/src/policies/MuteListPolicy.ts +++ b/src/policies/MuteListPolicy.ts @@ -6,15 +6,13 @@ export class MuteListPolicy implements NPolicy { constructor(private pubkey: string, private store: NStore) {} async call(event: NostrEvent): Promise { - const allowEvent: NostrRelayOK = ['OK', event.id, true, '']; - const blockEvent: NostrRelayOK = ['OK', event.id, false, 'You are banned in this server.']; - const [muteList] = await this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]); - if (!muteList) return allowEvent; + const pubkeys = getTagSet(muteList?.tags ?? [], 'p'); - const mutedPubkeys = getTagSet(muteList.tags, 'p'); - if (mutedPubkeys.has(event.pubkey)) return blockEvent; + if (pubkeys.has(event.pubkey)) { + return ['OK', event.id, false, 'You are banned in this server.']; + } - return allowEvent; + return ['OK', event.id, true, '']; } } From 4fdf15761c1e5570df4e5a18e8b5e5d6074c0ff6 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 18:18:55 -0300 Subject: [PATCH 122/252] refactor(UserStore): remove isMuted function --- src/storages/UserStore.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/storages/UserStore.ts b/src/storages/UserStore.ts index b13df420..1c7aaee2 100644 --- a/src/storages/UserStore.ts +++ b/src/storages/UserStore.ts @@ -30,11 +30,6 @@ export class UserStore implements NStore { }); } - async isMuted(pubkey: string): Promise { - const mutedPubkeys = await this.getMutedPubkeys(); - return mutedPubkeys.has(pubkey); - } - private async getMuteList(): Promise { const [muteList] = await this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]); return muteList; From 732cb45b1eb60b868e046c843ca8f176470d40c5 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 18:20:00 -0300 Subject: [PATCH 123/252] test(UserStore): update with 100.00% code coverage --- src/storages/UserStore.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/storages/UserStore.test.ts b/src/storages/UserStore.test.ts index 42c04399..b1955bd9 100644 --- a/src/storages/UserStore.test.ts +++ b/src/storages/UserStore.test.ts @@ -24,7 +24,6 @@ Deno.test('query events of users that are not muted', async () => { await store.event(event1authorUserMeCopy); assertEquals(await store.query([{ kinds: [1] }], { limit: 1 }), []); - assertEquals(await store.isMuted(userMeCopy.pubkey), true); }); Deno.test('user never muted anyone', async () => { @@ -38,5 +37,5 @@ Deno.test('user never muted anyone', async () => { await store.event(userBlackCopy); await store.event(userMeCopy); - assertEquals(await store.isMuted(userMeCopy.pubkey), false); + assertEquals(await store.query([{ kinds: [0], authors: [userMeCopy.pubkey] }], { limit: 1 }), [userMeCopy]); }); From 323e425e8be62ae68d4befd5f2e16d6efcefc0c8 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 10 May 2024 19:05:15 -0300 Subject: [PATCH 124/252] fix(renderAdminReport): make sure reportedPubkey is not undefined --- src/views/mastodon/reports.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index 31369413..e6e0090c 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -49,6 +49,11 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor } } + const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')![1]; + if (!reportedPubkey) { + return; + } + return { id: reportEvent.id, action_taken: actionTaken, @@ -62,7 +67,7 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor : await renderAdminAccountFromPubkey(reportEvent.pubkey), target_account: reportEvent.reported_profile ? await renderAdminAccount(reportEvent.reported_profile) - : await renderAdminAccountFromPubkey(reportEvent.tags.find(([name]) => name === 'p')![1]), + : await renderAdminAccountFromPubkey(reportedPubkey), assigned_account: null, action_taken_by_account: null, statuses, From c017770760ee5c9257807c26ea5162e1fa8a992d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 10 May 2024 22:17:44 +0000 Subject: [PATCH 125/252] ! -> ?. --- src/views/mastodon/reports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mastodon/reports.ts b/src/views/mastodon/reports.ts index e6e0090c..bec08b4a 100644 --- a/src/views/mastodon/reports.ts +++ b/src/views/mastodon/reports.ts @@ -49,7 +49,7 @@ async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminRepor } } - const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')![1]; + const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1]; if (!reportedPubkey) { return; } From 801e68c6c46d06bb8b59f9d5d87ba51194da21c4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 11 May 2024 12:03:41 -0300 Subject: [PATCH 126/252] fix: add error prefix according to NIP-01 --- src/policies/MuteListPolicy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/policies/MuteListPolicy.ts b/src/policies/MuteListPolicy.ts index 1db85566..cae08eba 100644 --- a/src/policies/MuteListPolicy.ts +++ b/src/policies/MuteListPolicy.ts @@ -10,7 +10,7 @@ export class MuteListPolicy implements NPolicy { const pubkeys = getTagSet(muteList?.tags ?? [], 'p'); if (pubkeys.has(event.pubkey)) { - return ['OK', event.id, false, 'You are banned in this server.']; + return ['OK', event.id, false, 'blocked: Your account has been deactivated.']; } return ['OK', event.id, true, '']; From fe66937bba49d6aa00679ed5da3bbf4b16c29069 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 11 May 2024 12:04:44 -0300 Subject: [PATCH 127/252] feat: do not allow deactivated accounts to post --- src/pipeline.ts | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index 3eb8913d..d05b09a3 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -21,18 +21,10 @@ import { AdminSigner } from '@/signers/AdminSigner.ts'; import { lnurlCache } from '@/utils/lnurl.ts'; import { nip05Cache } from '@/utils/nip05.ts'; +import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; + const debug = Debug('ditto:pipeline'); -let UserPolicy: any; - -try { - UserPolicy = (await import('../data/policy.ts')).default; - debug('policy loaded from data/policy.ts'); -} catch (_e) { - // do nothing - debug('policy not found'); -} - /** * Common pipeline function to process (and maybe store) events. * It is idempotent, so it can be called multiple times for the same event. @@ -43,17 +35,8 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise ${event.id}`); await hydrateEvent(event, signal); - if (UserPolicy) { - const result = await new UserPolicy().call(event, signal); - debug(JSON.stringify(result)); - const [_, _eventId, ok, reason] = result; - if (!ok) { - const [prefix, ...rest] = reason.split(': '); - throw new RelayError(prefix, rest.join(': ')); - } - } - await Promise.all([ + policyFilter(event), storeEvent(event, signal), parseMetadata(event, signal), processDeletions(event, signal), @@ -66,6 +49,25 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { + const UserPolicy = new MuteListPolicy(Conf.pubkey, Storages.admin); + const result = await UserPolicy.call(event); + + debug(JSON.stringify(result)); + + const [_, _eventId, ok, reason] = result; + if (!ok) { + const [prefix, ...rest] = reason.split(': '); + if (['duplicate', 'pow', 'blocked', 'rate-limited', 'invalid'].includes(prefix)) { + const error = new RelayError(prefix as any, rest.join(': ')); + return Promise.reject(error); + } else { + const error = new RelayError('error', rest.join(': ')); + return Promise.reject(error); + } + } +} + /** Encounter the event, and return whether it has already been encountered. */ async function encounterEvent(event: NostrEvent, signal: AbortSignal): Promise { const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]); From 04968fefaa9abd57e24c0609d45392644e8e50a3 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 11 May 2024 14:02:24 -0300 Subject: [PATCH 128/252] test(MuteListPolicy): update error msg --- src/policies/MuteListPolicy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/policies/MuteListPolicy.test.ts b/src/policies/MuteListPolicy.test.ts index 69561f80..2c3baa3d 100644 --- a/src/policies/MuteListPolicy.test.ts +++ b/src/policies/MuteListPolicy.test.ts @@ -27,7 +27,7 @@ Deno.test('block event: muted user cannot post', async () => { const ok = await policy.call(event1authorUserMeCopy); - assertEquals(ok, ['OK', event1authorUserMeCopy.id, false, 'You are banned in this server.']); + assertEquals(ok, ['OK', event1authorUserMeCopy.id, false, 'blocked: Your account has been deactivated.']); }); Deno.test('allow event: user is NOT muted because there is no muted event', async () => { From 65034a4aae10fa95f0e1900b4f46b6ddcaa997fe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 11 May 2024 14:34:02 -0500 Subject: [PATCH 129/252] Support Explicit Addressing --- src/controllers/api/statuses.ts | 46 +++++++++++++++++---------------- src/utils/lookup.ts | 27 ++++++++++++++----- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 56ea38b2..3420383a 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,6 +1,5 @@ -import { NIP05, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import ISO6391 from 'iso-639-1'; -import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; @@ -12,10 +11,10 @@ import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/uti import { renderEventAccounts } from '@/views.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { getLnurl } from '@/utils/lnurl.ts'; -import { nip05Cache } from '@/utils/nip05.ts'; import { asyncReplaceAll } from '@/utils/text.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; +import { lookupPubkey } from '@/utils/lookup.ts'; const createStatusSchema = z.object({ in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(), @@ -31,6 +30,7 @@ const createStatusSchema = z.object({ sensitive: z.boolean().nullish(), spoiler_text: z.string().nullish(), status: z.string().nullish(), + to: z.string().array().nullish(), visibility: z.enum(['public', 'unlisted', 'private', 'direct']).nullish(), quote_id: z.string().nullish(), }).refine( @@ -97,30 +97,32 @@ const createStatusController: AppController = async (c) => { tags.push(...media); } + const pubkeys = new Set(); + const content = await asyncReplaceAll(data.status ?? '', /@([\w@+._]+)/g, async (match, username) => { - try { - const result = nip19.decode(username); - if (result.type === 'npub') { - tags.push(['p', result.data]); - return `nostr:${username}`; - } else { - return match; - } - } catch (_e) { - // do nothing + const pubkey = await lookupPubkey(username); + if (!pubkey) return match; + + // Content addressing (default) + if (!data.to) { + pubkeys.add(pubkey); } - if (NIP05.regex().test(username)) { - const pointer = await nip05Cache.fetch(username); - if (pointer) { - tags.push(['p', pointer.pubkey]); - return `nostr:${nip19.npubEncode(pointer.pubkey)}`; - } - } - - return match; + return `nostr:${pubkey}`; }); + // Explicit addressing + for (const to of data.to ?? []) { + const pubkey = await lookupPubkey(to); + if (pubkey) { + pubkeys.add(pubkey); + } + } + + for (const pubkey of pubkeys) { + tags.push(['p', pubkey]); + } + for (const match of content.matchAll(/#(\w+)/g)) { tags.push(['t', match[1]]); } diff --git a/src/utils/lookup.ts b/src/utils/lookup.ts index 5fcad59e..ce42e2f2 100644 --- a/src/utils/lookup.ts +++ b/src/utils/lookup.ts @@ -1,18 +1,31 @@ -import { NostrEvent } from '@nostrify/nostrify'; +import { NIP05, NostrEvent } from '@nostrify/nostrify'; + import { getAuthor } from '@/queries.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { nip05Cache } from '@/utils/nip05.ts'; /** Resolve a bech32 or NIP-05 identifier to an account. */ -async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)): Promise { - console.log(`Looking up ${value}`); - - const pubkey = bech32ToPubkey(value) || - await nip05Cache.fetch(value, { signal }).then(({ pubkey }) => pubkey).catch(() => undefined); +export async function lookupAccount( + value: string, + signal = AbortSignal.timeout(3000), +): Promise { + const pubkey = await lookupPubkey(value, signal); if (pubkey) { return getAuthor(pubkey); } } -export { lookupAccount }; +/** Resolve a bech32 or NIP-05 identifier to a pubkey. */ +export async function lookupPubkey(value: string, signal?: AbortSignal): Promise { + if (NIP05.regex().test(value)) { + try { + const { pubkey } = await nip05Cache.fetch(value, { signal }); + return pubkey; + } catch { + return; + } + } + + return bech32ToPubkey(value); +} From 928ae4ec2252da2ec2e01930c15c92274f9e7af9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 10:56:27 -0500 Subject: [PATCH 130/252] oauthController: calculate the script hash on the fly so we can edit it --- src/controllers/api/oauth.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index 4f1f4959..e45b31a5 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -1,3 +1,4 @@ +import { encodeBase64 } from '@std/encoding/base64'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; @@ -60,7 +61,7 @@ const createTokenController: AppController = async (c) => { }; /** Display the OAuth form. */ -const oauthController: AppController = (c) => { +const oauthController: AppController = async (c) => { const encodedUri = c.req.query('redirect_uri'); if (!encodedUri) { return c.text('Missing `redirect_uri` query param.', 422); @@ -68,17 +69,7 @@ const oauthController: AppController = (c) => { const redirectUri = maybeDecodeUri(encodedUri); - c.res.headers.set( - 'content-security-policy', - "default-src 'self' 'sha256-m2qD6rbE2Ixbo2Bjy2dgQebcotRIAawW7zbmXItIYAM='", - ); - - return c.html(` - - - Log in with Ditto - - + `; + + const hash = encodeBase64(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(script))); + + c.res.headers.set( + 'content-security-policy', + `default-src 'self' 'sha256-${hash}'`, + ); + + return c.html(` + + + Log in with Ditto + +
From bdfa6f882640006f9fd3a7a1e7dd9de76b4b5eb0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 12:32:40 -0500 Subject: [PATCH 131/252] Add a getInstanceMetadata function to DRY a few controllers --- src/controllers/api/instance.ts | 20 ++++++---------- src/controllers/nostr/relay-info.ts | 15 ++++-------- src/utils/instance.ts | 37 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 src/utils/instance.ts diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 70f38e14..cc71b1f1 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,25 +1,19 @@ -import { NSchema as n } from '@nostrify/nostrify'; - -import { type AppController } from '@/app.ts'; +import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { serverMetaSchema } from '@/schemas/nostr.ts'; -import { Storages } from '@/storages.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; const instanceController: AppController = async (c) => { const { host, protocol } = Conf.url; - const { signal } = c.req.raw; - - const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content); + const meta = await getInstanceMetadata(c.req.raw.signal); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; return c.json({ uri: host, - title: meta.name ?? 'Ditto', - description: meta.about ?? 'Nostr and the Fediverse', - short_description: meta.tagline ?? meta.about ?? 'Nostr and the Fediverse', + title: meta.name, + description: meta.about, + short_description: meta.tagline, registrations: true, max_toot_chars: Conf.postCharLimit, configuration: { @@ -59,7 +53,7 @@ const instanceController: AppController = async (c) => { streaming_api: `${wsProtocol}//${host}`, }, version: '0.0.0 (compatible; Ditto 0.0.1)', - email: meta.email ?? `postmaster@${host}`, + email: meta.email, nostr: { pubkey: Conf.pubkey, relay: `${wsProtocol}//${host}/relay`, diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index a56df51e..192cab22 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,20 +1,15 @@ -import { NSchema as n } from '@nostrify/nostrify'; - import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { serverMetaSchema } from '@/schemas/nostr.ts'; -import { Storages } from '@/storages.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; const relayInfoController: AppController = async (c) => { - const { signal } = c.req.raw; - const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); - const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content); + const meta = await getInstanceMetadata(c.req.raw.signal); return c.json({ - name: meta.name ?? 'Ditto', - description: meta.about ?? 'Nostr and the Fediverse.', + name: meta.name, + description: meta.about, pubkey: Conf.pubkey, - contact: `mailto:${meta.email ?? `postmaster@${Conf.url.host}`}`, + contact: meta.email, supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98], software: 'Ditto', version: '0.0.0', diff --git a/src/utils/instance.ts b/src/utils/instance.ts new file mode 100644 index 00000000..004e4cf1 --- /dev/null +++ b/src/utils/instance.ts @@ -0,0 +1,37 @@ +import { NostrEvent, NostrMetadata, NSchema as n } from '@nostrify/nostrify'; + +import { Conf } from '@/config.ts'; +import { serverMetaSchema } from '@/schemas/nostr.ts'; +import { Storages } from '@/storages.ts'; + +/** Like NostrMetadata, but some fields are required and also contains some extra fields. */ +export interface InstanceMetadata extends NostrMetadata { + name: string; + about: string; + tagline: string; + email: string; + event?: NostrEvent; +} + +/** Get and parse instance metadata from the kind 0 of the admin user. */ +export async function getInstanceMetadata(signal?: AbortSignal): Promise { + const [event] = await Storages.db.query( + [{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], + { signal }, + ); + + const meta = n + .json() + .pipe(serverMetaSchema) + .catch({}) + .parse(event?.content); + + return { + ...meta, + name: meta.name ?? 'Ditto', + about: meta.about ?? 'Nostr community server', + tagline: meta.tagline ?? meta.about ?? 'Nostr community server', + email: meta.email ?? `postmaster@${Conf.url.host}`, + event, + }; +} From 3b0739f187172decc27aded70756031cc454c2e6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 13:12:46 -0500 Subject: [PATCH 132/252] Add a getClientConnectUri function, add "Nostr Connect" link in the OAuth form --- src/controllers/api/oauth.ts | 6 +++++- src/utils/connect.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/utils/connect.ts diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index e45b31a5..a755a4d5 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -2,10 +2,11 @@ import { encodeBase64 } from '@std/encoding/base64'; import { nip19 } from 'nostr-tools'; import { z } from 'zod'; -import { lodash } from '@/deps.ts'; import { AppController } from '@/app.ts'; +import { lodash } from '@/deps.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; +import { getClientConnectUri } from '@/utils/connect.ts'; const passwordGrantSchema = z.object({ grant_type: z.literal('password'), @@ -68,6 +69,7 @@ const oauthController: AppController = async (c) => { } const redirectUri = maybeDecodeUri(encodedUri); + const connectUri = await getClientConnectUri(c.req.raw.signal); const script = ` window.addEventListener('load', function() { @@ -101,6 +103,8 @@ const oauthController: AppController = async (c) => { +
+ Nostr Connect `); diff --git a/src/utils/connect.ts b/src/utils/connect.ts new file mode 100644 index 00000000..0c69a523 --- /dev/null +++ b/src/utils/connect.ts @@ -0,0 +1,20 @@ +import { Conf } from '@/config.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; + +/** Get NIP-46 `nostrconnect://` URI for the Ditto server. */ +export async function getClientConnectUri(signal?: AbortSignal): Promise { + const uri = new URL('nostrconnect://'); + const { name, description } = await getInstanceMetadata(signal); + + const metadata = { + name, + description, + url: Conf.localDomain, + }; + + uri.host = Conf.pubkey; + uri.searchParams.set('relay', Conf.relay); + uri.searchParams.set('metadata', JSON.stringify(metadata)); + + return uri.toString(); +} From dc8010a78eb1fd54e5403649d0127188f586c625 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 12 May 2024 13:26:26 -0500 Subject: [PATCH 133/252] getClientConnectUri: fix description value --- src/utils/connect.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/utils/connect.ts b/src/utils/connect.ts index 0c69a523..8b3fdf86 100644 --- a/src/utils/connect.ts +++ b/src/utils/connect.ts @@ -1,14 +1,21 @@ import { Conf } from '@/config.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; +/** NIP-46 client-connect metadata. */ +interface ConnectMetadata { + name: string; + description: string; + url: string; +} + /** Get NIP-46 `nostrconnect://` URI for the Ditto server. */ export async function getClientConnectUri(signal?: AbortSignal): Promise { const uri = new URL('nostrconnect://'); - const { name, description } = await getInstanceMetadata(signal); + const { name, tagline } = await getInstanceMetadata(signal); - const metadata = { + const metadata: ConnectMetadata = { name, - description, + description: tagline, url: Conf.localDomain, }; From 2140b3fbb22ada324f5ef0e084141a1ae5ac3283 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 13 May 2024 11:21:17 -0500 Subject: [PATCH 134/252] lookupPubkey: check the bech32 first --- ..env.swp | Bin 0 -> 1024 bytes src/utils/lookup.ts | 14 ++++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 ..env.swp diff --git a/..env.swp b/..env.swp new file mode 100644 index 0000000000000000000000000000000000000000..2ab134254314be032359c6894717262e417e7684 GIT binary patch literal 1024 zcmYc?$V<%2S1{8vVn6|1lPwq$b5bi%1aWW*@(XnHi*ZOI3G1cil_7CQnWG^v8Uh0x F0sw&A3CREe literal 0 HcmV?d00001 diff --git a/src/utils/lookup.ts b/src/utils/lookup.ts index ce42e2f2..90b30c2b 100644 --- a/src/utils/lookup.ts +++ b/src/utils/lookup.ts @@ -1,8 +1,9 @@ -import { NIP05, NostrEvent } from '@nostrify/nostrify'; +import { NIP05, NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { getAuthor } from '@/queries.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { nip05Cache } from '@/utils/nip05.ts'; +import { Stickynotes } from '@soapbox/stickynotes'; /** Resolve a bech32 or NIP-05 identifier to an account. */ export async function lookupAccount( @@ -18,14 +19,19 @@ export async function lookupAccount( /** Resolve a bech32 or NIP-05 identifier to a pubkey. */ export async function lookupPubkey(value: string, signal?: AbortSignal): Promise { + const console = new Stickynotes('ditto:lookup'); + + if (n.bech32().safeParse(value).success) { + return bech32ToPubkey(value); + } + if (NIP05.regex().test(value)) { try { const { pubkey } = await nip05Cache.fetch(value, { signal }); return pubkey; - } catch { + } catch (e) { + console.debug(e); return; } } - - return bech32ToPubkey(value); } From 9bff7a5086b62a0f61d6ff2a84f891e591c18eb9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 13 May 2024 12:30:56 -0500 Subject: [PATCH 135/252] Fix some issues in pipeline and utils/api.ts --- deno.json | 2 +- src/pipeline.ts | 17 +++++++++-------- src/utils/api.ts | 6 ++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/deno.json b/deno.json index 5d455889..02255105 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.17.1", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/pipeline.ts b/src/pipeline.ts index d05b09a3..c086c921 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -35,8 +35,9 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise ${event.id}`); await hydrateEvent(event, signal); + await policyFilter(event); + await Promise.all([ - policyFilter(event), storeEvent(event, signal), parseMetadata(event, signal), processDeletions(event, signal), @@ -50,8 +51,8 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - const UserPolicy = new MuteListPolicy(Conf.pubkey, Storages.admin); - const result = await UserPolicy.call(event); + const policy = new MuteListPolicy(Conf.pubkey, Storages.admin); + const result = await policy.call(event); debug(JSON.stringify(result)); @@ -59,11 +60,9 @@ async function policyFilter(event: NostrEvent): Promise { if (!ok) { const [prefix, ...rest] = reason.split(': '); if (['duplicate', 'pow', 'blocked', 'rate-limited', 'invalid'].includes(prefix)) { - const error = new RelayError(prefix as any, rest.join(': ')); - return Promise.reject(error); + throw new RelayError(prefix as RelayErrorPrefix, rest.join(': ')); } else { - const error = new RelayError('error', rest.join(': ')); - return Promise.reject(error); + throw new RelayError('error', rest.join(': ')); } } } @@ -272,9 +271,11 @@ async function streamOut(event: NostrEvent): Promise { } } +type RelayErrorPrefix = 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error'; + /** NIP-20 command line result. */ class RelayError extends Error { - constructor(prefix: 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error', message: string) { + constructor(prefix: RelayErrorPrefix, message: string) { super(`${prefix}: ${message}`); } } diff --git a/src/utils/api.ts b/src/utils/api.ts index cba7c663..8da87fb6 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -103,10 +103,8 @@ async function updateAdminEvent( async function publishEvent(event: NostrEvent, c: AppContext): Promise { debug('EVENT', event); try { - await Promise.all([ - pipeline.handleEvent(event, c.req.raw.signal), - Storages.client.event(event), - ]); + await pipeline.handleEvent(event, c.req.raw.signal); + await Storages.client.event(event); } catch (e) { if (e instanceof pipeline.RelayError) { throw new HTTPException(422, { From 6105e00c808ebc5a5b2ff2a764aa121ed95d2be8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 13 May 2024 12:43:01 -0500 Subject: [PATCH 136/252] pipeline: add a placeholder for custom policy --- src/pipeline.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index c086c921..7da026f0 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,5 +1,6 @@ import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; +import { PipePolicy } from '@nostrify/nostrify/policies'; import Debug from '@soapbox/stickynotes/debug'; import { sql } from 'kysely'; @@ -33,9 +34,12 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise ${event.id}`); - await hydrateEvent(event, signal); - await policyFilter(event); + if (event.kind !== 24133) { + await policyFilter(event); + } + + await hydrateEvent(event, signal); await Promise.all([ storeEvent(event, signal), @@ -51,9 +55,12 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - const policy = new MuteListPolicy(Conf.pubkey, Storages.admin); - const result = await policy.call(event); + const policy = new PipePolicy([ + new MuteListPolicy(Conf.pubkey, Storages.admin), + // put custom policy here + ]); + const result = await policy.call(event); debug(JSON.stringify(result)); const [_, _eventId, ok, reason] = result; From 4029971407165173cacf65105fbc4a04dc682816 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 17:44:33 -0300 Subject: [PATCH 137/252] fix(pipeline): load custom policy if available --- src/pipeline.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index 7da026f0..a47a2da5 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; import { PipePolicy } from '@nostrify/nostrify/policies'; import Debug from '@soapbox/stickynotes/debug'; @@ -55,10 +55,18 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - const policy = new PipePolicy([ + const policies: NPolicy[] = [ new MuteListPolicy(Conf.pubkey, Storages.admin), - // put custom policy here - ]); + ]; + + try { + const customPolicy = (await import('../data/policy.ts')).default; + policies.push(new customPolicy()); + } catch (_e) { + debug('policy not found - https://docs.soapbox.pub/ditto/policies/'); + } + + const policy = new PipePolicy(policies.reverse()); const result = await policy.call(event); debug(JSON.stringify(result)); From 9cb63a99183ab2e09adf5ba90846f505f6f52fb2 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 20:18:11 -0300 Subject: [PATCH 138/252] test: kind 0(user 'dictator') fixture --- fixtures/events/kind-0-dictator.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fixtures/events/kind-0-dictator.json diff --git a/fixtures/events/kind-0-dictator.json b/fixtures/events/kind-0-dictator.json new file mode 100644 index 00000000..a547332d --- /dev/null +++ b/fixtures/events/kind-0-dictator.json @@ -0,0 +1,9 @@ +{ + "id": "2238893aee54bbe9188498a5aa124d62870d5757894bf52cdb362d1a0874ed18", + "pubkey": "c9f5508526e213c3bc5468161f1b738a86063a2ece540730f9412e7becd5f0b2", + "created_at": 1715517440, + "kind": 0, + "tags": [], + "content": "{\"name\":\"dictator\",\"about\":\"\",\"nip05\":\"\"}", + "sig": "a630ba158833eea10289fe077087ccad22c71ddfbe475153958cfc158ae94fb0a5f7b7626e62da6a3ef8bfbe67321e8f993517ed7f1578a45aff11bc2bec484c" +} From aec0afd731d6da6247d6ee2cd272f00a5ba9a583 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 20:18:42 -0300 Subject: [PATCH 139/252] test: kind 0(user 'george orwell') fixture --- fixtures/events/kind-0-george-orwell.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fixtures/events/kind-0-george-orwell.json diff --git a/fixtures/events/kind-0-george-orwell.json b/fixtures/events/kind-0-george-orwell.json new file mode 100644 index 00000000..d8354478 --- /dev/null +++ b/fixtures/events/kind-0-george-orwell.json @@ -0,0 +1,9 @@ +{ + "id": "da4e1e727c6456cee2b0341a1d7a2356e4263523374a2570a7dd318ab5d73f93", + "pubkey": "e4d96e951739787e62ada74ee06a9a185af22791a899a6166ec23aab58c5d700", + "created_at": 1715517565, + "kind": 0, + "tags": [], + "content": "{\"name\":\"george orwell\",\"about\":\"\",\"nip05\":\"\"}", + "sig": "cd375e2065cf452d3bfefa9951b04ab63018ab7c253803256cca1d89d03b38e454c71ed36fdd3c28a8ff2723cc19b21371ce0f9bbd39a92b1d1aa946137237bd" +} From f225f7498df6383ee8978d1c7d31468192327535 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 20:19:30 -0300 Subject: [PATCH 140/252] test: kind 1 (author is 'george orwell') fixture --- fixtures/events/kind-1-author-george-orwell.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fixtures/events/kind-1-author-george-orwell.json diff --git a/fixtures/events/kind-1-author-george-orwell.json b/fixtures/events/kind-1-author-george-orwell.json new file mode 100644 index 00000000..d1bd4abe --- /dev/null +++ b/fixtures/events/kind-1-author-george-orwell.json @@ -0,0 +1,9 @@ +{ + "id": "44f19148f5af60b0f43ed8c737fbda31b165e05bb55562003c45d9a9f02e8228", + "pubkey": "e4d96e951739787e62ada74ee06a9a185af22791a899a6166ec23aab58c5d700", + "created_at": 1715636249, + "kind": 1, + "tags": [], + "content": "I like free speech", + "sig": "6b50db9c1c02bd8b0e64512e71d53a0058569f44e8dcff65ad17fce544d6ae79f8f79fa0f9a615446fa8cbc2375709bf835751843b0cd10e62ae5d505fe106d4" +} From c67cb034b08d10e867dbc4a1a6b3c6fc53d48f10 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 20:20:18 -0300 Subject: [PATCH 141/252] test: kind 1984 (author 'dictator') fixture --- ...d-1984-dictator-reports-george-orwell.json | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 fixtures/events/kind-1984-dictator-reports-george-orwell.json diff --git a/fixtures/events/kind-1984-dictator-reports-george-orwell.json b/fixtures/events/kind-1984-dictator-reports-george-orwell.json new file mode 100644 index 00000000..7280c594 --- /dev/null +++ b/fixtures/events/kind-1984-dictator-reports-george-orwell.json @@ -0,0 +1,24 @@ +{ + "id": "129b2749330a7f1189d3e74c6764a955851f1e4017a818dfd51ab8e24192b0f3", + "pubkey": "c9f5508526e213c3bc5468161f1b738a86063a2ece540730f9412e7becd5f0b2", + "created_at": 1715636348, + "kind": 1984, + "tags": [ + [ + "p", + "e4d96e951739787e62ada74ee06a9a185af22791a899a6166ec23aab58c5d700", + "other" + ], + [ + "P", + "e724b1c1b90eab9cc0f5976b380b80dda050de1820dc143e62d9e4f27a9a0b2c" + ], + [ + "e", + "44f19148f5af60b0f43ed8c737fbda31b165e05bb55562003c45d9a9f02e8228", + "other" + ] + ], + "content": "freedom of speech not freedom of reach", + "sig": "cd05a14749cdf0c7664d056e2c02518740000387732218dacd0c71de5b96c0c3c99a0b927b0cd0778f25a211525fa03b4ed4f4f537bb1221c73467780d4ee1bc" +} From 78137f373f7e14c7b228b0b8a630df532817c1e4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 20:22:27 -0300 Subject: [PATCH 142/252] test(hydrate): kind 1984 --- src/storages/hydrate.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index 4e38d8a2..1664a6dd 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -129,3 +129,31 @@ Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () }; assertEquals(event6copy, expectedEvent6); }); + +Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stats', async () => { + const db = new MockRelay(); + + const authorDictator = await eventFixture('kind-0-dictator'); + const authorVictim = await eventFixture('kind-0-george-orwell'); + const reportEvent = await eventFixture('kind-1984-dictator-reports-george-orwell'); + const event1 = await eventFixture('kind-1-author-george-orwell'); + + // Save events to database + await db.event(authorDictator); + await db.event(authorVictim); + await db.event(reportEvent); + await db.event(event1); + + await hydrateEvents({ + events: [reportEvent], + storage: db, + }); + + const expectedEvent: DittoEvent = { + ...reportEvent, + author: authorDictator, + reported_notes: [event1], + reported_profile: authorVictim, + }; + assertEquals(reportEvent, expectedEvent); +}); From cc9d2efef9501f1582fc80cd13ecdcecd218e4e7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 13 May 2024 18:50:20 -0500 Subject: [PATCH 143/252] Fix mentions --- src/views/mastodon/statuses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index c743e8b6..d00759db 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -41,7 +41,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< ]; const mentionedProfiles = await Storages.optimizer.query( - [{ authors: mentionedPubkeys, limit: mentionedPubkeys.length }], + [{ kinds: [0], authors: mentionedPubkeys, limit: mentionedPubkeys.length }], ); const { html, links, firstUrl } = parseNoteContent(event.content); From caaa7016f0feb875105166704974755398b2c063 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 13 May 2024 21:56:29 -0300 Subject: [PATCH 144/252] test(hydrate): refactor to import fixtures with 'eventFixture' function --- src/storages/hydrate.test.ts | 102 ++++++++++++++--------------------- 1 file changed, 39 insertions(+), 63 deletions(-) diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index 1664a6dd..b55cd2b8 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -5,129 +5,105 @@ import { MockRelay } from '@nostrify/nostrify/test'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { eventFixture } from '@/test.ts'; -import event0madePost from '~/fixtures/events/event-0-the-one-who-post-and-users-repost.json' with { type: 'json' }; -import event0madeRepost from '~/fixtures/events/event-0-the-one-who-repost.json' with { type: 'json' }; -import event0madeQuoteRepost from '~/fixtures/events/event-0-the-one-who-quote-repost.json' with { type: 'json' }; -import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; -import event1quoteRepost from '~/fixtures/events/event-1-quote-repost.json' with { type: 'json' }; -import event1futureIsMine from '~/fixtures/events/event-1-will-be-reposted-with-quote-repost.json' with { - type: 'json', -}; -import event1quoteRepostLatin from '~/fixtures/events/event-1-quote-repost-will-be-reposted.json' with { type: 'json' }; -import event1willBeQuoteReposted from '~/fixtures/events/event-1-that-will-be-quote-reposted.json' with { - type: 'json', -}; -import event1reposted from '~/fixtures/events/event-1-reposted.json' with { type: 'json' }; -import event6 from '~/fixtures/events/event-6.json' with { type: 'json' }; -import event6ofQuoteRepost from '~/fixtures/events/event-6-of-quote-repost.json' with { type: 'json' }; - Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => { const db = new MockRelay(); const event0 = await eventFixture('event-0'); - const event1copy = structuredClone(event1); + const event1 = await eventFixture('event-1'); // Save events to database await db.event(event0); - await db.event(event1copy); - - assertEquals((event1copy as DittoEvent).author, undefined, "Event hasn't been hydrated yet"); + await db.event(event1); await hydrateEvents({ - events: [event1copy], + events: [event1], storage: db, }); - const expectedEvent = { ...event1copy, author: event0 }; - assertEquals(event1copy, expectedEvent); + const expectedEvent = { ...event1, author: event0 }; + assertEquals(event1, expectedEvent); }); Deno.test('hydrateEvents(): repost --- WITHOUT stats', async () => { const db = new MockRelay(); - const event0madePostCopy = structuredClone(event0madePost); - const event0madeRepostCopy = structuredClone(event0madeRepost); - const event1repostedCopy = structuredClone(event1reposted); - const event6copy = structuredClone(event6); + const event0madePost = await eventFixture('event-0-the-one-who-post-and-users-repost'); + const event0madeRepost = await eventFixture('event-0-the-one-who-repost'); + const event1reposted = await eventFixture('event-1-reposted'); + const event6 = await eventFixture('event-6'); // Save events to database - await db.event(event0madePostCopy); - await db.event(event0madeRepostCopy); - await db.event(event1repostedCopy); - await db.event(event6copy); - - assertEquals((event6copy as DittoEvent).author, undefined, "Event hasn't hydrated author yet"); - assertEquals((event6copy as DittoEvent).repost, undefined, "Event hasn't hydrated repost yet"); + await db.event(event0madePost); + await db.event(event0madeRepost); + await db.event(event1reposted); + await db.event(event6); await hydrateEvents({ - events: [event6copy], + events: [event6], storage: db, }); const expectedEvent6 = { - ...event6copy, - author: event0madeRepostCopy, - repost: { ...event1repostedCopy, author: event0madePostCopy }, + ...event6, + author: event0madeRepost, + repost: { ...event1reposted, author: event0madePost }, }; - assertEquals(event6copy, expectedEvent6); + assertEquals(event6, expectedEvent6); }); Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => { const db = new MockRelay(); - const event0madeQuoteRepostCopy = structuredClone(event0madeQuoteRepost); + const event0madeQuoteRepost = await eventFixture('event-0-the-one-who-quote-repost'); const event0 = await eventFixture('event-0'); - const event1quoteRepostCopy = structuredClone(event1quoteRepost); - const event1willBeQuoteRepostedCopy = structuredClone(event1willBeQuoteReposted); + const event1quoteRepost = await eventFixture('event-1-quote-repost'); + const event1willBeQuoteReposted = await eventFixture('event-1-that-will-be-quote-reposted'); // Save events to database - await db.event(event0madeQuoteRepostCopy); + await db.event(event0madeQuoteRepost); await db.event(event0); - await db.event(event1quoteRepostCopy); - await db.event(event1willBeQuoteRepostedCopy); + await db.event(event1quoteRepost); + await db.event(event1willBeQuoteReposted); await hydrateEvents({ - events: [event1quoteRepostCopy], + events: [event1quoteRepost], storage: db, }); const expectedEvent1quoteRepost = { - ...event1quoteRepostCopy, - author: event0madeQuoteRepostCopy, - quote: { ...event1willBeQuoteRepostedCopy, author: event0 }, + ...event1quoteRepost, + author: event0madeQuoteRepost, + quote: { ...event1willBeQuoteReposted, author: event0 }, }; - assertEquals(event1quoteRepostCopy, expectedEvent1quoteRepost); + assertEquals(event1quoteRepost, expectedEvent1quoteRepost); }); Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () => { const db = new MockRelay(); const author = await eventFixture('event-0-makes-repost-with-quote-repost'); - const event1copy = structuredClone(event1futureIsMine); - const event1quoteCopy = structuredClone(event1quoteRepostLatin); - const event6copy = structuredClone(event6ofQuoteRepost); + const event1 = await eventFixture('event-1-will-be-reposted-with-quote-repost'); + const event6 = await eventFixture('event-6-of-quote-repost'); + const event1quote = await eventFixture('event-1-quote-repost-will-be-reposted'); // Save events to database await db.event(author); - await db.event(event1copy); - await db.event(event1quoteCopy); - await db.event(event6copy); - - assertEquals((event6copy as DittoEvent).author, undefined, "Event hasn't hydrated author yet"); - assertEquals((event6copy as DittoEvent).repost, undefined, "Event hasn't hydrated repost yet"); + await db.event(event1); + await db.event(event1quote); + await db.event(event6); await hydrateEvents({ - events: [event6copy], + events: [event6], storage: db, }); const expectedEvent6 = { - ...event6copy, + ...event6, author, - repost: { ...event1quoteCopy, author, quote: { author, ...event1copy } }, + repost: { ...event1quote, author, quote: { author, ...event1 } }, }; - assertEquals(event6copy, expectedEvent6); + assertEquals(event6, expectedEvent6); }); Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stats', async () => { From ee7864da8c191a626e5b579aa6bf2ef86d9576cc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:30:45 -0500 Subject: [PATCH 145/252] Add a signerMiddleware --- src/app.ts | 17 ++++++++++++++--- src/middleware/signerMiddleware.ts | 13 +++++++++++++ src/signers/APISigner.ts | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/middleware/signerMiddleware.ts diff --git a/src/app.ts b/src/app.ts index 05a1c81e..4f936085 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NStore } from '@nostrify/nostrify'; +import { NostrEvent, NostrSigner, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; @@ -29,6 +29,7 @@ import { adminAccountAction, adminAccountsController } from '@/controllers/api/a import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts'; import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; +import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { instanceController } from '@/controllers/api/instance.ts'; import { markersController, updateMarkersController } from '@/controllers/api/markers.ts'; @@ -84,13 +85,15 @@ import { auth19, requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; -import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; +import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/store.ts'; import { blockController } from '@/controllers/api/accounts.ts'; import { unblockController } from '@/controllers/api/accounts.ts'; interface AppEnv extends HonoEnv { Variables: { + /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ + signer?: NostrSigner; /** Hex pubkey for the current user. If provided, the user is considered "logged in." */ pubkey?: string; /** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */ @@ -123,7 +126,15 @@ app.get('/api/v1/streaming', streamingController); app.get('/api/v1/streaming/', streamingController); app.get('/relay', relayController); -app.use('*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), auth19, auth98(), storeMiddleware); +app.use( + '*', + csp(), + cors({ origin: '*', exposeHeaders: ['link'] }), + auth19, + auth98(), + storeMiddleware, + signerMiddleware, +); app.get('/.well-known/webfinger', webfingerController); app.get('/.well-known/host-meta', hostMetaController); diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts new file mode 100644 index 00000000..8e7eb7ac --- /dev/null +++ b/src/middleware/signerMiddleware.ts @@ -0,0 +1,13 @@ +import { AppMiddleware } from '@/app.ts'; +import { APISigner } from '@/signers/APISigner.ts'; + +/** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */ +export const signerMiddleware: AppMiddleware = async (c, next) => { + try { + c.set('signer', new APISigner(c)); + } catch { + // do nothing + } + + await next(); +}; diff --git a/src/signers/APISigner.ts b/src/signers/APISigner.ts index e9914b19..0a9317b9 100644 --- a/src/signers/APISigner.ts +++ b/src/signers/APISigner.ts @@ -2,6 +2,7 @@ import { NConnectSigner, NostrEvent, NostrSigner, NSecSigner } from '@nostrify/nostrify'; import { HTTPException } from 'hono'; + import { type AppContext } from '@/app.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; From 5a2b1b7de7aec546abed2c1f8fd02e4dabcbe4df Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:40:20 -0500 Subject: [PATCH 146/252] Destroy everything --- src/app.ts | 7 +--- src/middleware/auth19.ts | 49 ---------------------- src/middleware/signerMiddleware.ts | 41 ++++++++++++++++--- src/signers/APISigner.ts | 66 ------------------------------ 4 files changed, 37 insertions(+), 126 deletions(-) delete mode 100644 src/middleware/auth19.ts delete mode 100644 src/signers/APISigner.ts diff --git a/src/app.ts b/src/app.ts index 4f936085..057bded4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,7 +81,7 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { auth19, requirePubkey } from '@/middleware/auth19.ts'; +import { requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; @@ -94,10 +94,6 @@ interface AppEnv extends HonoEnv { Variables: { /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ signer?: NostrSigner; - /** Hex pubkey for the current user. If provided, the user is considered "logged in." */ - pubkey?: string; - /** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */ - seckey?: Uint8Array; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; /** User associated with the pubkey, if any. */ @@ -130,7 +126,6 @@ app.use( '*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), - auth19, auth98(), storeMiddleware, signerMiddleware, diff --git a/src/middleware/auth19.ts b/src/middleware/auth19.ts deleted file mode 100644 index 90fc4444..00000000 --- a/src/middleware/auth19.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { HTTPException } from 'hono'; -import { getPublicKey, nip19 } from 'nostr-tools'; - -import { type AppMiddleware } from '@/app.ts'; - -/** We only accept "Bearer" type. */ -const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); - -/** NIP-19 auth middleware. */ -const auth19: AppMiddleware = async (c, next) => { - const authHeader = c.req.header('authorization'); - const match = authHeader?.match(BEARER_REGEX); - - if (match) { - const [_, bech32] = match; - - try { - const decoded = nip19.decode(bech32!); - - switch (decoded.type) { - case 'npub': - c.set('pubkey', decoded.data); - break; - case 'nprofile': - c.set('pubkey', decoded.data.pubkey); - break; - case 'nsec': - c.set('pubkey', getPublicKey(decoded.data)); - c.set('seckey', decoded.data); - break; - } - } catch (_e) { - // - } - } - - await next(); -}; - -/** Throw a 401 if the pubkey isn't set. */ -const requirePubkey: AppMiddleware = async (c, next) => { - if (!c.get('pubkey')) { - throw new HTTPException(401, { message: 'No pubkey provided' }); - } - - await next(); -}; - -export { auth19, requirePubkey }; diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 8e7eb7ac..d0563919 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,12 +1,43 @@ +import { NConnectSigner, NSecSigner } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; + import { AppMiddleware } from '@/app.ts'; -import { APISigner } from '@/signers/APISigner.ts'; +import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { Storages } from '@/storages.ts'; + +/** We only accept "Bearer" type. */ +const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); /** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */ export const signerMiddleware: AppMiddleware = async (c, next) => { - try { - c.set('signer', new APISigner(c)); - } catch { - // do nothing + const header = c.req.header('authorization'); + const match = header?.match(BEARER_REGEX); + + if (match) { + const [_, bech32] = match; + + try { + const decoded = nip19.decode(bech32!); + + switch (decoded.type) { + case 'npub': + c.set( + 'signer', + new NConnectSigner({ + pubkey: decoded.data, + relay: Storages.pubsub, + signer: new AdminSigner(), + timeout: 60000, + }), + ); + break; + case 'nsec': + c.set('signer', new NSecSigner(decoded.data)); + break; + } + } catch { + // the user is not logged in + } } await next(); diff --git a/src/signers/APISigner.ts b/src/signers/APISigner.ts deleted file mode 100644 index 0a9317b9..00000000 --- a/src/signers/APISigner.ts +++ /dev/null @@ -1,66 +0,0 @@ -// deno-lint-ignore-file require-await - -import { NConnectSigner, NostrEvent, NostrSigner, NSecSigner } from '@nostrify/nostrify'; -import { HTTPException } from 'hono'; - -import { type AppContext } from '@/app.ts'; -import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { Storages } from '@/storages.ts'; - -/** - * Sign Nostr event using the app context. - * - * - If a secret key is provided, it will be used to sign the event. - * - Otherwise, it will use NIP-46 to sign the event. - */ -export class APISigner implements NostrSigner { - private signer: NostrSigner; - - constructor(c: AppContext) { - const seckey = c.get('seckey'); - const pubkey = c.get('pubkey'); - - if (!pubkey) { - throw new HTTPException(401, { message: 'Missing pubkey' }); - } - - if (seckey) { - this.signer = new NSecSigner(seckey); - } else { - this.signer = new NConnectSigner({ - pubkey, - relay: Storages.pubsub, - signer: new AdminSigner(), - timeout: 60000, - }); - } - } - - async getPublicKey(): Promise { - return this.signer.getPublicKey(); - } - - async signEvent(event: Omit): Promise { - return this.signer.signEvent(event); - } - - readonly nip04 = { - encrypt: async (pubkey: string, plaintext: string): Promise => { - return this.signer.nip04!.encrypt(pubkey, plaintext); - }, - - decrypt: async (pubkey: string, ciphertext: string): Promise => { - return this.signer.nip04!.decrypt(pubkey, ciphertext); - }, - }; - - readonly nip44 = { - encrypt: async (pubkey: string, plaintext: string): Promise => { - return this.signer.nip44!.encrypt(pubkey, plaintext); - }, - - decrypt: async (pubkey: string, ciphertext: string): Promise => { - return this.signer.nip44!.decrypt(pubkey, ciphertext); - }, - }; -} From c5fbe69b80c2de965614e404742da8b96a1e6836 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:42:53 -0500 Subject: [PATCH 147/252] requirePubkey -> requireSigner --- src/app.ts | 62 ++++++++++++++++----------------- src/middleware/requireSigner.ts | 12 +++++++ 2 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 src/middleware/requireSigner.ts diff --git a/src/app.ts b/src/app.ts index 057bded4..5ef50560 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,10 +81,10 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; +import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/store.ts'; import { blockController } from '@/controllers/api/accounts.ts'; @@ -151,17 +151,17 @@ app.post('/oauth/authorize', oauthAuthorizeController); app.get('/oauth/authorize', oauthController); app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController); -app.get('/api/v1/accounts/verify_credentials', requirePubkey, verifyCredentialsController); -app.patch('/api/v1/accounts/update_credentials', requirePubkey, updateCredentialsController); +app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController); +app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController); app.get('/api/v1/accounts/search', accountSearchController); app.get('/api/v1/accounts/lookup', accountLookupController); -app.get('/api/v1/accounts/relationships', requirePubkey, relationshipsController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requirePubkey, blockController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requirePubkey, unblockController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requirePubkey, muteController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requirePubkey, unmuteController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requirePubkey, followController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requirePubkey, unfollowController); +app.get('/api/v1/accounts/relationships', requireSigner, relationshipsController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requireSigner, blockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requireSigner, unblockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requireSigner, muteController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requireSigner, unmuteController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requireSigner, followController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requireSigner, unfollowController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/followers', followersController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController); @@ -171,21 +171,21 @@ app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/favourited_by', favouritedByControll app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/reblogged_by', rebloggedByController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/context', contextController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}', statusController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', requirePubkey, favouriteController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requirePubkey, bookmarkController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requirePubkey, unbookmarkController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requirePubkey, pinController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requirePubkey, unpinController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requirePubkey, zapController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requirePubkey, reblogStatusController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requirePubkey, unreblogStatusController); -app.post('/api/v1/statuses', requirePubkey, createStatusController); -app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requirePubkey, deleteStatusController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', requireSigner, favouriteController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requireSigner, bookmarkController); +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}}/zap', requireSigner, zapController); +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); +app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requireSigner, deleteStatusController); app.post('/api/v1/media', mediaController); app.post('/api/v2/media', mediaController); -app.get('/api/v1/timelines/home', requirePubkey, homeTimelineController); +app.get('/api/v1/timelines/home', requireSigner, homeTimelineController); app.get('/api/v1/timelines/public', publicTimelineController); app.get('/api/v1/timelines/tag/:hashtag', hashtagTimelineController); @@ -201,11 +201,11 @@ app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }) app.get('/api/v1/suggestions', suggestionsV1Controller); app.get('/api/v2/suggestions', suggestionsV2Controller); -app.get('/api/v1/notifications', requirePubkey, notificationsController); -app.get('/api/v1/favourites', requirePubkey, favouritesController); -app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); -app.get('/api/v1/blocks', requirePubkey, blocksController); -app.get('/api/v1/mutes', requirePubkey, mutesController); +app.get('/api/v1/notifications', requireSigner, notificationsController); +app.get('/api/v1/favourites', requireSigner, favouritesController); +app.get('/api/v1/bookmarks', requireSigner, bookmarksController); +app.get('/api/v1/blocks', requireSigner, blocksController); +app.get('/api/v1/mutes', requireSigner, mutesController); app.get('/api/v1/markers', requireProof(), markersController); app.post('/api/v1/markers', requireProof(), updateMarkersController); @@ -218,17 +218,17 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); -app.post('/api/v1/reports', requirePubkey, reportController); -app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); -app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requirePubkey, requireRole('admin'), adminReportController); +app.post('/api/v1/reports', requireSigner, reportController); +app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController); +app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requireSigner, requireRole('admin'), adminReportController); app.post( '/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve', - requirePubkey, + requireSigner, requireRole('admin'), adminReportResolveController, ); -app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requirePubkey, requireRole('admin'), adminAccountAction); +app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminAccountAction); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/middleware/requireSigner.ts b/src/middleware/requireSigner.ts new file mode 100644 index 00000000..6e337c24 --- /dev/null +++ b/src/middleware/requireSigner.ts @@ -0,0 +1,12 @@ +import { HTTPException } from 'hono'; + +import { AppMiddleware } from '@/app.ts'; + +/** Throw a 401 if a signer isn't set. */ +export const requireSigner: AppMiddleware = async (c, next) => { + if (!c.get('signer')) { + throw new HTTPException(401, { message: 'No pubkey provided' }); + } + + await next(); +}; From c715827c81a9e85d8b68de655b865b9665267f0e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:57:03 -0500 Subject: [PATCH 148/252] c.get('pubkey') -> await c.get('signer')?.getPublicKey() --- src/controllers/api/accounts.ts | 30 +++++++++++++++++---------- src/controllers/api/bookmarks.ts | 2 +- src/controllers/api/markers.ts | 4 ++-- src/controllers/api/media.ts | 2 +- src/controllers/api/mutes.ts | 2 +- src/controllers/api/notifications.ts | 6 +++--- src/controllers/api/reports.ts | 12 ++++++++--- src/controllers/api/search.ts | 3 ++- src/controllers/api/statuses.ts | 31 ++++++++++++++++------------ src/controllers/api/timelines.ts | 8 ++++--- src/middleware/auth98.ts | 12 ++++++++--- src/middleware/store.ts | 2 +- src/utils/api.ts | 9 ++++++-- src/views.ts | 4 +++- 14 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 68edfbc9..88d19b10 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -29,7 +29,7 @@ const createAccountSchema = z.object({ }); const createAccountController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const result = createAccountSchema.safeParse(await c.req.json()); if (!result.success) { @@ -45,7 +45,7 @@ const createAccountController: AppController = async (c) => { }; const verifyCredentialsController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const event = await getAuthor(pubkey, { relations: ['author_stats'] }); if (event) { @@ -122,7 +122,7 @@ const accountSearchController: AppController = async (c) => { }; const relationshipsController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const ids = z.array(z.string()).safeParse(c.req.queries('id[]')); if (!ids.success) { @@ -178,7 +178,11 @@ const accountStatusesController: AppController = async (c) => { return events; }); - const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + + const statuses = await Promise.all( + events.map((event) => renderStatus(event, { viewerPubkey })), + ); return paginated(c, events, statuses); }; @@ -194,7 +198,7 @@ const updateCredentialsSchema = z.object({ }); const updateCredentialsController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const body = await parseBody(c.req.raw); const result = updateCredentialsSchema.safeParse(body); @@ -236,7 +240,7 @@ const updateCredentialsController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/accounts/#follow */ const followController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -253,7 +257,7 @@ const followController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/accounts/#unfollow */ const unfollowController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -290,7 +294,7 @@ const unblockController: AppController = (c) => { /** https://docs.joinmastodon.org/methods/accounts/#mute */ const muteController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -305,7 +309,7 @@ const muteController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/accounts/#unmute */ const unmuteController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -319,7 +323,7 @@ const unmuteController: AppController = async (c) => { }; const favouritesController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const params = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; @@ -335,7 +339,11 @@ const favouritesController: AppController = async (c) => { const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal }) .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); - const statuses = await Promise.all(events1.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + + const statuses = await Promise.all( + events1.map((event) => renderStatus(event, { viewerPubkey })), + ); return paginated(c, events1, statuses); }; diff --git a/src/controllers/api/bookmarks.ts b/src/controllers/api/bookmarks.ts index 8d44f953..1616fa2d 100644 --- a/src/controllers/api/bookmarks.ts +++ b/src/controllers/api/bookmarks.ts @@ -5,7 +5,7 @@ import { renderStatuses } from '@/views.ts'; /** https://docs.joinmastodon.org/methods/bookmarks/#get */ const bookmarksController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; const [event10003] = await Storages.db.query( diff --git a/src/controllers/api/markers.ts b/src/controllers/api/markers.ts index ce1c4ec3..005ebbe5 100644 --- a/src/controllers/api/markers.ts +++ b/src/controllers/api/markers.ts @@ -14,7 +14,7 @@ interface Marker { } export const markersController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const timelines = c.req.queries('timeline[]') ?? []; const results = await kv.getMany( @@ -37,7 +37,7 @@ const markerDataSchema = z.object({ }); export const updateMarkersController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw)); const timelines = Object.keys(record) as Timeline[]; diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index dd36a532..33b79810 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -14,7 +14,7 @@ const mediaBodySchema = z.object({ }); const mediaController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const result = mediaBodySchema.safeParse(await parseBody(c.req.raw)); const { signal } = c.req.raw; diff --git a/src/controllers/api/mutes.ts b/src/controllers/api/mutes.ts index 77b60e32..fe048e98 100644 --- a/src/controllers/api/mutes.ts +++ b/src/controllers/api/mutes.ts @@ -5,7 +5,7 @@ import { renderAccounts } from '@/views.ts'; /** https://docs.joinmastodon.org/methods/mutes/#get */ const mutesController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; const [event10000] = await Storages.db.query( diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index b2fa15ea..857f2a32 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -5,8 +5,8 @@ import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; -const notificationsController: AppController = (c) => { - const pubkey = c.get('pubkey')!; +const notificationsController: AppController = async (c) => { + const pubkey = await c.get('signer')?.getPublicKey()!; const { since, until } = paginationSchema.parse(c.req.query()); return renderNotifications(c, [{ kinds: [1, 6, 7], '#p': [pubkey], since, until }]); @@ -14,7 +14,7 @@ const notificationsController: AppController = (c) => { async function renderNotifications(c: AppContext, filters: NostrFilter[]) { const store = c.get('store'); - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; const events = await store diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index eb85bd28..55fb6019 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -55,9 +55,15 @@ const reportController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/admin/reports/#get */ const adminReportsController: AppController = async (c) => { const store = c.get('store'); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]) .then((events) => hydrateEvents({ storage: store, events: events, signal: c.req.raw.signal })) - .then((events) => Promise.all(events.map((event) => renderAdminReport(event, { viewerPubkey: c.get('pubkey') })))); + .then((events) => + Promise.all( + events.map((event) => renderAdminReport(event, { viewerPubkey })), + ) + ); return c.json(reports); }; @@ -67,7 +73,7 @@ const adminReportController: AppController = async (c) => { const eventId = c.req.param('id'); const { signal } = c.req.raw; const store = c.get('store'); - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const [event] = await store.query([{ kinds: [1984], @@ -89,7 +95,7 @@ const adminReportResolveController: AppController = async (c) => { const eventId = c.req.param('id'); const { signal } = c.req.raw; const store = c.get('store'); - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const [event] = await store.query([{ kinds: [1984], diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index e674d0e9..fe08ace1 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -43,6 +43,7 @@ const searchController: AppController = async (c) => { } const results = dedupeEvents(events); + const viewerPubkey = await c.get('signer')?.getPublicKey(); const [accounts, statuses] = await Promise.all([ Promise.all( @@ -54,7 +55,7 @@ const searchController: AppController = async (c) => { Promise.all( results .filter((event) => event.kind === 1) - .map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })) + .map((event) => renderStatus(event, { viewerPubkey })) .filter(Boolean), ), ]); diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 3420383a..1138c0a3 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -47,7 +47,7 @@ const statusController: AppController = async (c) => { }); if (event) { - return c.json(await renderStatus(event, { viewerPubkey: c.get('pubkey') })); + return c.json(await renderStatus(event, { viewerPubkey: await c.get('signer')?.getPublicKey() })); } return c.json({ error: 'Event not found.' }, 404); @@ -89,9 +89,11 @@ const createStatusController: AppController = async (c) => { tags.push(['subject', data.spoiler_text]); } + const viewerPubkey = await c.get('signer')?.getPublicKey(); + if (data.media_ids?.length) { const media = await getUnattachedMediaByIds(data.media_ids) - .then((media) => media.filter(({ pubkey }) => pubkey === c.get('pubkey'))) + .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) .then((media) => media.map(({ url, data }) => ['media', url, data])); tags.push(...media); @@ -143,12 +145,12 @@ const createStatusController: AppController = async (c) => { }); } - return c.json(await renderStatus({ ...event, author }, { viewerPubkey: c.get('pubkey') })); + return c.json(await renderStatus({ ...event, author }, { viewerPubkey: await c.get('signer')?.getPublicKey() })); }; const deleteStatusController: AppController = async (c) => { const id = c.req.param('id'); - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const event = await getEvent(id, { signal: c.req.raw.signal }); @@ -172,9 +174,12 @@ const deleteStatusController: AppController = async (c) => { const contextController: AppController = async (c) => { const id = c.req.param('id'); const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] }); + const viewerPubkey = await c.get('signer')?.getPublicKey(); async function renderStatuses(events: NostrEvent[]) { - const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); + const statuses = await Promise.all( + events.map((event) => renderStatus(event, { viewerPubkey })), + ); return statuses.filter(Boolean); } @@ -204,7 +209,7 @@ const favouriteController: AppController = async (c) => { ], }, c); - const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') }); + const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() }); if (status) { status.favourited = true; @@ -247,7 +252,7 @@ const reblogStatusController: AppController = async (c) => { signal: signal, }); - const status = await renderReblog(reblogEvent, { viewerPubkey: c.get('pubkey') }); + const status = await renderReblog(reblogEvent, { viewerPubkey: await c.get('signer')?.getPublicKey() }); return c.json(status); }; @@ -255,7 +260,7 @@ const reblogStatusController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unreblog */ const unreblogStatusController: AppController = async (c) => { const eventId = c.req.param('id'); - const pubkey = c.get('pubkey') as string; + const pubkey = await c.get('signer')?.getPublicKey() as string; const event = await getEvent(eventId, { kind: 1, @@ -282,7 +287,7 @@ const rebloggedByController: AppController = (c) => { /** https://docs.joinmastodon.org/methods/statuses/#bookmark */ const bookmarkController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const event = await getEvent(eventId, { @@ -309,7 +314,7 @@ const bookmarkController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unbookmark */ const unbookmarkController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const event = await getEvent(eventId, { @@ -336,7 +341,7 @@ const unbookmarkController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#pin */ const pinController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const event = await getEvent(eventId, { @@ -363,7 +368,7 @@ const pinController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unpin */ const unpinController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const { signal } = c.req.raw; @@ -423,7 +428,7 @@ const zapController: AppController = async (c) => { ], }, c); - const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') }); + const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() }); status.zapped = true; return c.json(status); diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 27459a82..0880d84f 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -11,7 +11,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const homeTimelineController: AppController = async (c) => { const params = paginationSchema.parse(c.req.query()); - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const authors = await getFeedPubkeys(pubkey); return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]); }; @@ -61,11 +61,13 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { return c.json([]); } + const viewerPubkey = await c.get('signer')?.getPublicKey(); + const statuses = (await Promise.all(events.map((event) => { if (event.kind === 6) { - return renderReblog(event, { viewerPubkey: c.get('pubkey') }); + return renderReblog(event, { viewerPubkey }); } - return renderStatus(event, { viewerPubkey: c.get('pubkey') }); + return renderStatus(event, { viewerPubkey }); }))).filter((boolean) => boolean); if (!statuses.length) { diff --git a/src/middleware/auth98.ts b/src/middleware/auth98.ts index db025ae5..d761b95d 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98.ts @@ -8,7 +8,6 @@ import { validateAuthEvent, } from '@/utils/nip98.ts'; import { localRequest } from '@/utils/api.ts'; -import { APISigner } from '@/signers/APISigner.ts'; import { findUser, User } from '@/db/users.ts'; /** @@ -70,7 +69,7 @@ function withProof( opts?: ParseAuthRequestOpts, ): AppMiddleware { return async (c, next) => { - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const proof = c.get('proof') || await obtainProof(c, opts); // Prevent people from accidentally using the wrong account. This has no other security implications. @@ -90,9 +89,16 @@ function withProof( /** Get the proof over Nostr Connect. */ async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) { + const signer = c.get('signer'); + if (!signer) { + throw new HTTPException(401, { + res: c.json({ error: 'No way to sign Nostr event' }, 401), + }); + } + const req = localRequest(c); const reqEvent = await buildAuthEventTemplate(req, opts); - const resEvent = await new APISigner(c).signEvent(reqEvent); + const resEvent = await signer.signEvent(reqEvent); const result = await validateAuthEvent(req, resEvent, opts); if (result.success) { diff --git a/src/middleware/store.ts b/src/middleware/store.ts index 60055b3f..40b7c599 100644 --- a/src/middleware/store.ts +++ b/src/middleware/store.ts @@ -4,7 +4,7 @@ import { Storages } from '@/storages.ts'; /** Store middleware. */ const storeMiddleware: AppMiddleware = async (c, next) => { - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); if (pubkey) { const store = new UserStore(pubkey, Storages.admin); diff --git a/src/utils/api.ts b/src/utils/api.ts index cba7c663..3dfdfa34 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -10,7 +10,6 @@ import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { APISigner } from '@/signers/APISigner.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; @@ -21,7 +20,13 @@ type EventStub = TypeFest.SetOptional { - const signer = new APISigner(c); + const signer = c.get('signer'); + + if (!signer) { + throw new HTTPException(401, { + res: c.json({ error: 'No way to sign Nostr event' }, 401), + }); + } const event = await signer.signEvent({ content: '', diff --git a/src/views.ts b/src/views.ts index 9c31dfcb..451ce140 100644 --- a/src/views.ts +++ b/src/views.ts @@ -59,8 +59,10 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal const sortedEvents = [...events].sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + const statuses = await Promise.all( - sortedEvents.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })), + sortedEvents.map((event) => renderStatus(event, { viewerPubkey })), ); // TODO: pagination with min_id and max_id based on the order of `ids`. From 1accae2222a5a7c7571e4e72c8d0fce93f576b97 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:04:31 -0500 Subject: [PATCH 149/252] Add a ConnectSigner to wrap our default opts to NConnectSigner, add c.set('signer') calls to nip98 middleware --- src/app.ts | 2 +- src/middleware/auth98.ts | 10 ++++++---- src/middleware/signerMiddleware.ts | 15 +++------------ src/signers/ConnectSigner.ts | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 src/signers/ConnectSigner.ts diff --git a/src/app.ts b/src/app.ts index 5ef50560..62da64c3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -126,9 +126,9 @@ app.use( '*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), + signerMiddleware, auth98(), storeMiddleware, - signerMiddleware, ); app.get('/.well-known/webfinger', webfingerController); diff --git a/src/middleware/auth98.ts b/src/middleware/auth98.ts index d761b95d..fbadb2cc 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98.ts @@ -1,14 +1,16 @@ import { NostrEvent } from '@nostrify/nostrify'; import { HTTPException } from 'hono'; + import { type AppContext, type AppMiddleware } from '@/app.ts'; +import { findUser, User } from '@/db/users.ts'; +import { ConnectSigner } from '@/signers/ConnectSigner.ts'; +import { localRequest } from '@/utils/api.ts'; import { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent, } from '@/utils/nip98.ts'; -import { localRequest } from '@/utils/api.ts'; -import { findUser, User } from '@/db/users.ts'; /** * NIP-98 auth. @@ -20,7 +22,7 @@ function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware { const result = await parseAuthRequest(req, opts); if (result.success) { - c.set('pubkey', result.data.pubkey); + c.set('signer', new ConnectSigner(result.data.pubkey)); c.set('proof', result.data); } @@ -78,7 +80,7 @@ function withProof( } if (proof) { - c.set('pubkey', proof.pubkey); + c.set('signer', new ConnectSigner(proof.pubkey)); c.set('proof', proof); await handler(c, proof, next); } else { diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index d0563919..85a2ff12 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,9 +1,8 @@ -import { NConnectSigner, NSecSigner } from '@nostrify/nostrify'; +import { NSecSigner } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { AppMiddleware } from '@/app.ts'; -import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { Storages } from '@/storages.ts'; +import { ConnectSigner } from '@/signers/ConnectSigner.ts'; /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); @@ -21,15 +20,7 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { switch (decoded.type) { case 'npub': - c.set( - 'signer', - new NConnectSigner({ - pubkey: decoded.data, - relay: Storages.pubsub, - signer: new AdminSigner(), - timeout: 60000, - }), - ); + c.set('signer', new ConnectSigner(decoded.data)); break; case 'nsec': c.set('signer', new NSecSigner(decoded.data)); diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts new file mode 100644 index 00000000..b98a59f5 --- /dev/null +++ b/src/signers/ConnectSigner.ts @@ -0,0 +1,20 @@ +import { NConnectSigner } from '@nostrify/nostrify'; + +import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { Storages } from '@/storages.ts'; + +/** + * NIP-46 signer. + * + * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. + */ +export class ConnectSigner extends NConnectSigner { + constructor(pubkey: string) { + super({ + pubkey, + relay: Storages.pubsub, + signer: new AdminSigner(), + timeout: 60000, + }); + } +} From 084143c5c8d9fd8a61c90cc3319005a6d8a3841c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:07:54 -0500 Subject: [PATCH 150/252] Rename all middleware to thingMiddleware --- src/app.ts | 22 +++++++++++-------- .../{auth98.ts => auth98Middleware.ts} | 4 ++-- .../{cache.ts => cacheMiddleware.ts} | 2 +- src/middleware/{csp.ts => cspMiddleware.ts} | 4 +--- .../{store.ts => storeMiddleware.ts} | 4 +--- 5 files changed, 18 insertions(+), 18 deletions(-) rename src/middleware/{auth98.ts => auth98Middleware.ts} (95%) rename src/middleware/{cache.ts => cacheMiddleware.ts} (95%) rename src/middleware/{csp.ts => cspMiddleware.ts} (93%) rename src/middleware/{store.ts => storeMiddleware.ts} (81%) diff --git a/src/app.ts b/src/app.ts index 62da64c3..8c8cbc05 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,12 +81,12 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; -import { cache } from '@/middleware/cache.ts'; -import { csp } from '@/middleware/csp.ts'; +import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; +import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts'; +import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; -import { storeMiddleware } from '@/middleware/store.ts'; +import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { blockController } from '@/controllers/api/accounts.ts'; import { unblockController } from '@/controllers/api/accounts.ts'; @@ -124,10 +124,10 @@ app.get('/relay', relayController); app.use( '*', - csp(), + cspMiddleware(), cors({ origin: '*', exposeHeaders: ['link'] }), signerMiddleware, - auth98(), + auth98Middleware(), storeMiddleware, ); @@ -140,7 +140,7 @@ app.get('/users/:username', actorController); app.get('/nodeinfo/:version', nodeInfoSchemaController); -app.get('/api/v1/instance', cache({ cacheName: 'web', expires: Time.minutes(5) }), instanceController); +app.get('/api/v1/instance', cacheMiddleware({ cacheName: 'web', expires: Time.minutes(5) }), instanceController); app.get('/api/v1/apps/verify_credentials', appCredentialsController); app.post('/api/v1/apps', createAppController); @@ -195,8 +195,12 @@ app.get('/api/v2/search', searchController); app.get('/api/pleroma/frontend_configurations', frontendConfigController); -app.get('/api/v1/trends/tags', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); -app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); +app.get( + '/api/v1/trends/tags', + cacheMiddleware({ cacheName: 'web', expires: Time.minutes(15) }), + trendingTagsController, +); +app.get('/api/v1/trends', cacheMiddleware({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); app.get('/api/v1/suggestions', suggestionsV1Controller); app.get('/api/v2/suggestions', suggestionsV2Controller); diff --git a/src/middleware/auth98.ts b/src/middleware/auth98Middleware.ts similarity index 95% rename from src/middleware/auth98.ts rename to src/middleware/auth98Middleware.ts index fbadb2cc..7cd7059d 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98Middleware.ts @@ -16,7 +16,7 @@ import { * NIP-98 auth. * https://github.com/nostr-protocol/nips/blob/master/98.md */ -function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware { +function auth98Middleware(opts: ParseAuthRequestOpts = {}): AppMiddleware { return async (c, next) => { const req = localRequest(c); const result = await parseAuthRequest(req, opts); @@ -108,4 +108,4 @@ async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) { } } -export { auth98, requireProof, requireRole }; +export { auth98Middleware, requireProof, requireRole }; diff --git a/src/middleware/cache.ts b/src/middleware/cacheMiddleware.ts similarity index 95% rename from src/middleware/cache.ts rename to src/middleware/cacheMiddleware.ts index 181623f6..baa4976d 100644 --- a/src/middleware/cache.ts +++ b/src/middleware/cacheMiddleware.ts @@ -5,7 +5,7 @@ import ExpiringCache from '@/utils/expiring-cache.ts'; const debug = Debug('ditto:middleware:cache'); -export const cache = (options: { +export const cacheMiddleware = (options: { cacheName: string; expires?: number; }): MiddlewareHandler => { diff --git a/src/middleware/csp.ts b/src/middleware/cspMiddleware.ts similarity index 93% rename from src/middleware/csp.ts rename to src/middleware/cspMiddleware.ts index fdce5c75..00c4ecc3 100644 --- a/src/middleware/csp.ts +++ b/src/middleware/cspMiddleware.ts @@ -1,7 +1,7 @@ import { AppMiddleware } from '@/app.ts'; import { Conf } from '@/config.ts'; -const csp = (): AppMiddleware => { +export const cspMiddleware = (): AppMiddleware => { return async (c, next) => { const { host, protocol, origin } = Conf.url; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; @@ -26,5 +26,3 @@ const csp = (): AppMiddleware => { await next(); }; }; - -export { csp }; diff --git a/src/middleware/store.ts b/src/middleware/storeMiddleware.ts similarity index 81% rename from src/middleware/store.ts rename to src/middleware/storeMiddleware.ts index 40b7c599..efb65ed8 100644 --- a/src/middleware/store.ts +++ b/src/middleware/storeMiddleware.ts @@ -3,7 +3,7 @@ import { UserStore } from '@/storages/UserStore.ts'; import { Storages } from '@/storages.ts'; /** Store middleware. */ -const storeMiddleware: AppMiddleware = async (c, next) => { +export const storeMiddleware: AppMiddleware = async (c, next) => { const pubkey = await c.get('signer')?.getPublicKey(); if (pubkey) { @@ -14,5 +14,3 @@ const storeMiddleware: AppMiddleware = async (c, next) => { } await next(); }; - -export { storeMiddleware }; From 03182f8a5a260447173c0e40d9f6c388002ffabd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:14:27 -0500 Subject: [PATCH 151/252] ConnectSigner: implement getRelays, support nprofile auth again --- src/middleware/signerMiddleware.ts | 3 +++ src/signers/ConnectSigner.ts | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 85a2ff12..8779937e 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -22,6 +22,9 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { case 'npub': c.set('signer', new ConnectSigner(decoded.data)); break; + case 'nprofile': + c.set('signer', new ConnectSigner(decoded.data.pubkey, decoded.data.relays)); + break; case 'nsec': c.set('signer', new NSecSigner(decoded.data)); break; diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index b98a59f5..e7d61a85 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -9,12 +9,22 @@ import { Storages } from '@/storages.ts'; * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. */ export class ConnectSigner extends NConnectSigner { - constructor(pubkey: string) { + constructor(pubkey: string, private relays?: string[]) { super({ pubkey, + // TODO: use a remote relay for `nprofile` signing, if present and Conf.relay isn't already in the list relay: Storages.pubsub, signer: new AdminSigner(), timeout: 60000, }); } + + /** Get the user's relays if they passed in an `nprofile` auth token. */ + // deno-lint-ignore require-await + async getRelays(): Promise> { + return this.relays?.reduce>((acc, relay) => { + acc[relay] = { read: true, write: true }; + return acc; + }, {}) ?? {}; + } } From cd2a35d951e6db472e9fd36b496de8748e340376 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:20:36 -0500 Subject: [PATCH 152/252] ConnectSigner: make getPublicKey used the stored value instead of actually hitting the relay --- src/signers/ConnectSigner.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index e7d61a85..4dda0b1d 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file require-await import { NConnectSigner } from '@nostrify/nostrify'; import { AdminSigner } from '@/signers/AdminSigner.ts'; @@ -9,18 +10,26 @@ import { Storages } from '@/storages.ts'; * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. */ export class ConnectSigner extends NConnectSigner { + private _pubkey: string; + constructor(pubkey: string, private relays?: string[]) { super({ pubkey, - // TODO: use a remote relay for `nprofile` signing, if present and Conf.relay isn't already in the list + // TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list) relay: Storages.pubsub, signer: new AdminSigner(), timeout: 60000, }); + + this._pubkey = pubkey; + } + + // Prevent unnecessary NIP-46 round-trips. + async getPublicKey(): Promise { + return this._pubkey; } /** Get the user's relays if they passed in an `nprofile` auth token. */ - // deno-lint-ignore require-await async getRelays(): Promise> { return this.relays?.reduce>((acc, relay) => { acc[relay] = { read: true, write: true }; From b626d75262cc74626f4644a949d598bb253c7d41 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 14 May 2024 14:22:37 -0300 Subject: [PATCH 153/252] fix(streaming): posts from blocked users does not show up in global tab --- src/controllers/api/streaming.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 8d22d5cc..fbe825be 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -9,6 +9,7 @@ import { bech32ToPubkey } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; +import { getTagSet } from '@/tags.ts'; const debug = Debug('ditto:streaming'); @@ -33,11 +34,12 @@ const streamSchema = z.enum([ type Stream = z.infer; -const streamingController: AppController = (c) => { +const streamingController: AppController = async (c) => { const upgrade = c.req.header('upgrade'); const token = c.req.header('sec-websocket-protocol'); const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream')); const controller = new AbortController(); + const signal = c.req.raw.signal; if (upgrade?.toLowerCase() !== 'websocket') { return c.text('Please use websocket protocol', 400); @@ -61,6 +63,16 @@ const streamingController: AppController = (c) => { } } + const mutedUsersSet = new Set(); + if (pubkey) { + const [mutedUsers] = await Storages.admin.query([{ authors: [pubkey], kinds: [10000], limit: 1 }], { signal }); + if (mutedUsers) { + for (const pubkey of getTagSet(mutedUsers.tags, 'p')) { + mutedUsersSet.add(pubkey); + } + } + } + socket.onopen = async () => { if (!stream) return; @@ -72,6 +84,10 @@ const streamingController: AppController = (c) => { if (msg[0] === 'EVENT') { const event = msg[2]; + if (mutedUsersSet.has(event.pubkey)) { + continue; + } + await hydrateEvents({ events: [event], storage: Storages.admin, From dd3c64ef849e3dfeb5e1cd4847e4a1c954369edb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 13:09:16 -0500 Subject: [PATCH 154/252] Remove accidentally checked in file --- ..env.swp | Bin 1024 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 ..env.swp diff --git a/..env.swp b/..env.swp deleted file mode 100644 index 2ab134254314be032359c6894717262e417e7684..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1024 zcmYc?$V<%2S1{8vVn6|1lPwq$b5bi%1aWW*@(XnHi*ZOI3G1cil_7CQnWG^v8Uh0x F0sw&A3CREe diff --git a/.gitignore b/.gitignore index d816f9d2..39dbfbbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env *.cpuprofile +*.swp deno-test.xml \ No newline at end of file From b5f0d2f0e60c23bb7025727a13ad59c409ce5212 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:12:43 -0500 Subject: [PATCH 155/252] docs/events: clean up User event kind --- docs/events.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/events.md b/docs/events.md index e850fcb8..1674239a 100644 --- a/docs/events.md +++ b/docs/events.md @@ -9,9 +9,7 @@ The Ditto server publishes kind `30361` events to represent users. These events User events have the following tags: - `d` - pubkey of the user. -- `name` - NIP-05 username granted to the user, without the domain. - `role` - one of `admin` or `user`. -- `origin` - the origin of the user's NIP-05, at the time the event was published. Example: @@ -25,7 +23,6 @@ Example: "tags": [ ["d", "79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6"], ["role", "user"], - ["origin", "https://ditto.ngrok.app"], ["alt", "User's account was updated by the admins of ditto.ngrok.app"] ], "sig": "fc12db77b1c8f8aa86c73b617f0cd4af1e6ba244239eaf3164a292de6d39363f32d6b817ffff796ace7a103d75e1d8e6a0fb7f618819b32d81a953b4a75d7507" @@ -40,4 +37,4 @@ The sections below describe the `content` field. Some are encrypted and some are ### `pub.ditto.pleroma.config` -NIP-04 encrypted JSON array of Pleroma ConfigDB objects. Pleroma admin API endpoints set this config, and Ditto reads from it. \ No newline at end of file +NIP-04 encrypted JSON array of Pleroma ConfigDB objects. Pleroma admin API endpoints set this config, and Ditto reads from it. From a061c248bd5d288a27305c3a502add4296d6d999 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:18:44 -0500 Subject: [PATCH 156/252] signerMiddleware: add debug log --- src/middleware/signerMiddleware.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 8779937e..1d357082 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,9 +1,12 @@ import { NSecSigner } from '@nostrify/nostrify'; +import { Stickynotes } from '@soapbox/stickynotes'; import { nip19 } from 'nostr-tools'; import { AppMiddleware } from '@/app.ts'; import { ConnectSigner } from '@/signers/ConnectSigner.ts'; +const console = new Stickynotes('ditto:signerMiddleware'); + /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); @@ -30,7 +33,7 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { break; } } catch { - // the user is not logged in + console.debug('The user is not logged in'); } } From 45b766af4d768853c6236eeeadfeb7aee16f47a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:24:48 -0500 Subject: [PATCH 157/252] Remove 'user' from AppContext --- src/app.ts | 2 -- src/middleware/auth98Middleware.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index 8c8cbc05..447499f8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -96,8 +96,6 @@ interface AppEnv extends HonoEnv { signer?: NostrSigner; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; - /** User associated with the pubkey, if any. */ - user?: User; /** Store */ store: NStore; }; diff --git a/src/middleware/auth98Middleware.ts b/src/middleware/auth98Middleware.ts index 7cd7059d..abecea72 100644 --- a/src/middleware/auth98Middleware.ts +++ b/src/middleware/auth98Middleware.ts @@ -34,9 +34,8 @@ type UserRole = 'user' | 'admin'; /** Require the user to prove their role before invoking the controller. */ function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware { - return withProof(async (c, proof, next) => { + return withProof(async (_c, proof, next) => { const user = await findUser({ pubkey: proof.pubkey }); - c.set('user', user); if (user && matchesRole(user, role)) { await next(); From ecfea827e1a3b5a06f6fad43f6e2a1806e683ffb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:38:05 -0500 Subject: [PATCH 158/252] Move RelayError into its own file, add helper methods --- src/RelayError.ts | 24 ++++++++++++++++++++++++ src/controllers/nostr/relay.ts | 3 ++- src/pipeline.ts | 22 +++------------------- src/utils/api.ts | 3 ++- 4 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/RelayError.ts diff --git a/src/RelayError.ts b/src/RelayError.ts new file mode 100644 index 00000000..1d275f63 --- /dev/null +++ b/src/RelayError.ts @@ -0,0 +1,24 @@ +import { NostrRelayOK } from '@nostrify/nostrify'; + +export type RelayErrorPrefix = 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error'; + +/** NIP-01 command line result. */ +export class RelayError extends Error { + constructor(prefix: RelayErrorPrefix, message: string) { + super(`${prefix}: ${message}`); + } + + /** Construct a RelayError from the reason message. */ + static fromReason(reason: string): RelayError { + const [prefix, ...rest] = reason.split(': '); + return new RelayError(prefix as RelayErrorPrefix, rest.join(': ')); + } + + /** Throw a new RelayError if the OK message is false. */ + static assert(msg: NostrRelayOK): void { + const [_, _eventId, ok, reason] = msg; + if (!ok) { + throw RelayError.fromReason(reason); + } + } +} diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 7d70ad9f..c0fa0263 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -10,6 +10,7 @@ import { } from '@nostrify/nostrify'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import * as pipeline from '@/pipeline.ts'; +import { RelayError } from '@/RelayError.ts'; import { Storages } from '@/storages.ts'; import type { AppController } from '@/app.ts'; @@ -95,7 +96,7 @@ function connectStream(socket: WebSocket) { await pipeline.handleEvent(event, AbortSignal.timeout(1000)); send(['OK', event.id, true, '']); } catch (e) { - if (e instanceof pipeline.RelayError) { + if (e instanceof RelayError) { send(['OK', event.id, false, e.message]); } else { send(['OK', event.id, false, 'error: something went wrong']); diff --git a/src/pipeline.ts b/src/pipeline.ts index a47a2da5..995abf21 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -10,6 +10,7 @@ import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; +import { RelayError } from '@/RelayError.ts'; import { updateStats } from '@/stats.ts'; import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; @@ -71,15 +72,7 @@ async function policyFilter(event: NostrEvent): Promise { const result = await policy.call(event); debug(JSON.stringify(result)); - const [_, _eventId, ok, reason] = result; - if (!ok) { - const [prefix, ...rest] = reason.split(': '); - if (['duplicate', 'pow', 'blocked', 'rate-limited', 'invalid'].includes(prefix)) { - throw new RelayError(prefix as RelayErrorPrefix, rest.join(': ')); - } else { - throw new RelayError('error', rest.join(': ')); - } - } + RelayError.assert(result); } /** Encounter the event, and return whether it has already been encountered. */ @@ -286,13 +279,4 @@ async function streamOut(event: NostrEvent): Promise { } } -type RelayErrorPrefix = 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error'; - -/** NIP-20 command line result. */ -class RelayError extends Error { - constructor(prefix: RelayErrorPrefix, message: string) { - super(`${prefix}: ${message}`); - } -} - -export { handleEvent, RelayError }; +export { handleEvent }; diff --git a/src/utils/api.ts b/src/utils/api.ts index 8da87fb6..70fd9955 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -9,6 +9,7 @@ import { z } from 'zod'; import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import * as pipeline from '@/pipeline.ts'; +import { RelayError } from '@/RelayError.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; import { Storages } from '@/storages.ts'; @@ -106,7 +107,7 @@ async function publishEvent(event: NostrEvent, c: AppContext): Promise Date: Tue, 14 May 2024 14:39:48 -0500 Subject: [PATCH 159/252] Uppercase CustomPolicy --- src/pipeline.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index 995abf21..b3eea251 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -61,8 +61,8 @@ async function policyFilter(event: NostrEvent): Promise { ]; try { - const customPolicy = (await import('../data/policy.ts')).default; - policies.push(new customPolicy()); + const CustomPolicy = (await import('../data/policy.ts')).default; + policies.push(new CustomPolicy()); } catch (_e) { debug('policy not found - https://docs.soapbox.pub/ditto/policies/'); } @@ -71,7 +71,6 @@ async function policyFilter(event: NostrEvent): Promise { const result = await policy.call(event); debug(JSON.stringify(result)); - RelayError.assert(result); } From e53ea222742ec0d7074988caa5b73884d140d469 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:48:37 -0500 Subject: [PATCH 160/252] Remove unused import --- src/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 447499f8..84ad780c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,7 +3,6 @@ import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; -import { type User } from '@/db/users.ts'; import '@/firehose.ts'; import { Time } from '@/utils.ts'; From eef349f1e9aa19d3b1dd80489531f69361b1e55f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 15:05:59 -0500 Subject: [PATCH 161/252] Update stats before storing event --- src/pipeline.ts | 6 ++---- src/stats.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index b3eea251..25cadb54 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -107,10 +107,8 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise[] = []; // Kind 3 is a special case - replace the count with the new list. if (event.kind === 3) { - prev = await maybeGetPrev(event); + prev = await getPrevEvent(event); if (!prev || event.created_at >= prev.created_at) { queries.push(updateFollowingCountQuery(event)); } @@ -153,12 +153,14 @@ function eventStatsQuery(diffs: EventStatDiff[]) { } /** Get the last version of the event, if any. */ -async function maybeGetPrev(event: NostrEvent): Promise { - const [prev] = await Storages.db.query([ - { kinds: [event.kind], authors: [event.pubkey], limit: 1 }, - ]); +async function getPrevEvent(event: NostrEvent): Promise { + if (NKinds.replaceable(event.kind) || NKinds.parameterizedReplaceable(event.kind)) { + const [prev] = await Storages.db.query([ + { kinds: [event.kind], authors: [event.pubkey], limit: 1 }, + ]); - return prev; + return prev; + } } /** Set the following count to the total number of unique "p" tags in the follow list. */ From 7feecab7232d284e42f70c0e0b942d8c1fa4346c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 15:25:56 -0500 Subject: [PATCH 162/252] stats: fix ambiguous column name error in Postgres? --- src/stats.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/stats.ts b/src/stats.ts index 1e95901c..21a4d979 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -119,9 +119,9 @@ function authorStatsQuery(diffs: AuthorStatDiff[]) { oc .column('pubkey') .doUpdateSet((eb) => ({ - followers_count: eb('followers_count', '+', eb.ref('excluded.followers_count')), - following_count: eb('following_count', '+', eb.ref('excluded.following_count')), - notes_count: eb('notes_count', '+', eb.ref('excluded.notes_count')), + followers_count: eb('author_stats.followers_count', '+', eb.ref('excluded.followers_count')), + following_count: eb('author_stats.following_count', '+', eb.ref('excluded.following_count')), + notes_count: eb('author_stats.notes_count', '+', eb.ref('excluded.notes_count')), })) ); } @@ -145,9 +145,9 @@ function eventStatsQuery(diffs: EventStatDiff[]) { oc .column('event_id') .doUpdateSet((eb) => ({ - replies_count: eb('replies_count', '+', eb.ref('excluded.replies_count')), - reposts_count: eb('reposts_count', '+', eb.ref('excluded.reposts_count')), - reactions_count: eb('reactions_count', '+', eb.ref('excluded.reactions_count')), + replies_count: eb('event_stats.replies_count', '+', eb.ref('excluded.replies_count')), + reposts_count: eb('event_stats.reposts_count', '+', eb.ref('excluded.reposts_count')), + reactions_count: eb('event_stats.reactions_count', '+', eb.ref('excluded.reactions_count')), })) ); } From dc87d3599dd3d0cc0442872c7061180ac15557ed Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 15:55:19 -0500 Subject: [PATCH 163/252] Add stats:recompute script --- deno.json | 3 ++- scripts/stats-recompute.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 scripts/stats-recompute.ts diff --git a/deno.json b/deno.json index 02255105..7cd56f03 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,8 @@ "check": "deno check 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" + "admin:role": "deno run -A scripts/admin-role.ts", + "stats:recompute": "deno run -A scripts/stats-recompute.ts" }, "unstable": ["ffi", "kv"], "exclude": ["./public"], diff --git a/scripts/stats-recompute.ts b/scripts/stats-recompute.ts new file mode 100644 index 00000000..9d204c7f --- /dev/null +++ b/scripts/stats-recompute.ts @@ -0,0 +1,36 @@ +import { nip19 } from 'nostr-tools'; + +import { db } from '@/db.ts'; +import { DittoTables } from '@/db/DittoTables.ts'; +import { Storages } from '@/storages.ts'; + +let pubkey: string; +try { + const result = nip19.decode(Deno.args[0]); + if (result.type === 'npub') { + pubkey = result.data; + } else { + throw new Error('Invalid npub'); + } +} catch { + console.error('Invalid npub'); + Deno.exit(1); +} + +const [followList] = await Storages.db.query([{ kinds: [3], authors: [pubkey], limit: 1 }]); + +const authorStats: DittoTables['author_stats'] = { + pubkey, + followers_count: (await Storages.db.count([{ kinds: [3], '#p': [pubkey] }])).count, + following_count: followList?.tags.filter(([name]) => name === 'p')?.length ?? 0, + notes_count: (await Storages.db.count([{ kinds: [1], authors: [pubkey] }])).count, +}; + +await db.insertInto('author_stats') + .values(authorStats) + .onConflict((oc) => + oc + .column('pubkey') + .doUpdateSet(authorStats) + ) + .execute(); From 3c706dc81be4e883505ccd8bfdb648df18d4a0f6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 16:10:50 -0500 Subject: [PATCH 164/252] Storages: make all methods async (total chaos and destruction) --- src/storages.ts | 85 +++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/storages.ts b/src/storages.ts index a51cbd19..056228be 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file require-await import { NCache } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; @@ -12,89 +13,97 @@ import { UserStore } from '@/storages/UserStore.ts'; import { Time } from '@/utils/time.ts'; export class Storages { - private static _db: EventsDB | undefined; - private static _admin: UserStore | undefined; - private static _cache: NCache | undefined; - private static _client: PoolStore | undefined; - private static _optimizer: Optimizer | undefined; - private static _reqmeister: Reqmeister | undefined; - private static _pubsub: InternalRelay | undefined; - private static _search: SearchStore | undefined; + private static _db: Promise | undefined; + private static _admin: Promise | undefined; + private static _cache: Promise | undefined; + private static _client: Promise | undefined; + private static _optimizer: Promise | undefined; + private static _reqmeister: Promise | undefined; + private static _pubsub: Promise | undefined; + private static _search: Promise | undefined; /** SQLite database to store events this Ditto server cares about. */ - public static get db(): EventsDB { + public static async db(): Promise { if (!this._db) { - this._db = new EventsDB(db); + this._db = Promise.resolve(new EventsDB(db)); } return this._db; } /** Admin user storage. */ - public static get admin(): UserStore { + public static async admin(): Promise { if (!this._admin) { - this._admin = new UserStore(Conf.pubkey, this.db); + this._admin = Promise.resolve(new UserStore(Conf.pubkey, await this.db())); } return this._admin; } /** Internal pubsub relay between controllers and the pipeline. */ - public static get pubsub(): InternalRelay { + public static async pubsub(): Promise { if (!this._pubsub) { - this._pubsub = new InternalRelay(); + this._pubsub = Promise.resolve(new InternalRelay()); } return this._pubsub; } /** Relay pool storage. */ - public static get client(): PoolStore { + public static async client(): Promise { if (!this._client) { - this._client = new PoolStore({ - pool, - relays: activeRelays, - }); + this._client = Promise.resolve( + new PoolStore({ + pool, + relays: activeRelays, + }), + ); } return this._client; } /** In-memory data store for cached events. */ - public static get cache(): NCache { + public static async cache(): Promise { if (!this._cache) { - this._cache = new NCache({ max: 3000 }); + this._cache = Promise.resolve(new NCache({ max: 3000 })); } return this._cache; } /** Batches requests for single events. */ - public static get reqmeister(): Reqmeister { + public static async reqmeister(): Promise { if (!this._reqmeister) { - this._reqmeister = new Reqmeister({ - client: this.client, - delay: Time.seconds(1), - timeout: Time.seconds(1), - }); + this._reqmeister = Promise.resolve( + new Reqmeister({ + client: await this.client(), + delay: Time.seconds(1), + timeout: Time.seconds(1), + }), + ); } return this._reqmeister; } /** Main Ditto storage adapter */ - public static get optimizer(): Optimizer { + public static async optimizer(): Promise { if (!this._optimizer) { - this._optimizer = new Optimizer({ - db: this.db, - cache: this.cache, - client: this.reqmeister, - }); + this._optimizer = Promise.resolve( + new Optimizer({ + db: await this.db(), + cache: await this.cache(), + client: await this.reqmeister(), + }), + ); } return this._optimizer; } /** Storage to use for remote search. */ - public static get search(): SearchStore { + public static async search(): Promise { if (!this._search) { - this._search = new SearchStore({ - relay: Conf.searchRelay, - fallback: this.optimizer, - }); + this._search = Promise.resolve( + new SearchStore({ + relay: Conf.searchRelay, + fallback: await this.optimizer(), + }), + ); } return this._search; } From 08c9ee0670f3d583af56e24271153006ef01b523 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 16:25:24 -0500 Subject: [PATCH 165/252] Refactor client and firehose --- src/app.ts | 7 ++++++- src/config.ts | 4 ++++ src/firehose.ts | 39 +++++++++++++++++++-------------------- src/pool.ts | 34 ---------------------------------- src/stats.ts | 9 ++++++--- src/storages.ts | 39 ++++++++++++++++++++++++++++++++++----- 6 files changed, 69 insertions(+), 63 deletions(-) delete mode 100644 src/pool.ts diff --git a/src/app.ts b/src/app.ts index 84ad780c..059e883c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,7 +3,8 @@ import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; -import '@/firehose.ts'; +import { Conf } from '@/config.ts'; +import { startFirehose } from '@/firehose.ts'; import { Time } from '@/utils.ts'; import { actorController } from '@/controllers/activitypub/actor.ts'; @@ -108,6 +109,10 @@ const app = new Hono(); const debug = Debug('ditto:http'); +if (Conf.firehoseEnabled) { + startFirehose(); +} + app.use('/api/*', logger(debug)); app.use('/relay/*', logger(debug)); app.use('/.well-known/*', logger(debug)); diff --git a/src/config.ts b/src/config.ts index 4266033e..3d3e51b4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -215,6 +215,10 @@ class Conf { return Number(Deno.env.get('PG_POOL_SIZE') ?? 10); }, }; + /** Whether to enable requesting events from known relays. */ + static get firehoseEnabled(): boolean { + return optionalBooleanSchema.parse(Deno.env.get('FIREHOSE_ENABLED')) ?? true; + } } const optionalBooleanSchema = z diff --git a/src/firehose.ts b/src/firehose.ts index d7aaab35..2c776fe4 100644 --- a/src/firehose.ts +++ b/src/firehose.ts @@ -1,29 +1,28 @@ -import { NostrEvent } from '@nostrify/nostrify'; -import Debug from '@soapbox/stickynotes/debug'; +import { Stickynotes } from '@soapbox/stickynotes'; -import { activeRelays, pool } from '@/pool.ts'; +import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; import * as pipeline from './pipeline.ts'; -const debug = Debug('ditto:firehose'); +const console = new Stickynotes('ditto:firehose'); -// This file watches events on all known relays and performs -// side-effects based on them, such as trending hashtag tracking -// and storing events for notifications and the home feed. -pool.subscribe( - [{ kinds: [0, 1, 3, 5, 6, 7, 9735, 10002], limit: 0, since: nostrNow() }], - activeRelays, - handleEvent, - undefined, - undefined, -); +/** + * This function watches events on all known relays and performs + * side-effects based on them, such as trending hashtag tracking + * and storing events for notifications and the home feed. + */ +export async function startFirehose() { + const store = await Storages.client(); -/** Handle events through the firehose pipeline. */ -function handleEvent(event: NostrEvent): Promise { - debug(`NostrEvent<${event.kind}> ${event.id}`); + for await (const msg of store.req([{ kinds: [0, 1, 3, 5, 6, 7, 9735, 10002], limit: 0, since: nostrNow() }])) { + if (msg[0] === 'EVENT') { + const event = msg[2]; + console.debug(`NostrEvent<${event.kind}> ${event.id}`); - return pipeline - .handleEvent(event, AbortSignal.timeout(5000)) - .catch(() => {}); + pipeline + .handleEvent(event, AbortSignal.timeout(5000)) + .catch(() => {}); + } + } } diff --git a/src/pool.ts b/src/pool.ts deleted file mode 100644 index 3ac1a1db..00000000 --- a/src/pool.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { RelayPoolWorker } from 'nostr-relaypool'; - -import { Storages } from '@/storages.ts'; -import { Conf } from '@/config.ts'; - -const [relayList] = await Storages.db.query([ - { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, -]); - -const tags = relayList?.tags ?? []; - -const activeRelays = tags.reduce((acc, [name, url, marker]) => { - if (name === 'r' && !marker) { - acc.push(url); - } - return acc; -}, []); - -console.log(`pool: connecting to ${activeRelays.length} relays.`); - -const worker = new Worker('https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js', { - type: 'module', -}); - -// @ts-ignore Wrong types. -const pool = new RelayPoolWorker(worker, activeRelays, { - autoReconnect: true, - // The pipeline verifies events. - skipVerification: true, - // The logging feature overwhelms the CPU and creates too many logs. - logErrorsAndNotices: false, -}); - -export { activeRelays, pool }; diff --git a/src/stats.ts b/src/stats.ts index 21a4d979..bff2aebc 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -47,6 +47,7 @@ async function updateStats(event: NostrEvent) { /** Calculate stats changes ahead of time so we can build an efficient query. */ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Promise { + const store = await Storages.db(); const statDiffs: StatDiff[] = []; const firstTaggedId = event.tags.find(([name]) => name === 'e')?.[1]; @@ -65,7 +66,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr case 5: { if (!firstTaggedId) break; - const [repostedEvent] = await Storages.db.query( + const [repostedEvent] = await store.query( [{ kinds: [6], ids: [firstTaggedId], authors: [event.pubkey] }], { limit: 1 }, ); @@ -77,7 +78,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr const eventBeingRepostedPubkey = repostedEvent.tags.find(([name]) => name === 'p')?.[1]; if (!eventBeingRepostedId || !eventBeingRepostedPubkey) break; - const [eventBeingReposted] = await Storages.db.query( + const [eventBeingReposted] = await store.query( [{ kinds: [1], ids: [eventBeingRepostedId], authors: [eventBeingRepostedPubkey] }], { limit: 1 }, ); @@ -155,7 +156,9 @@ function eventStatsQuery(diffs: EventStatDiff[]) { /** Get the last version of the event, if any. */ async function getPrevEvent(event: NostrEvent): Promise { if (NKinds.replaceable(event.kind) || NKinds.parameterizedReplaceable(event.kind)) { - const [prev] = await Storages.db.query([ + const store = await Storages.db(); + + const [prev] = await store.query([ { kinds: [event.kind], authors: [event.pubkey], limit: 1 }, ]); diff --git a/src/storages.ts b/src/storages.ts index 056228be..cbda925d 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -2,7 +2,6 @@ import { NCache } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; import { db } from '@/db.ts'; -import { activeRelays, pool } from '@/pool.ts'; import { EventsDB } from '@/storages/events-db.ts'; import { Optimizer } from '@/storages/optimizer.ts'; import { PoolStore } from '@/storages/pool-store.ts'; @@ -49,12 +48,42 @@ export class Storages { /** Relay pool storage. */ public static async client(): Promise { if (!this._client) { - this._client = Promise.resolve( - new PoolStore({ + this._client = (async () => { + const db = await this.db(); + + const [relayList] = await db.query([ + { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, + ]); + + const tags = relayList?.tags ?? []; + + const activeRelays = tags.reduce((acc, [name, url, marker]) => { + if (name === 'r' && !marker) { + acc.push(url); + } + return acc; + }, []); + + console.log(`pool: connecting to ${activeRelays.length} relays.`); + + const worker = new Worker('https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js', { + type: 'module', + }); + + // @ts-ignore Wrong types. + const pool = new RelayPoolWorker(worker, activeRelays, { + autoReconnect: true, + // The pipeline verifies events. + skipVerification: true, + // The logging feature overwhelms the CPU and creates too many logs. + logErrorsAndNotices: false, + }); + + return new PoolStore({ pool, relays: activeRelays, - }), - ); + }); + })(); } return this._client; } From 68b5887ed01ac64badec805439e359cef44d7aa2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 18:23:41 -0500 Subject: [PATCH 166/252] Don't let your memes be dreams --- src/controllers/activitypub/actor.ts | 2 +- src/controllers/api/accounts.ts | 29 ++++++++------ src/controllers/api/admin.ts | 7 ++-- src/controllers/api/bookmarks.ts | 3 +- src/controllers/api/ditto.ts | 7 +++- src/controllers/api/instance.ts | 3 +- src/controllers/api/mutes.ts | 3 +- src/controllers/api/notifications.ts | 2 +- src/controllers/api/pleroma.ts | 15 +++++--- src/controllers/api/reports.ts | 8 ++-- src/controllers/api/search.ts | 13 ++++--- src/controllers/api/statuses.ts | 37 ++++++++++-------- src/controllers/api/streaming.ts | 6 ++- src/controllers/api/suggestions.ts | 2 +- src/controllers/api/timelines.ts | 8 +--- src/controllers/nostr/relay-info.ts | 4 +- src/controllers/nostr/relay.ts | 10 +++-- src/controllers/well-known/nostr.ts | 2 +- src/controllers/well-known/webfinger.ts | 2 +- src/db/users.ts | 3 +- src/middleware/storeMiddleware.ts | 4 +- src/pipeline.ts | 39 ++++++++++++------- src/pipeline/DVM.ts | 4 +- src/queries.ts | 22 +++++++---- src/signers/ConnectSigner.ts | 51 ++++++++++++++++++++----- src/storages/UserStore.ts | 17 +++------ src/storages/hydrate.test.ts | 10 ++--- src/storages/hydrate.ts | 46 +++++++++++----------- src/storages/pool-store.ts | 3 +- src/storages/search-store.ts | 2 +- src/utils/api.ts | 11 ++++-- src/utils/connect.ts | 3 +- src/utils/instance.ts | 7 ++-- src/utils/nip05.ts | 9 +++-- src/utils/outbox.ts | 9 +++-- src/views.ts | 18 +++++---- src/views/mastodon/relationships.ts | 4 +- src/views/mastodon/statuses.ts | 9 +++-- 38 files changed, 260 insertions(+), 174 deletions(-) diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts index e82a88a0..19f5f100 100644 --- a/src/controllers/activitypub/actor.ts +++ b/src/controllers/activitypub/actor.ts @@ -9,7 +9,7 @@ const actorController: AppController = async (c) => { const username = c.req.param('username'); const { signal } = c.req.raw; - const pointer = await localNip05Lookup(username); + const pointer = await localNip05Lookup(c.get('store'), username); if (!pointer) return notFound(c); const event = await getAuthor(pointer.pubkey, { signal }); diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 88d19b10..5c26ba51 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -94,15 +94,16 @@ const accountSearchController: AppController = async (c) => { } const query = decodeURIComponent(q); + const store = await Storages.search(); const [event, events] = await Promise.all([ lookupAccount(query), - Storages.search.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), + store.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), ]); const results = await hydrateEvents({ events: event ? [event, ...events] : events, - storage: Storages.db, + store, signal: c.req.raw.signal, }); @@ -147,8 +148,10 @@ const accountStatusesController: AppController = async (c) => { const { pinned, limit, exclude_replies, tagged } = accountStatusesQuerySchema.parse(c.req.query()); const { signal } = c.req.raw; + const store = await Storages.db(); + if (pinned) { - const [pinEvent] = await Storages.db.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal }); + const [pinEvent] = await store.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal }); if (pinEvent) { const pinnedEventIds = getTagSet(pinEvent.tags, 'e'); return renderStatuses(c, [...pinnedEventIds].reverse()); @@ -169,8 +172,8 @@ const accountStatusesController: AppController = async (c) => { filter['#t'] = [tagged]; } - const events = await Storages.db.query([filter], { signal }) - .then((events) => hydrateEvents({ events, storage: Storages.db, signal })) + const events = await store.query([filter], { signal }) + .then((events) => hydrateEvents({ events, store, signal })) .then((events) => { if (exclude_replies) { return events.filter((event) => !findReplyTag(event.tags)); @@ -244,7 +247,7 @@ const followController: AppController = async (c) => { const targetPubkey = c.req.param('pubkey'); await updateListEvent( - { kinds: [3], authors: [sourcePubkey] }, + { kinds: [3], authors: [sourcePubkey], limit: 1 }, (tags) => addTag(tags, ['p', targetPubkey]), c, ); @@ -261,7 +264,7 @@ const unfollowController: AppController = async (c) => { const targetPubkey = c.req.param('pubkey'); await updateListEvent( - { kinds: [3], authors: [sourcePubkey] }, + { kinds: [3], authors: [sourcePubkey], limit: 1 }, (tags) => deleteTag(tags, ['p', targetPubkey]), c, ); @@ -298,7 +301,7 @@ const muteController: AppController = async (c) => { const targetPubkey = c.req.param('pubkey'); await updateListEvent( - { kinds: [10000], authors: [sourcePubkey] }, + { kinds: [10000], authors: [sourcePubkey], limit: 1 }, (tags) => addTag(tags, ['p', targetPubkey]), c, ); @@ -313,7 +316,7 @@ const unmuteController: AppController = async (c) => { const targetPubkey = c.req.param('pubkey'); await updateListEvent( - { kinds: [10000], authors: [sourcePubkey] }, + { kinds: [10000], authors: [sourcePubkey], limit: 1 }, (tags) => deleteTag(tags, ['p', targetPubkey]), c, ); @@ -327,7 +330,9 @@ const favouritesController: AppController = async (c) => { const params = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events7 = await Storages.db.query( + const store = await Storages.db(); + + const events7 = await store.query( [{ kinds: [7], authors: [pubkey], ...params }], { signal }, ); @@ -336,8 +341,8 @@ const favouritesController: AppController = async (c) => { .map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1]) .filter((id): id is string => !!id); - const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal }) - .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); + const events1 = await store.query([{ kinds: [1], ids }], { signal }) + .then((events) => hydrateEvents({ events, store, signal })); const viewerPubkey = await c.get('signer')?.getPublicKey(); diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 99a8e5bc..b9464a3f 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -39,12 +39,13 @@ const adminAccountsController: AppController = async (c) => { return c.json([]); } + const store = await Storages.db(); const { since, until, limit } = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events = await Storages.db.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal }); + const events = await store.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal }); const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); - const authors = await Storages.db.query([{ kinds: [0], authors: pubkeys }], { signal }); + const authors = await store.query([{ kinds: [0], authors: pubkeys }], { signal }); for (const event of events) { const d = event.tags.find(([name]) => name === 'd')?.[1]; @@ -78,7 +79,7 @@ const adminAccountAction: AppController = async (c) => { } await updateListAdminEvent( - { kinds: [10000], authors: [Conf.pubkey] }, + { kinds: [10000], authors: [Conf.pubkey], limit: 1 }, (tags) => addTag(tags, ['p', authorId]), c, ); diff --git a/src/controllers/api/bookmarks.ts b/src/controllers/api/bookmarks.ts index 1616fa2d..76551827 100644 --- a/src/controllers/api/bookmarks.ts +++ b/src/controllers/api/bookmarks.ts @@ -5,10 +5,11 @@ import { renderStatuses } from '@/views.ts'; /** https://docs.joinmastodon.org/methods/bookmarks/#get */ const bookmarksController: AppController = async (c) => { + const store = await Storages.db(); const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; - const [event10003] = await Storages.db.query( + const [event10003] = await store.query( [{ kinds: [10003], authors: [pubkey], limit: 1 }], { signal }, ); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index f0f70360..df4f210e 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -16,7 +16,9 @@ const relaySchema = z.object({ type RelayEntity = z.infer; export const adminRelaysController: AppController = async (c) => { - const [event] = await Storages.db.query([ + const store = await Storages.db(); + + const [event] = await store.query([ { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, ]); @@ -28,6 +30,7 @@ export const adminRelaysController: AppController = async (c) => { }; export const adminSetRelaysController: AppController = async (c) => { + const store = await Storages.db(); const relays = relaySchema.array().parse(await c.req.json()); const event = await new AdminSigner().signEvent({ @@ -37,7 +40,7 @@ export const adminSetRelaysController: AppController = async (c) => { created_at: Math.floor(Date.now() / 1000), }); - await Storages.db.event(event); + await store.event(event); return c.json(renderRelays(event)); }; diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index cc71b1f1..5f949b05 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,10 +1,11 @@ import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { Storages } from '@/storages.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; const instanceController: AppController = async (c) => { const { host, protocol } = Conf.url; - const meta = await getInstanceMetadata(c.req.raw.signal); + const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; diff --git a/src/controllers/api/mutes.ts b/src/controllers/api/mutes.ts index fe048e98..4afb6c40 100644 --- a/src/controllers/api/mutes.ts +++ b/src/controllers/api/mutes.ts @@ -5,10 +5,11 @@ import { renderAccounts } from '@/views.ts'; /** https://docs.joinmastodon.org/methods/mutes/#get */ const mutesController: AppController = async (c) => { + const store = await Storages.db(); const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; - const [event10000] = await Storages.db.query( + const [event10000] = await store.query( [{ kinds: [10000], authors: [pubkey], limit: 1 }], { signal }, ); diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index 857f2a32..ba15bd02 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -20,7 +20,7 @@ async function renderNotifications(c: AppContext, filters: NostrFilter[]) { const events = await store .query(filters, { signal }) .then((events) => events.filter((event) => event.pubkey !== pubkey)) - .then((events) => hydrateEvents({ events, storage: store, signal })); + .then((events) => hydrateEvents({ events, store, signal })); if (!events.length) { return c.json([]); diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 4b693c4f..3bbdd70e 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -1,4 +1,4 @@ -import { NSchema as n } from '@nostrify/nostrify'; +import { NSchema as n, NStore } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; @@ -9,7 +9,8 @@ import { Storages } from '@/storages.ts'; import { createAdminEvent } from '@/utils/api.ts'; const frontendConfigController: AppController = async (c) => { - const configs = await getConfigs(c.req.raw.signal); + 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'); if (frontendConfig) { @@ -25,7 +26,8 @@ const frontendConfigController: AppController = async (c) => { }; const configController: AppController = async (c) => { - const configs = await getConfigs(c.req.raw.signal); + const store = await Storages.db(); + const configs = await getConfigs(store, c.req.raw.signal); return c.json({ configs, need_reboot: false }); }; @@ -33,7 +35,8 @@ const configController: AppController = async (c) => { const updateConfigController: AppController = async (c) => { const { pubkey } = Conf; - const configs = await getConfigs(c.req.raw.signal); + const store = await Storages.db(); + const configs = await getConfigs(store, c.req.raw.signal); const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json()); for (const { group, key, value } of newConfigs) { @@ -63,10 +66,10 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => { return c.json({}); }; -async function getConfigs(signal: AbortSignal): Promise { +async function getConfigs(store: NStore, signal: AbortSignal): Promise { const { pubkey } = Conf; - const [event] = await Storages.db.query([{ + const [event] = await store.query([{ kinds: [30078], authors: [pubkey], '#d': ['pub.ditto.pleroma.config'], diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 55fb6019..9cb2627a 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -48,7 +48,7 @@ const reportController: AppController = async (c) => { tags, }, c); - await hydrateEvents({ events: [event], storage: store }); + await hydrateEvents({ events: [event], store }); return c.json(await renderReport(event)); }; @@ -58,7 +58,7 @@ const adminReportsController: AppController = async (c) => { const viewerPubkey = await c.get('signer')?.getPublicKey(); const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]) - .then((events) => hydrateEvents({ storage: store, events: events, signal: c.req.raw.signal })) + .then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal })) .then((events) => Promise.all( events.map((event) => renderAdminReport(event, { viewerPubkey })), @@ -85,7 +85,7 @@ const adminReportController: AppController = async (c) => { return c.json({ error: 'This action is not allowed' }, 403); } - await hydrateEvents({ events: [event], storage: store, signal }); + await hydrateEvents({ events: [event], store, signal }); return c.json(await renderAdminReport(event, { viewerPubkey: pubkey })); }; @@ -107,7 +107,7 @@ const adminReportResolveController: AppController = async (c) => { return c.json({ error: 'This action is not allowed' }, 403); } - await hydrateEvents({ events: [event], storage: store, signal }); + await hydrateEvents({ events: [event], store, signal }); await createAdminEvent({ kind: 5, diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index fe08ace1..0151f7de 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -78,7 +78,7 @@ const searchController: AppController = async (c) => { }; /** Get events for the search params. */ -function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise { +async function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise { if (type === 'hashtags') return Promise.resolve([]); const filter: NostrFilter = { @@ -91,8 +91,10 @@ function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: Abort filter.authors = [account_id]; } - return Storages.search.query([filter], { signal }) - .then((events) => hydrateEvents({ events, storage: Storages.search, signal })); + const store = await Storages.search(); + + return store.query([filter], { signal }) + .then((events) => hydrateEvents({ events, store, signal })); } /** Get event kinds to search from `type` query param. */ @@ -110,9 +112,10 @@ function typeToKinds(type: SearchQuery['type']): number[] { /** Resolve a searched value into an event, if applicable. */ async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise { const filters = await getLookupFilters(query, signal); + const store = await Storages.search(); - return Storages.search.query(filters, { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: Storages.search, signal })) + return store.query(filters, { limit: 1, signal }) + .then((events) => hydrateEvents({ events, store, signal })) .then(([event]) => event); } diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 1138c0a3..a52a4088 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -140,7 +140,7 @@ const createStatusController: AppController = async (c) => { if (data.quote_id) { await hydrateEvents({ events: [event], - storage: Storages.db, + store: await Storages.db(), signal: c.req.raw.signal, }); } @@ -248,7 +248,7 @@ const reblogStatusController: AppController = async (c) => { await hydrateEvents({ events: [reblogEvent], - storage: Storages.db, + store: await Storages.db(), signal: signal, }); @@ -260,23 +260,30 @@ const reblogStatusController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unreblog */ const unreblogStatusController: AppController = async (c) => { const eventId = c.req.param('id'); - const pubkey = await c.get('signer')?.getPublicKey() as string; + const pubkey = await c.get('signer')?.getPublicKey()!; - const event = await getEvent(eventId, { - kind: 1, - }); - if (!event) return c.json({ error: 'Event not found.' }, 404); + const event = await getEvent(eventId, { kind: 1 }); - const filters: NostrFilter[] = [{ kinds: [6], authors: [pubkey], '#e': [event.id] }]; - const [repostedEvent] = await Storages.db.query(filters, { limit: 1 }); - if (!repostedEvent) return c.json({ error: 'Event not found.' }, 404); + if (!event) { + return c.json({ error: 'Event not found.' }, 404); + } + + const store = await Storages.db(); + + const [repostedEvent] = await store.query( + [{ kinds: [6], authors: [pubkey], '#e': [event.id], limit: 1 }], + ); + + if (!repostedEvent) { + return c.json({ error: 'Event not found.' }, 404); + } await createEvent({ kind: 5, tags: [['e', repostedEvent.id]], }, c); - return c.json(await renderStatus(event, {})); + return c.json(await renderStatus(event, { viewerPubkey: pubkey })); }; const rebloggedByController: AppController = (c) => { @@ -297,7 +304,7 @@ const bookmarkController: AppController = async (c) => { if (event) { await updateListEvent( - { kinds: [10003], authors: [pubkey] }, + { kinds: [10003], authors: [pubkey], limit: 1 }, (tags) => addTag(tags, ['e', eventId]), c, ); @@ -324,7 +331,7 @@ const unbookmarkController: AppController = async (c) => { if (event) { await updateListEvent( - { kinds: [10003], authors: [pubkey] }, + { kinds: [10003], authors: [pubkey], limit: 1 }, (tags) => deleteTag(tags, ['e', eventId]), c, ); @@ -351,7 +358,7 @@ const pinController: AppController = async (c) => { if (event) { await updateListEvent( - { kinds: [10001], authors: [pubkey] }, + { kinds: [10001], authors: [pubkey], limit: 1 }, (tags) => addTag(tags, ['e', eventId]), c, ); @@ -380,7 +387,7 @@ const unpinController: AppController = async (c) => { if (event) { await updateListEvent( - { kinds: [10001], authors: [pubkey] }, + { kinds: [10001], authors: [pubkey], limit: 1 }, (tags) => deleteTag(tags, ['e', eventId]), c, ); diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 8d22d5cc..e79c51eb 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -68,13 +68,15 @@ const streamingController: AppController = (c) => { if (!filter) return; try { - for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) { + const store = await Storages.pubsub(); + + for await (const msg of store.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { const event = msg[2]; await hydrateEvents({ events: [event], - storage: Storages.admin, + store, signal: AbortSignal.timeout(1000), }); diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index bde09165..6377bd4f 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -40,7 +40,7 @@ async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) { [{ kinds: [0], authors: pubkeys, limit: pubkeys.length }], { signal }, ) - .then((events) => hydrateEvents({ events, storage: store, signal })); + .then((events) => hydrateEvents({ events, store, signal })); const accounts = await Promise.all(pubkeys.map((pubkey) => { const profile = profiles.find((event) => event.pubkey === pubkey); diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 0880d84f..e83c50cd 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -49,13 +49,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { const events = await store .query(filters, { signal }) - .then((events) => - hydrateEvents({ - events, - storage: store, - signal, - }) - ); + .then((events) => hydrateEvents({ events, store, signal })); if (!events.length) { return c.json([]); diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index 192cab22..bbce7d34 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,9 +1,11 @@ import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { Storages } from '@/storages.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; const relayInfoController: AppController = async (c) => { - const meta = await getInstanceMetadata(c.req.raw.signal); + const store = await Storages.db(); + const meta = await getInstanceMetadata(store, c.req.raw.signal); return c.json({ name: meta.name, diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index c0fa0263..259f5e94 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -72,14 +72,17 @@ function connectStream(socket: WebSocket) { controllers.get(subId)?.abort(); controllers.set(subId, controller); - for (const event of await Storages.db.query(filters, { limit: FILTER_LIMIT })) { + const db = await Storages.db(); + const pubsub = await Storages.pubsub(); + + for (const event of await db.query(filters, { limit: FILTER_LIMIT })) { send(['EVENT', subId, event]); } send(['EOSE', subId]); try { - for await (const msg of Storages.pubsub.req(filters, { signal: controller.signal })) { + for await (const msg of pubsub.req(filters, { signal: controller.signal })) { if (msg[0] === 'EVENT') { send(['EVENT', subId, msg[2]]); } @@ -116,7 +119,8 @@ function connectStream(socket: WebSocket) { /** Handle COUNT. Return the number of events matching the filters. */ async function handleCount([_, subId, ...rest]: NostrClientCOUNT): Promise { - const { count } = await Storages.db.count(prepareFilters(rest)); + const store = await Storages.db(); + const { count } = await store.count(prepareFilters(rest)); send(['COUNT', subId, { count, approximate: false }]); } diff --git a/src/controllers/well-known/nostr.ts b/src/controllers/well-known/nostr.ts index f1ebb6bb..06698887 100644 --- a/src/controllers/well-known/nostr.ts +++ b/src/controllers/well-known/nostr.ts @@ -12,7 +12,7 @@ const nameSchema = z.string().min(1).regex(/^\w+$/); const nostrController: AppController = async (c) => { const result = nameSchema.safeParse(c.req.query('name')); const name = result.success ? result.data : undefined; - const pointer = name ? await localNip05Lookup(name) : undefined; + const pointer = name ? await localNip05Lookup(c.get('store'), name) : undefined; if (!name || !pointer) { return c.json({ names: {}, relays: {} }); diff --git a/src/controllers/well-known/webfinger.ts b/src/controllers/well-known/webfinger.ts index 38bc9943..c1c8b815 100644 --- a/src/controllers/well-known/webfinger.ts +++ b/src/controllers/well-known/webfinger.ts @@ -45,7 +45,7 @@ async function handleAcct(c: AppContext, resource: URL): Promise { } const [username, host] = result.data; - const pointer = await localNip05Lookup(username); + const pointer = await localNip05Lookup(c.get('store'), username); if (!pointer) { return c.json({ error: 'Not found' }, 404); diff --git a/src/db/users.ts b/src/db/users.ts index c7659e43..bf0cab7c 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -60,7 +60,8 @@ async function findUser(user: Partial, signal?: AbortSignal): Promise { const pubkey = await c.get('signer')?.getPublicKey(); if (pubkey) { - const store = new UserStore(pubkey, Storages.admin); + const store = new UserStore(pubkey, await Storages.admin()); c.set('store', store); } else { - c.set('store', Storages.admin); + c.set('store', await Storages.admin()); } await next(); }; diff --git a/src/pipeline.ts b/src/pipeline.ts index 25cadb54..9742b492 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -57,7 +57,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { const policies: NPolicy[] = [ - new MuteListPolicy(Conf.pubkey, Storages.admin), + new MuteListPolicy(Conf.pubkey, await Storages.admin()), ]; try { @@ -76,15 +76,20 @@ async function policyFilter(event: NostrEvent): Promise { /** Encounter the event, and return whether it has already been encountered. */ async function encounterEvent(event: NostrEvent, signal: AbortSignal): Promise { - const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]); - Storages.cache.event(event); - Storages.reqmeister.event(event, { signal }); + const cache = await Storages.cache(); + const reqmeister = await Storages.reqmeister(); + + const [existing] = await cache.query([{ ids: [event.id], limit: 1 }]); + + cache.event(event); + reqmeister.event(event, { signal }); + return !!existing; } /** Hydrate the event with the user, if applicable. */ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { - await hydrateEvents({ events: [event], storage: Storages.db, signal }); + await hydrateEvents({ events: [event], store: await Storages.db(), signal }); const domain = await db .selectFrom('pubkey_domains') @@ -98,8 +103,9 @@ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { if (isEphemeralKind(event.kind)) return; + const store = await Storages.db(); - const [deletion] = await Storages.db.query( + const [deletion] = await store.query( [{ kinds: [5], authors: [Conf.pubkey, event.pubkey], '#e': [event.id], limit: 1 }], { signal }, ); @@ -108,7 +114,7 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise { if (event.kind === 5) { const ids = getTagSet(event.tags, 'e'); + const store = await Storages.db(); if (event.pubkey === Conf.pubkey) { - await Storages.db.remove([{ ids: [...ids] }], { signal }); + await store.remove([{ ids: [...ids] }], { signal }); } else { - const events = await Storages.db.query( + const events = await store.query( [{ ids: [...ids], authors: [event.pubkey] }], { signal }, ); const deleteIds = events.map(({ id }) => id); - await Storages.db.remove([{ ids: deleteIds }], { signal }); + await store.remove([{ ids: deleteIds }], { signal }); } } } @@ -189,19 +196,22 @@ async function trackHashtags(event: NostrEvent): Promise { /** Queue related events to fetch. */ async function fetchRelatedEvents(event: DittoEvent) { + const cache = await Storages.cache(); + const reqmeister = await Storages.reqmeister(); + if (!event.author) { const signal = AbortSignal.timeout(3000); - Storages.reqmeister.query([{ kinds: [0], authors: [event.pubkey] }], { signal }) + reqmeister.query([{ kinds: [0], authors: [event.pubkey] }], { signal }) .then((events) => Promise.allSettled(events.map((event) => handleEvent(event, signal)))) .catch(() => {}); } for (const [name, id] of event.tags) { if (name === 'e') { - const { count } = await Storages.cache.count([{ ids: [id] }]); + const { count } = await cache.count([{ ids: [id] }]); if (!count) { const signal = AbortSignal.timeout(3000); - Storages.reqmeister.query([{ ids: [id] }], { signal }) + reqmeister.query([{ ids: [id] }], { signal }) .then((events) => Promise.allSettled(events.map((event) => handleEvent(event, signal)))) .catch(() => {}); } @@ -272,7 +282,8 @@ function isFresh(event: NostrEvent): boolean { /** Distribute the event through active subscriptions. */ async function streamOut(event: NostrEvent): Promise { if (isFresh(event)) { - await Storages.pubsub.event(event); + const pubsub = await Storages.pubsub(); + await pubsub.event(event); } } diff --git a/src/pipeline/DVM.ts b/src/pipeline/DVM.ts index 953e9be0..a811067c 100644 --- a/src/pipeline/DVM.ts +++ b/src/pipeline/DVM.ts @@ -34,7 +34,9 @@ export class DVM { return DVM.feedback(event, 'error', `Forbidden user: ${user}`); } - const [label] = await Storages.db.query([{ + const store = await Storages.db(); + + const [label] = await store.query([{ kinds: [1985], authors: [admin], '#L': ['nip05'], diff --git a/src/queries.ts b/src/queries.ts index 5626c45e..76fabfdd 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -25,6 +25,7 @@ const getEvent = async ( opts: GetEventOpts = {}, ): Promise => { debug(`getEvent: ${id}`); + const store = await Storages.optimizer(); const { kind, signal = AbortSignal.timeout(1000) } = opts; const filter: NostrFilter = { ids: [id], limit: 1 }; @@ -32,23 +33,25 @@ const getEvent = async ( filter.kinds = [kind]; } - return await Storages.optimizer.query([filter], { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: Storages.optimizer, signal })) + return await store.query([filter], { limit: 1, signal }) + .then((events) => hydrateEvents({ events, store, signal })) .then(([event]) => event); }; /** Get a Nostr `set_medatadata` event for a user's pubkey. */ const getAuthor = async (pubkey: string, opts: GetEventOpts = {}): Promise => { + const store = await Storages.optimizer(); const { signal = AbortSignal.timeout(1000) } = opts; - return await Storages.optimizer.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: Storages.optimizer, signal })) + return await store.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal }) + .then((events) => hydrateEvents({ events, store, signal })) .then(([event]) => event); }; /** Get users the given pubkey follows. */ const getFollows = async (pubkey: string, signal?: AbortSignal): Promise => { - const [event] = await Storages.db.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); + const store = await Storages.db(); + const [event] = await store.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); return event; }; @@ -84,15 +87,18 @@ async function getAncestors(event: NostrEvent, result: NostrEvent[] = []): Promi } async function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise { - const events = await Storages.db.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); - return hydrateEvents({ events, storage: Storages.db, signal }); + const store = await Storages.db(); + const events = await store.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); + return hydrateEvents({ events, store, signal }); } /** Returns whether the pubkey is followed by a local user. */ async function isLocallyFollowed(pubkey: string): Promise { const { host } = Conf.url; - const [event] = await Storages.db.query( + const store = await Storages.db(); + + const [event] = await store.query( [{ kinds: [3], '#p': [pubkey], search: `domain:${host}`, limit: 1 }], { limit: 1 }, ); diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index 4dda0b1d..f482413d 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file require-await -import { NConnectSigner } from '@nostrify/nostrify'; +import { NConnectSigner, NostrEvent, NostrSigner } from '@nostrify/nostrify'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; @@ -9,24 +9,55 @@ import { Storages } from '@/storages.ts'; * * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. */ -export class ConnectSigner extends NConnectSigner { - private _pubkey: string; +export class ConnectSigner implements NostrSigner { + private signer: Promise; - constructor(pubkey: string, private relays?: string[]) { - super({ - pubkey, + constructor(private pubkey: string, private relays?: string[]) { + this.signer = this.init(); + } + + async init(): Promise { + return new NConnectSigner({ + pubkey: this.pubkey, // TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list) - relay: Storages.pubsub, + relay: await Storages.pubsub(), signer: new AdminSigner(), timeout: 60000, }); - - this._pubkey = pubkey; } + async signEvent(event: Omit): Promise { + const signer = await this.signer; + return signer.signEvent(event); + } + + readonly nip04 = { + encrypt: async (pubkey: string, plaintext: string): Promise => { + const signer = await this.signer; + return signer.nip04.encrypt(pubkey, plaintext); + }, + + decrypt: async (pubkey: string, ciphertext: string): Promise => { + const signer = await this.signer; + return signer.nip04.decrypt(pubkey, ciphertext); + }, + }; + + readonly nip44 = { + encrypt: async (pubkey: string, plaintext: string): Promise => { + const signer = await this.signer; + return signer.nip44.encrypt(pubkey, plaintext); + }, + + decrypt: async (pubkey: string, ciphertext: string): Promise => { + const signer = await this.signer; + return signer.nip44.decrypt(pubkey, ciphertext); + }, + }; + // Prevent unnecessary NIP-46 round-trips. async getPublicKey(): Promise { - return this._pubkey; + return this.pubkey; } /** Get the user's relays if they passed in an `nprofile` auth token. */ diff --git a/src/storages/UserStore.ts b/src/storages/UserStore.ts index 1c7aaee2..c5657b6e 100644 --- a/src/storages/UserStore.ts +++ b/src/storages/UserStore.ts @@ -4,13 +4,7 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getTagSet } from '@/tags.ts'; export class UserStore implements NStore { - private store: NStore; - private pubkey: string; - - constructor(pubkey: string, store: NStore) { - this.pubkey = pubkey; - this.store = store; - } + constructor(private pubkey: string, private store: NStore) {} async event(event: NostrEvent, opts?: { signal?: AbortSignal }): Promise { return await this.store.event(event, opts); @@ -21,12 +15,11 @@ export class UserStore implements NStore { * https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists */ async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { - const allEvents = await this.store.query(filters, opts); + const events = await this.store.query(filters, opts); + const pubkeys = await this.getMutedPubkeys(); - const mutedPubkeys = await this.getMutedPubkeys(); - - return allEvents.filter((event) => { - return event.kind === 0 || mutedPubkeys.has(event.pubkey) === false; + return events.filter((event) => { + return event.kind === 0 || !pubkeys.has(event.pubkey); }); } diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index b55cd2b8..f5c70afe 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -17,7 +17,7 @@ Deno.test('hydrateEvents(): author --- WITHOUT stats', async () => { await hydrateEvents({ events: [event1], - storage: db, + store: db, }); const expectedEvent = { ...event1, author: event0 }; @@ -40,7 +40,7 @@ Deno.test('hydrateEvents(): repost --- WITHOUT stats', async () => { await hydrateEvents({ events: [event6], - storage: db, + store: db, }); const expectedEvent6 = { @@ -67,7 +67,7 @@ Deno.test('hydrateEvents(): quote repost --- WITHOUT stats', async () => { await hydrateEvents({ events: [event1quoteRepost], - storage: db, + store: db, }); const expectedEvent1quoteRepost = { @@ -95,7 +95,7 @@ Deno.test('hydrateEvents(): repost of quote repost --- WITHOUT stats', async () await hydrateEvents({ events: [event6], - storage: db, + store: db, }); const expectedEvent6 = { @@ -122,7 +122,7 @@ Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stat await hydrateEvents({ events: [reportEvent], - storage: db, + store: db, }); const expectedEvent: DittoEvent = { diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index e197ca8e..3109ac60 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -8,13 +8,13 @@ import { Conf } from '@/config.ts'; interface HydrateOpts { events: DittoEvent[]; - storage: NStore; + store: NStore; signal?: AbortSignal; } /** Hydrate events using the provided storage. */ async function hydrateEvents(opts: HydrateOpts): Promise { - const { events, storage, signal } = opts; + const { events, store, signal } = opts; if (!events.length) { return events; @@ -22,31 +22,31 @@ async function hydrateEvents(opts: HydrateOpts): Promise { const cache = [...events]; - for (const event of await gatherReposts({ events: cache, storage, signal })) { + for (const event of await gatherReposts({ events: cache, store, signal })) { cache.push(event); } - for (const event of await gatherReacted({ events: cache, storage, signal })) { + for (const event of await gatherReacted({ events: cache, store, signal })) { cache.push(event); } - for (const event of await gatherQuotes({ events: cache, storage, signal })) { + for (const event of await gatherQuotes({ events: cache, store, signal })) { cache.push(event); } - for (const event of await gatherAuthors({ events: cache, storage, signal })) { + for (const event of await gatherAuthors({ events: cache, store, signal })) { cache.push(event); } - for (const event of await gatherUsers({ events: cache, storage, signal })) { + for (const event of await gatherUsers({ events: cache, store, signal })) { cache.push(event); } - for (const event of await gatherReportedProfiles({ events: cache, storage, signal })) { + for (const event of await gatherReportedProfiles({ events: cache, store, signal })) { cache.push(event); } - for (const event of await gatherReportedNotes({ events: cache, storage, signal })) { + for (const event of await gatherReportedNotes({ events: cache, store, signal })) { cache.push(event); } @@ -123,7 +123,7 @@ function assembleEvents( } /** Collect reposts from the events. */ -function gatherReposts({ events, storage, signal }: HydrateOpts): Promise { +function gatherReposts({ events, store, signal }: HydrateOpts): Promise { const ids = new Set(); for (const event of events) { @@ -135,14 +135,14 @@ function gatherReposts({ events, storage, signal }: HydrateOpts): Promise { +function gatherReacted({ events, store, signal }: HydrateOpts): Promise { const ids = new Set(); for (const event of events) { @@ -154,14 +154,14 @@ function gatherReacted({ events, storage, signal }: HydrateOpts): Promise { +function gatherQuotes({ events, store, signal }: HydrateOpts): Promise { const ids = new Set(); for (const event of events) { @@ -173,34 +173,34 @@ function gatherQuotes({ events, storage, signal }: HydrateOpts): Promise { +function gatherAuthors({ events, store, signal }: HydrateOpts): Promise { const pubkeys = new Set(events.map((event) => event.pubkey)); - return storage.query( + return store.query( [{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }], { signal }, ); } /** Collect users from the events. */ -function gatherUsers({ events, storage, signal }: HydrateOpts): Promise { +function gatherUsers({ events, store, signal }: HydrateOpts): Promise { const pubkeys = new Set(events.map((event) => event.pubkey)); - return storage.query( + return store.query( [{ kinds: [30361], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }], { signal }, ); } /** Collect reported notes from the events. */ -function gatherReportedNotes({ events, storage, signal }: HydrateOpts): Promise { +function gatherReportedNotes({ events, store, signal }: HydrateOpts): Promise { const ids = new Set(); for (const event of events) { if (event.kind === 1984) { @@ -213,14 +213,14 @@ function gatherReportedNotes({ events, storage, signal }: HydrateOpts): Promise< } } - return storage.query( + return store.query( [{ kinds: [1], ids: [...ids], limit: ids.size }], { signal }, ); } /** Collect reported profiles from the events. */ -function gatherReportedProfiles({ events, storage, signal }: HydrateOpts): Promise { +function gatherReportedProfiles({ events, store, signal }: HydrateOpts): Promise { const pubkeys = new Set(); for (const event of events) { @@ -232,7 +232,7 @@ function gatherReportedProfiles({ events, storage, signal }: HydrateOpts): Promi } } - return storage.query( + return store.query( [{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }], { signal }, ); diff --git a/src/storages/pool-store.ts b/src/storages/pool-store.ts index 9f452058..54565091 100644 --- a/src/storages/pool-store.ts +++ b/src/storages/pool-store.ts @@ -13,6 +13,7 @@ import { RelayPoolWorker } from 'nostr-relaypool'; import { getFilterLimit, matchFilters } from 'nostr-tools'; import { Conf } from '@/config.ts'; +import { Storages } from '@/storages.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { abortError } from '@/utils/abort.ts'; import { getRelays } from '@/utils/outbox.ts'; @@ -35,7 +36,7 @@ class PoolStore implements NRelay { async event(event: NostrEvent, opts: { signal?: AbortSignal } = {}): Promise { if (opts.signal?.aborted) return Promise.reject(abortError()); - const relaySet = await getRelays(event.pubkey); + const relaySet = await getRelays(await Storages.db(), event.pubkey); relaySet.delete(Conf.relay); const relays = [...relaySet].slice(0, 4); diff --git a/src/storages/search-store.ts b/src/storages/search-store.ts index be6e2b44..4951c722 100644 --- a/src/storages/search-store.ts +++ b/src/storages/search-store.ts @@ -48,7 +48,7 @@ class SearchStore implements NStore { return hydrateEvents({ events, - storage: this.#hydrator, + store: this.#hydrator, signal: opts?.signal, }); } else { diff --git a/src/utils/api.ts b/src/utils/api.ts index 81b157ce..dceede7a 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -42,7 +42,7 @@ async function createEvent(t: EventStub, c: AppContext): Promise { /** Filter for fetching an existing event to update. */ interface UpdateEventFilter extends NostrFilter { kinds: [number]; - limit?: 1; + limit: 1; } /** Fetch existing event, update it, then publish the new event. */ @@ -51,7 +51,8 @@ async function updateEvent( fn: (prev: NostrEvent | undefined) => E, c: AppContext, ): Promise { - const [prev] = await Storages.db.query([filter], { limit: 1, signal: c.req.raw.signal }); + const store = await Storages.db(); + const [prev] = await store.query([filter], { signal: c.req.raw.signal }); return createEvent(fn(prev), c); } @@ -101,7 +102,8 @@ async function updateAdminEvent( fn: (prev: NostrEvent | undefined) => E, c: AppContext, ): Promise { - const [prev] = await Storages.db.query([filter], { limit: 1, signal: c.req.raw.signal }); + const store = await Storages.db(); + const [prev] = await store.query([filter], { limit: 1, signal: c.req.raw.signal }); return createAdminEvent(fn(prev), c); } @@ -110,7 +112,8 @@ async function publishEvent(event: NostrEvent, c: AppContext): Promise { const uri = new URL('nostrconnect://'); - const { name, tagline } = await getInstanceMetadata(signal); + const { name, tagline } = await getInstanceMetadata(await Storages.db(), signal); const metadata: ConnectMetadata = { name, diff --git a/src/utils/instance.ts b/src/utils/instance.ts index 004e4cf1..386c796f 100644 --- a/src/utils/instance.ts +++ b/src/utils/instance.ts @@ -1,8 +1,7 @@ -import { NostrEvent, NostrMetadata, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; import { serverMetaSchema } from '@/schemas/nostr.ts'; -import { Storages } from '@/storages.ts'; /** Like NostrMetadata, but some fields are required and also contains some extra fields. */ export interface InstanceMetadata extends NostrMetadata { @@ -14,8 +13,8 @@ export interface InstanceMetadata extends NostrMetadata { } /** Get and parse instance metadata from the kind 0 of the admin user. */ -export async function getInstanceMetadata(signal?: AbortSignal): Promise { - const [event] = await Storages.db.query( +export async function getInstanceMetadata(store: NStore, signal?: AbortSignal): Promise { + const [event] = await store.query( [{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }, ); diff --git a/src/utils/nip05.ts b/src/utils/nip05.ts index 0b4c6e39..eaab6edf 100644 --- a/src/utils/nip05.ts +++ b/src/utils/nip05.ts @@ -1,4 +1,4 @@ -import { NIP05 } from '@nostrify/nostrify'; +import { NIP05, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { nip19 } from 'nostr-tools'; @@ -16,7 +16,8 @@ const nip05Cache = new SimpleLRU( const [name, domain] = key.split('@'); try { if (domain === Conf.url.host) { - const pointer = await localNip05Lookup(name); + const store = await Storages.db(); + const pointer = await localNip05Lookup(store, name); if (pointer) { debug(`Found: ${key} is ${pointer.pubkey}`); return pointer; @@ -36,8 +37,8 @@ const nip05Cache = new SimpleLRU( { max: 500, ttl: Time.hours(1) }, ); -async function localNip05Lookup(name: string): Promise { - const [label] = await Storages.db.query([{ +async function localNip05Lookup(store: NStore, name: string): Promise { + const [label] = await store.query([{ kinds: [1985], authors: [Conf.pubkey], '#L': ['nip05'], diff --git a/src/utils/outbox.ts b/src/utils/outbox.ts index 13edaf69..72b83388 100644 --- a/src/utils/outbox.ts +++ b/src/utils/outbox.ts @@ -1,10 +1,11 @@ -import { Conf } from '@/config.ts'; -import { Storages } from '@/storages.ts'; +import { NStore } from '@nostrify/nostrify'; -export async function getRelays(pubkey: string): Promise> { +import { Conf } from '@/config.ts'; + +export async function getRelays(store: NStore, pubkey: string): Promise> { const relays = new Set<`wss://${string}`>(); - const events = await Storages.db.query([ + const events = await store.query([ { kinds: [10002], authors: [pubkey, Conf.pubkey], limit: 2 }, ]); diff --git a/src/views.ts b/src/views.ts index 451ce140..a7375429 100644 --- a/src/views.ts +++ b/src/views.ts @@ -12,15 +12,16 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal return c.json([]); } - const events = await Storages.db.query(filters, { signal }); + const store = await Storages.db(); + const events = await store.query(filters, { signal }); const pubkeys = new Set(events.map(({ pubkey }) => pubkey)); if (!pubkeys.size) { return c.json([]); } - const authors = await Storages.db.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) - .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); + const authors = await store.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) + .then((events) => hydrateEvents({ events, store, signal })); const accounts = await Promise.all( authors.map((event) => renderAccount(event)), @@ -32,8 +33,10 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal async function renderAccounts(c: AppContext, authors: string[], signal = AbortSignal.timeout(1000)) { const { since, until, limit } = paginationSchema.parse(c.req.query()); - const events = await Storages.db.query([{ kinds: [0], authors, since, until, limit }], { signal }) - .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); + const store = await Storages.db(); + + const events = await store.query([{ kinds: [0], authors, since, until, limit }], { signal }) + .then((events) => hydrateEvents({ events, store, signal })); const accounts = await Promise.all( events.map((event) => renderAccount(event)), @@ -48,10 +51,11 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal return c.json([]); } + const store = await Storages.db(); const { limit } = paginationSchema.parse(c.req.query()); - const events = await Storages.db.query([{ kinds: [1], ids, limit }], { signal }) - .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); + const events = await store.query([{ kinds: [1], ids, limit }], { signal }) + .then((events) => hydrateEvents({ events, store, signal })); if (!events.length) { return c.json([]); diff --git a/src/views/mastodon/relationships.ts b/src/views/mastodon/relationships.ts index d358024f..2f8ffdde 100644 --- a/src/views/mastodon/relationships.ts +++ b/src/views/mastodon/relationships.ts @@ -2,7 +2,9 @@ import { Storages } from '@/storages.ts'; import { hasTag } from '@/tags.ts'; async function renderRelationship(sourcePubkey: string, targetPubkey: string) { - const events = await Storages.db.query([ + const db = await Storages.db(); + + const events = await db.query([ { kinds: [3], authors: [sourcePubkey], limit: 1 }, { kinds: [3], authors: [targetPubkey], limit: 1 }, { kinds: [10000], authors: [sourcePubkey], limit: 1 }, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index d00759db..776f0169 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -22,7 +22,7 @@ interface RenderStatusOpts { async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise { const { viewerPubkey, depth = 1 } = opts; - if (depth > 2 || depth < 0) return null; + if (depth > 2 || depth < 0) return; const note = nip19.noteEncode(event.id); @@ -40,7 +40,10 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< ), ]; - const mentionedProfiles = await Storages.optimizer.query( + const db = await Storages.db(); + const optimizer = await Storages.optimizer(); + + const mentionedProfiles = await optimizer.query( [{ kinds: [0], authors: mentionedPubkeys, limit: mentionedPubkeys.length }], ); @@ -53,7 +56,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< ), firstUrl ? unfurlCardCached(firstUrl) : null, viewerPubkey - ? await Storages.db.query([ + ? await db.query([ { kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, { kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, { kinds: [9734], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, From a4226a963f4a398c6db9ee00b43d0b56f2d0b857 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 18:44:42 -0500 Subject: [PATCH 167/252] Rework Kysely db to be async --- deno.json | 1 + src/db.ts | 41 ------------------------- src/db/DittoDB.ts | 55 ++++++++++++++++++++++++++++++++-- src/db/unattached-media.ts | 27 ++++++++++------- src/pipeline.ts | 8 +++-- src/stats.ts | 23 +++++++------- src/storages.ts | 8 +++-- src/storages/events-db.test.ts | 10 ++++--- src/storages/hydrate.ts | 12 ++++---- 9 files changed, 106 insertions(+), 79 deletions(-) delete mode 100644 src/db.ts diff --git a/deno.json b/deno.json index 02255105..12fbb574 100644 --- a/deno.json +++ b/deno.json @@ -24,6 +24,7 @@ "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", + "@std/assert": "jsr:@std/assert@^0.225.1", "@std/cli": "jsr:@std/cli@^0.223.0", "@std/crypto": "jsr:@std/crypto@^0.224.0", "@std/dotenv": "jsr:@std/dotenv@^0.224.0", diff --git a/src/db.ts b/src/db.ts deleted file mode 100644 index 1a5f06d8..00000000 --- a/src/db.ts +++ /dev/null @@ -1,41 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; - -import { FileMigrationProvider, Migrator } from 'kysely'; - -import { DittoDB } from '@/db/DittoDB.ts'; - -const db = await DittoDB.getInstance(); - -const migrator = new Migrator({ - db, - provider: new FileMigrationProvider({ - fs, - path, - migrationFolder: new URL(import.meta.resolve('./db/migrations')).pathname, - }), -}); - -/** Migrate the database to the latest version. */ -async function migrate() { - console.info('Running migrations...'); - const results = await migrator.migrateToLatest(); - - if (results.error) { - console.error(results.error); - Deno.exit(1); - } else { - if (!results.results?.length) { - console.info('Everything up-to-date.'); - } else { - console.info('Migrations finished!'); - for (const { migrationName, status } of results.results!) { - console.info(` - ${migrationName}: ${status}`); - } - } - } -} - -await migrate(); - -export { db }; diff --git a/src/db/DittoDB.ts b/src/db/DittoDB.ts index abe068b8..9c3b280b 100644 --- a/src/db/DittoDB.ts +++ b/src/db/DittoDB.ts @@ -1,4 +1,7 @@ -import { Kysely } from 'kysely'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import { FileMigrationProvider, Kysely, Migrator } from 'kysely'; import { Conf } from '@/config.ts'; import { DittoPostgres } from '@/db/adapters/DittoPostgres.ts'; @@ -6,17 +9,63 @@ import { DittoSQLite } from '@/db/adapters/DittoSQLite.ts'; import { DittoTables } from '@/db/DittoTables.ts'; export class DittoDB { + private static kysely: Promise> | undefined; + static getInstance(): Promise> { + if (!this.kysely) { + this.kysely = this._getInstance(); + } + return this.kysely; + } + + static async _getInstance(): Promise> { const { databaseUrl } = Conf; + let kysely: Kysely; + switch (databaseUrl.protocol) { case 'sqlite:': - return DittoSQLite.getInstance(); + kysely = await DittoSQLite.getInstance(); + break; case 'postgres:': case 'postgresql:': - return DittoPostgres.getInstance(); + kysely = await DittoPostgres.getInstance(); + break; default: throw new Error('Unsupported database URL.'); } + + await this.migrate(kysely); + + return kysely; + } + + /** Migrate the database to the latest version. */ + private static async migrate(kysely: Kysely) { + const migrator = new Migrator({ + db: kysely, + provider: new FileMigrationProvider({ + fs, + path, + migrationFolder: new URL(import.meta.resolve('../db/migrations')).pathname, + }), + }); + + console.info('Running migrations...'); + const results = await migrator.migrateToLatest(); + + if (results.error) { + console.error(results.error); + Deno.exit(1); + } else { + if (!results.results?.length) { + console.info('Everything up-to-date.'); + } else { + console.info('Migrations finished!'); + for (const { migrationName, status } of results.results!) { + console.info(` - ${migrationName}: ${status}`); + } + } + } } } diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 960abe8c..708e2f96 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -1,6 +1,6 @@ import uuid62 from 'uuid62'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { type MediaData } from '@/schemas/nostr.ts'; interface UnattachedMedia { @@ -19,7 +19,8 @@ async function insertUnattachedMedia(media: Omit { if (!urls.length) return; - await db.deleteFrom('unattached_media') + const kysely = await DittoDB.getInstance(); + await kysely.deleteFrom('unattached_media') .where('pubkey', '=', pubkey) .where('url', 'in', urls) .execute(); diff --git a/src/pipeline.ts b/src/pipeline.ts index 9742b492..48e5c384 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -5,7 +5,7 @@ import Debug from '@soapbox/stickynotes/debug'; import { sql } from 'kysely'; import { Conf } from '@/config.ts'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isEphemeralKind } from '@/kinds.ts'; @@ -91,7 +91,8 @@ async function encounterEvent(event: NostrEvent, signal: AbortSignal): Promise { await hydrateEvents({ events: [event], store: await Storages.db(), signal }); - const domain = await db + const kysely = await DittoDB.getInstance(); + const domain = await kysely .selectFrom('pubkey_domains') .select('domain') .where('pubkey', '=', event.pubkey) @@ -140,6 +141,7 @@ async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise pubkey_domains.last_updated_at - `.execute(db); + `.execute(kysely); } catch (_e) { // do nothing } diff --git a/src/stats.ts b/src/stats.ts index bff2aebc..f8efe16c 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -2,7 +2,7 @@ import { NKinds, NostrEvent } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { InsertQueryBuilder } from 'kysely'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; @@ -25,7 +25,7 @@ async function updateStats(event: NostrEvent) { if (event.kind === 3) { prev = await getPrevEvent(event); if (!prev || event.created_at >= prev.created_at) { - queries.push(updateFollowingCountQuery(event)); + queries.push(await updateFollowingCountQuery(event)); } } @@ -37,8 +37,8 @@ async function updateStats(event: NostrEvent) { debug(JSON.stringify({ id: event.id, pubkey: event.pubkey, kind: event.kind, tags: event.tags, statDiffs })); } - if (pubkeyDiffs.length) queries.push(authorStatsQuery(pubkeyDiffs)); - if (eventDiffs.length) queries.push(eventStatsQuery(eventDiffs)); + if (pubkeyDiffs.length) queries.push(await authorStatsQuery(pubkeyDiffs)); + if (eventDiffs.length) queries.push(await eventStatsQuery(eventDiffs)); if (queries.length) { await Promise.all(queries.map((query) => query.execute())); @@ -102,7 +102,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr } /** Create an author stats query from the list of diffs. */ -function authorStatsQuery(diffs: AuthorStatDiff[]) { +async function authorStatsQuery(diffs: AuthorStatDiff[]) { const values: DittoTables['author_stats'][] = diffs.map(([_, pubkey, stat, diff]) => { const row: DittoTables['author_stats'] = { pubkey, @@ -114,7 +114,8 @@ function authorStatsQuery(diffs: AuthorStatDiff[]) { return row; }); - return db.insertInto('author_stats') + const kysely = await DittoDB.getInstance(); + return kysely.insertInto('author_stats') .values(values) .onConflict((oc) => oc @@ -128,7 +129,7 @@ function authorStatsQuery(diffs: AuthorStatDiff[]) { } /** Create an event stats query from the list of diffs. */ -function eventStatsQuery(diffs: EventStatDiff[]) { +async function eventStatsQuery(diffs: EventStatDiff[]) { const values: DittoTables['event_stats'][] = diffs.map(([_, event_id, stat, diff]) => { const row: DittoTables['event_stats'] = { event_id, @@ -140,7 +141,8 @@ function eventStatsQuery(diffs: EventStatDiff[]) { return row; }); - return db.insertInto('event_stats') + const kysely = await DittoDB.getInstance(); + return kysely.insertInto('event_stats') .values(values) .onConflict((oc) => oc @@ -167,14 +169,15 @@ async function getPrevEvent(event: NostrEvent): Promise } /** Set the following count to the total number of unique "p" tags in the follow list. */ -function updateFollowingCountQuery({ pubkey, tags }: NostrEvent) { +async function updateFollowingCountQuery({ pubkey, tags }: NostrEvent) { const following_count = new Set( tags .filter(([name]) => name === 'p') .map(([_, value]) => value), ).size; - return db.insertInto('author_stats') + const kysely = await DittoDB.getInstance(); + return kysely.insertInto('author_stats') .values({ pubkey, following_count, diff --git a/src/storages.ts b/src/storages.ts index cbda925d..f591e111 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -1,7 +1,8 @@ // deno-lint-ignore-file require-await import { NCache } from '@nostrify/nostrify'; + import { Conf } from '@/config.ts'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { EventsDB } from '@/storages/events-db.ts'; import { Optimizer } from '@/storages/optimizer.ts'; import { PoolStore } from '@/storages/pool-store.ts'; @@ -24,7 +25,10 @@ export class Storages { /** SQLite database to store events this Ditto server cares about. */ public static async db(): Promise { if (!this._db) { - this._db = Promise.resolve(new EventsDB(db)); + this._db = (async () => { + const kysely = await DittoDB.getInstance(); + return new EventsDB(kysely); + })(); } return this._db; } diff --git a/src/storages/events-db.test.ts b/src/storages/events-db.test.ts index dd92c1b4..d935e6bb 100644 --- a/src/storages/events-db.test.ts +++ b/src/storages/events-db.test.ts @@ -1,12 +1,14 @@ -import { db } from '@/db.ts'; -import { assertEquals, assertRejects } from '@/deps-test.ts'; +import { assertEquals, assertRejects } from '@std/assert'; + +import { DittoDB } from '@/db/DittoDB.ts'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; import { EventsDB } from '@/storages/events-db.ts'; -const eventsDB = new EventsDB(db); +const kysely = await DittoDB.getInstance(); +const eventsDB = new EventsDB(kysely); Deno.test('count filters', async () => { assertEquals((await eventsDB.count([{ kinds: [1] }])).count, 0); @@ -34,7 +36,7 @@ Deno.test('query events with domain search filter', async () => { assertEquals(await eventsDB.query([{ search: 'domain:localhost:8000' }]), []); assertEquals(await eventsDB.query([{ search: '' }]), [event1]); - await db + await kysely .insertInto('pubkey_domains') .values({ pubkey: event1.pubkey, domain: 'localhost:8000', last_updated_at: event1.created_at }) .execute(); diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 3109ac60..8d2d3027 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -1,7 +1,7 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; import { matchFilter } from 'nostr-tools'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Conf } from '@/config.ts'; @@ -239,7 +239,7 @@ function gatherReportedProfiles({ events, store, signal }: HydrateOpts): Promise } /** Collect author stats from the events. */ -function gatherAuthorStats(events: DittoEvent[]): Promise { +async function gatherAuthorStats(events: DittoEvent[]): Promise { const pubkeys = new Set( events .filter((event) => event.kind === 0) @@ -250,7 +250,8 @@ function gatherAuthorStats(events: DittoEvent[]): Promise { +async function gatherEventStats(events: DittoEvent[]): Promise { const ids = new Set( events .filter((event) => event.kind === 1) @@ -269,7 +270,8 @@ function gatherEventStats(events: DittoEvent[]): Promise Date: Tue, 14 May 2024 18:46:55 -0500 Subject: [PATCH 168/252] Add missing nostr-relaypool import --- src/storages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/storages.ts b/src/storages.ts index f591e111..1cf06c11 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -1,5 +1,6 @@ // deno-lint-ignore-file require-await import { NCache } from '@nostrify/nostrify'; +import { RelayPoolWorker } from 'nostr-relaypool'; import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; From 99a6c668c8844c30500f437986857f7a386ca3c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 18:50:05 -0500 Subject: [PATCH 169/252] Update recompute script --- scripts/stats-recompute.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/stats-recompute.ts b/scripts/stats-recompute.ts index 9d204c7f..dcb0bc07 100644 --- a/scripts/stats-recompute.ts +++ b/scripts/stats-recompute.ts @@ -1,6 +1,6 @@ import { nip19 } from 'nostr-tools'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Storages } from '@/storages.ts'; @@ -17,16 +17,19 @@ try { Deno.exit(1); } -const [followList] = await Storages.db.query([{ kinds: [3], authors: [pubkey], limit: 1 }]); +const store = await Storages.db(); +const kysely = await DittoDB.getInstance(); + +const [followList] = await store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]); const authorStats: DittoTables['author_stats'] = { pubkey, - followers_count: (await Storages.db.count([{ kinds: [3], '#p': [pubkey] }])).count, + followers_count: (await store.count([{ kinds: [3], '#p': [pubkey] }])).count, following_count: followList?.tags.filter(([name]) => name === 'p')?.length ?? 0, - notes_count: (await Storages.db.count([{ kinds: [1], authors: [pubkey] }])).count, + notes_count: (await store.count([{ kinds: [1], authors: [pubkey] }])).count, }; -await db.insertInto('author_stats') +await kysely.insertInto('author_stats') .values(authorStats) .onConflict((oc) => oc From 3d1d56355d9b511be1031a136d5c90989db12df2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 18:51:35 -0500 Subject: [PATCH 170/252] Update scripts for async db --- scripts/admin-event.ts | 5 +++-- scripts/admin-role.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/admin-event.ts b/scripts/admin-event.ts index a9939adf..29d3ae2e 100644 --- a/scripts/admin-event.ts +++ b/scripts/admin-event.ts @@ -1,7 +1,7 @@ import { JsonParseStream } from '@std/json/json-parse-stream'; import { TextLineStream } from '@std/streams/text-line-stream'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { EventsDB } from '@/storages/events-db.ts'; import { type EventStub } from '@/utils/api.ts'; @@ -9,7 +9,8 @@ import { nostrNow } from '@/utils.ts'; const signer = new AdminSigner(); -const eventsDB = new EventsDB(db); +const kysely = await DittoDB.getInstance(); +const eventsDB = new EventsDB(kysely); const readable = Deno.stdin.readable .pipeThrough(new TextDecoderStream()) diff --git a/scripts/admin-role.ts b/scripts/admin-role.ts index 4fa212e7..57a17e37 100644 --- a/scripts/admin-role.ts +++ b/scripts/admin-role.ts @@ -1,12 +1,13 @@ import { NSchema } from '@nostrify/nostrify'; -import { db } from '@/db.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { Conf } from '@/config.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { EventsDB } from '@/storages/events-db.ts'; import { nostrNow } from '@/utils.ts'; -const eventsDB = new EventsDB(db); +const kysely = await DittoDB.getInstance(); +const eventsDB = new EventsDB(kysely); const [pubkey, role] = Deno.args; From d3a7f0849f0026ed40f18b28bee145d01fa98c6e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 19:02:15 -0500 Subject: [PATCH 171/252] deno lint --- src/controllers/api/statuses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index a52a4088..98173b0b 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import ISO6391 from 'iso-639-1'; import { z } from 'zod'; From 477ee8b124f2fddd12e3e0d8c44cd453837ad350 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 19:09:25 -0500 Subject: [PATCH 172/252] Fix hydrateEvents in streaming --- src/controllers/api/streaming.ts | 7 ++++--- src/controllers/api/timelines.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index e79c51eb..1de7bbf7 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -68,15 +68,16 @@ const streamingController: AppController = (c) => { if (!filter) return; try { - const store = await Storages.pubsub(); + const db = await Storages.db(); + const pubsub = await Storages.pubsub(); - for await (const msg of store.req([filter], { signal: controller.signal })) { + for await (const msg of pubsub.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { const event = msg[2]; await hydrateEvents({ events: [event], - store, + store: db, signal: AbortSignal.timeout(1000), }); diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index e83c50cd..8ea66baf 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -62,7 +62,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { return renderReblog(event, { viewerPubkey }); } return renderStatus(event, { viewerPubkey }); - }))).filter((boolean) => boolean); + }))).filter(Boolean); if (!statuses.length) { return c.json([]); From 2fd50261f9b1032ae64614d49a410302eb9c8074 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 19:11:38 -0500 Subject: [PATCH 173/252] streaming: actually hydrate with optimizer --- src/controllers/api/streaming.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 1de7bbf7..ff47c07d 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -68,8 +68,8 @@ const streamingController: AppController = (c) => { if (!filter) return; try { - const db = await Storages.db(); const pubsub = await Storages.pubsub(); + const optimizer = await Storages.optimizer(); for await (const msg of pubsub.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { @@ -77,7 +77,7 @@ const streamingController: AppController = (c) => { await hydrateEvents({ events: [event], - store: db, + store: optimizer, signal: AbortSignal.timeout(1000), }); From 4d342dff4a48ba68493dbce9de27a5bcdbc2dd28 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 14 May 2024 21:14:00 -0300 Subject: [PATCH 174/252] fix(streaming): move get muted users logic before upgrading connection to web socket --- src/controllers/api/streaming.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index fbe825be..310b5295 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -50,6 +50,16 @@ const streamingController: AppController = async (c) => { return c.json({ error: 'Invalid access token' }, 401); } + const mutedUsersSet = new Set(); + if (pubkey) { + const [mutedUsers] = await Storages.admin.query([{ authors: [pubkey], kinds: [10000], limit: 1 }], { signal }); + if (mutedUsers) { + for (const pubkey of getTagSet(mutedUsers.tags, 'p')) { + mutedUsersSet.add(pubkey); + } + } + } + const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token }); function send(name: string, payload: object) { @@ -63,16 +73,6 @@ const streamingController: AppController = async (c) => { } } - const mutedUsersSet = new Set(); - if (pubkey) { - const [mutedUsers] = await Storages.admin.query([{ authors: [pubkey], kinds: [10000], limit: 1 }], { signal }); - if (mutedUsers) { - for (const pubkey of getTagSet(mutedUsers.tags, 'p')) { - mutedUsersSet.add(pubkey); - } - } - } - socket.onopen = async () => { if (!stream) return; From f163af55d86f00f19f5de47b4c5090bd5cd700a1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 19:19:57 -0500 Subject: [PATCH 175/252] Remove deps-test.ts --- src/deps-test.ts | 1 - src/filter.test.ts | 2 +- src/policies/MuteListPolicy.test.ts | 2 +- src/storages/UserStore.test.ts | 2 +- src/storages/hydrate.test.ts | 2 +- src/tags.test.ts | 2 +- src/utils/expiring-cache.test.ts | 2 +- src/utils/time.test.ts | 2 +- src/workers/fetch.test.ts | 2 +- src/workers/trends.test.ts | 2 +- 10 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 src/deps-test.ts diff --git a/src/deps-test.ts b/src/deps-test.ts deleted file mode 100644 index 3e6da88e..00000000 --- a/src/deps-test.ts +++ /dev/null @@ -1 +0,0 @@ -export { assert, assertEquals, assertRejects, assertThrows } from 'https://deno.land/std@0.198.0/assert/mod.ts'; diff --git a/src/filter.test.ts b/src/filter.test.ts index 9983b5a0..9379208e 100644 --- a/src/filter.test.ts +++ b/src/filter.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; diff --git a/src/policies/MuteListPolicy.test.ts b/src/policies/MuteListPolicy.test.ts index 2c3baa3d..89d7d993 100644 --- a/src/policies/MuteListPolicy.test.ts +++ b/src/policies/MuteListPolicy.test.ts @@ -1,6 +1,6 @@ import { MockRelay } from '@nostrify/nostrify/test'; -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import { UserStore } from '@/storages/UserStore.ts'; import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; diff --git a/src/storages/UserStore.test.ts b/src/storages/UserStore.test.ts index b1955bd9..d04ece07 100644 --- a/src/storages/UserStore.test.ts +++ b/src/storages/UserStore.test.ts @@ -1,6 +1,6 @@ import { MockRelay } from '@nostrify/nostrify/test'; -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import { UserStore } from '@/storages/UserStore.ts'; import userBlack from '~/fixtures/events/kind-0-black.json' with { type: 'json' }; diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index f5c70afe..1edafd7e 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { MockRelay } from '@nostrify/nostrify/test'; diff --git a/src/tags.test.ts b/src/tags.test.ts index c4d32143..e49d31ab 100644 --- a/src/tags.test.ts +++ b/src/tags.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import { addTag, deleteTag, getTagSet } from './tags.ts'; diff --git a/src/utils/expiring-cache.test.ts b/src/utils/expiring-cache.test.ts index 9827de8b..8c6d7b18 100644 --- a/src/utils/expiring-cache.test.ts +++ b/src/utils/expiring-cache.test.ts @@ -1,4 +1,4 @@ -import { assert } from '@/deps-test.ts'; +import { assert } from '@std/assert'; import ExpiringCache from './expiring-cache.ts'; diff --git a/src/utils/time.test.ts b/src/utils/time.test.ts index c167cafe..f820a1bf 100644 --- a/src/utils/time.test.ts +++ b/src/utils/time.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import { generateDateRange } from './time.ts'; diff --git a/src/workers/fetch.test.ts b/src/workers/fetch.test.ts index e7283b75..d657d1f5 100644 --- a/src/workers/fetch.test.ts +++ b/src/workers/fetch.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertRejects } from '@/deps-test.ts'; +import { assertEquals, assertRejects } from '@std/assert'; import { fetchWorker } from '@/workers/fetch.ts'; diff --git a/src/workers/trends.test.ts b/src/workers/trends.test.ts index ef51f23e..ca1646e1 100644 --- a/src/workers/trends.test.ts +++ b/src/workers/trends.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from '@/deps-test.ts'; +import { assertEquals } from '@std/assert'; import { TrendsWorker } from './trends.ts'; From 0383726663886534b2020acd1c97dd6d9a95c157 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 14 May 2024 21:44:19 -0300 Subject: [PATCH 176/252] fix(streaming): use policy instead of hand coding --- src/controllers/api/streaming.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 310b5295..8fa23ea4 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -9,7 +9,7 @@ import { bech32ToPubkey } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; -import { getTagSet } from '@/tags.ts'; +import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; const debug = Debug('ditto:streaming'); @@ -34,12 +34,11 @@ const streamSchema = z.enum([ type Stream = z.infer; -const streamingController: AppController = async (c) => { +const streamingController: AppController = (c) => { const upgrade = c.req.header('upgrade'); const token = c.req.header('sec-websocket-protocol'); const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream')); const controller = new AbortController(); - const signal = c.req.raw.signal; if (upgrade?.toLowerCase() !== 'websocket') { return c.text('Please use websocket protocol', 400); @@ -50,16 +49,6 @@ const streamingController: AppController = async (c) => { return c.json({ error: 'Invalid access token' }, 401); } - const mutedUsersSet = new Set(); - if (pubkey) { - const [mutedUsers] = await Storages.admin.query([{ authors: [pubkey], kinds: [10000], limit: 1 }], { signal }); - if (mutedUsers) { - for (const pubkey of getTagSet(mutedUsers.tags, 'p')) { - mutedUsersSet.add(pubkey); - } - } - } - const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token }); function send(name: string, payload: object) { @@ -84,8 +73,12 @@ const streamingController: AppController = async (c) => { if (msg[0] === 'EVENT') { const event = msg[2]; - if (mutedUsersSet.has(event.pubkey)) { - continue; + if (pubkey) { + const policy = new MuteListPolicy(pubkey, Storages.admin); + const ok = await policy.call(event); + if (ok[2] === false) { + continue; + } } await hydrateEvents({ From a1326dedcc303d1ddb1d36b6e91c0fff41d7dde1 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 14 May 2024 21:53:50 -0300 Subject: [PATCH 177/252] fix(streaming): async storage --- src/controllers/api/streaming.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index f1e2b4ef..04cfbbc4 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -77,7 +77,7 @@ const streamingController: AppController = (c) => { const event = msg[2]; if (pubkey) { - const policy = new MuteListPolicy(pubkey, Storages.admin); + const policy = new MuteListPolicy(pubkey, await Storages.admin()); const ok = await policy.call(event); if (ok[2] === false) { continue; From af9fb6aaa30eb4abd27ebe815478276125fa84fe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 20:05:38 -0500 Subject: [PATCH 178/252] Sort imports of streaming.ts --- src/RelayError.ts | 2 +- src/controllers/api/streaming.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/RelayError.ts b/src/RelayError.ts index 1d275f63..0b01de3e 100644 --- a/src/RelayError.ts +++ b/src/RelayError.ts @@ -16,7 +16,7 @@ export class RelayError extends Error { /** Throw a new RelayError if the OK message is false. */ static assert(msg: NostrRelayOK): void { - const [_, _eventId, ok, reason] = msg; + const [, , ok, reason] = msg; if (!ok) { throw RelayError.fromReason(reason); } diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 04cfbbc4..e3852d97 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -4,12 +4,12 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; import { getFeedPubkeys } from '@/queries.ts'; -import { bech32ToPubkey } from '@/utils.ts'; -import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; -import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; +import { bech32ToPubkey } from '@/utils.ts'; +import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const debug = Debug('ditto:streaming'); @@ -78,8 +78,8 @@ const streamingController: AppController = (c) => { if (pubkey) { const policy = new MuteListPolicy(pubkey, await Storages.admin()); - const ok = await policy.call(event); - if (ok[2] === false) { + const [, , ok] = await policy.call(event); + if (!ok) { continue; } } From b3985e740b3283410dd4d68e77b73c78868a5a1b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 20:54:09 -0500 Subject: [PATCH 179/252] EventsDB: migrate tables to match NDatabase --- src/db/DittoTables.ts | 12 ++--- src/db/migrations/019_ndatabase_schema.ts | 25 ++++++++++ src/storages/events-db.ts | 60 +++++++++++------------ 3 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 src/db/migrations/019_ndatabase_schema.ts diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index d71f48ad..77e0b807 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -1,7 +1,7 @@ export interface DittoTables { - events: EventRow; - events_fts: EventFTSRow; - tags: TagRow; + nostr_events: EventRow; + nostr_tags: TagRow; + nostr_fts5: EventFTSRow; unattached_media: UnattachedMediaRow; author_stats: AuthorStatsRow; event_stats: EventStatsRow; @@ -34,14 +34,14 @@ interface EventRow { } interface EventFTSRow { - id: string; + event_id: string; content: string; } interface TagRow { - tag: string; - value: string; event_id: string; + name: string; + value: string; } interface UnattachedMediaRow { diff --git a/src/db/migrations/019_ndatabase_schema.ts b/src/db/migrations/019_ndatabase_schema.ts new file mode 100644 index 00000000..94378f00 --- /dev/null +++ b/src/db/migrations/019_ndatabase_schema.ts @@ -0,0 +1,25 @@ +import { Kysely, sql } from 'kysely'; + +import { Conf } from '@/config.ts'; + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('events').renameTo('nostr_events').execute(); + await db.schema.alterTable('tags').renameTo('nostr_tags').execute(); + await db.schema.alterTable('nostr_tags').renameColumn('tag', 'name').execute(); + + if (Conf.databaseUrl.protocol === 'sqlite:') { + await db.schema.dropTable('events_fts').execute(); + await sql`CREATE VIRTUAL TABLE nostr_fts5 USING fts5(event_id, content)`.execute(db); + } +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('nostr_events').renameTo('events').execute(); + await db.schema.alterTable('nostr_tags').renameTo('tags').execute(); + await db.schema.alterTable('tags').renameColumn('name', 'tag').execute(); + + if (Conf.databaseUrl.protocol === 'sqlite:') { + await db.schema.dropTable('nostr_fts5').execute(); + await sql`CREATE VIRTUAL TABLE events_fts USING fts5(id, content)`.execute(db); + } +} diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index de1ec4a2..19caf5a3 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -35,7 +35,7 @@ const tagConditions: Record = { 'role': ({ event, count }) => event.kind === 30361 && count === 0, }; -type EventQuery = SelectQueryBuilder { /** Insert the event into the database. */ async function addEvent() { - await trx.insertInto('events') + await trx.insertInto('nostr_events') .values({ ...event, tags: JSON.stringify(event.tags) }) .execute(); } @@ -91,18 +91,18 @@ class EventsDB implements NStore { if (protocol !== 'sqlite:') return; const searchContent = buildSearchContent(event); if (!searchContent) return; - await trx.insertInto('events_fts') - .values({ id: event.id, content: searchContent.substring(0, 1000) }) + await trx.insertInto('nostr_fts5') + .values({ event_id: event.id, content: searchContent.substring(0, 1000) }) .execute(); } /** Index event tags depending on the conditions defined above. */ async function indexTags() { const tags = filterIndexableTags(event); - const rows = tags.map(([tag, value]) => ({ event_id: event.id, tag, value })); + const rows = tags.map(([name, value]) => ({ event_id: event.id, name, value })); if (!tags.length) return; - await trx.insertInto('tags') + await trx.insertInto('nostr_tags') .values(rows) .execute(); } @@ -150,17 +150,17 @@ class EventsDB implements NStore { /** Build the query for a filter. */ getFilterQuery(db: Kysely, filter: NostrFilter): EventQuery { let query = db - .selectFrom('events') + .selectFrom('nostr_events') .select([ - 'events.id', - 'events.kind', - 'events.pubkey', - 'events.content', - 'events.tags', - 'events.created_at', - 'events.sig', + 'nostr_events.id', + 'nostr_events.kind', + 'nostr_events.pubkey', + 'nostr_events.content', + 'nostr_events.tags', + 'nostr_events.created_at', + 'nostr_events.sig', ]) - .where('events.deleted_at', 'is', null); + .where('nostr_events.deleted_at', 'is', null); /** Whether we are querying for replaceable events by author. */ const isAddrQuery = filter.authors && @@ -169,7 +169,7 @@ class EventsDB implements NStore { // Avoid ORDER BY when querying for replaceable events by author. if (!isAddrQuery) { - query = query.orderBy('events.created_at', 'desc'); + query = query.orderBy('nostr_events.created_at', 'desc'); } for (const [key, value] of Object.entries(filter)) { @@ -177,19 +177,19 @@ class EventsDB implements NStore { switch (key as keyof NostrFilter) { case 'ids': - query = query.where('events.id', 'in', filter.ids!); + query = query.where('nostr_events.id', 'in', filter.ids!); break; case 'kinds': - query = query.where('events.kind', 'in', filter.kinds!); + query = query.where('nostr_events.kind', 'in', filter.kinds!); break; case 'authors': - query = query.where('events.pubkey', 'in', filter.authors!); + query = query.where('nostr_events.pubkey', 'in', filter.authors!); break; case 'since': - query = query.where('events.created_at', '>=', filter.since!); + query = query.where('nostr_events.created_at', '>=', filter.since!); break; case 'until': - query = query.where('events.created_at', '<=', filter.until!); + query = query.where('nostr_events.created_at', '<=', filter.until!); break; case 'limit': query = query.limit(filter.limit!); @@ -197,21 +197,21 @@ class EventsDB implements NStore { } } - const joinedQuery = query.leftJoin('tags', 'tags.event_id', 'events.id'); + const joinedQuery = query.leftJoin('nostr_tags', 'nostr_tags.event_id', 'nostr_events.id'); for (const [key, value] of Object.entries(filter)) { if (key.startsWith('#') && Array.isArray(value)) { const name = key.replace(/^#/, ''); query = joinedQuery - .where('tags.tag', '=', name) - .where('tags.value', 'in', value); + .where('nostr_tags.name', '=', name) + .where('nostr_tags.value', 'in', value); } } if (filter.search && this.protocol === 'sqlite:') { query = query - .innerJoin('events_fts', 'events_fts.id', 'events.id') - .where('events_fts.content', 'match', JSON.stringify(filter.search)); + .innerJoin('nostr_fts5', 'nostr_fts5.event_id', 'nostr_events.id') + .where('nostr_fts5.content', 'match', JSON.stringify(filter.search)); } return query; @@ -227,9 +227,9 @@ class EventsDB implements NStore { /** Query to get user events, joined by tags. */ usersQuery() { return this.getFilterQuery(this.#db, { kinds: [30361], authors: [Conf.pubkey] }) - .leftJoin('tags', 'tags.event_id', 'events.id') - .where('tags.tag', '=', 'd') - .select('tags.value as d_tag') + .leftJoin('nostr_tags', 'nostr_tags.event_id', 'nostr_events.id') + .where('nostr_tags.name', '=', 'd') + .select('nostr_tags.value as d_tag') .as('users'); } @@ -335,7 +335,7 @@ class EventsDB implements NStore { const query = this.getEventsQuery(filters).clearSelect().select('id'); - return await db.updateTable('events') + return await db.updateTable('nostr_events') .where('id', 'in', () => query) .set({ deleted_at: Math.floor(Date.now() / 1000) }) .execute(); From 69108c0375a4c98f31b1367b6aa4b140b359c441 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 20:58:29 -0500 Subject: [PATCH 180/252] UnattachedMedia: point to new EventsDB tables --- src/db/unattached-media.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 708e2f96..80636283 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -44,7 +44,7 @@ async function selectUnattachedMediaQuery() { async function getUnattachedMedia(until: Date) { const query = await selectUnattachedMediaQuery(); return query - .leftJoin('tags', 'unattached_media.url', 'tags.value') + .leftJoin('nostr_tags', 'unattached_media.url', 'nostr_tags.value') .where('uploaded_at', '<', until.getTime()) .execute(); } From 221c41fdfaee97f6f5011c48d1ab37c85a9e7c17 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 21:22:09 -0500 Subject: [PATCH 181/252] EventsDB: make it a simple wrapper around NDatabase --- src/kinds.ts | 46 ---- src/pipeline.ts | 5 +- src/storages/events-db.ts | 475 +++++++++----------------------------- 3 files changed, 116 insertions(+), 410 deletions(-) delete mode 100644 src/kinds.ts diff --git a/src/kinds.ts b/src/kinds.ts deleted file mode 100644 index 79538373..00000000 --- a/src/kinds.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** Events are **regular**, which means they're all expected to be stored by relays. */ -function isRegularKind(kind: number) { - return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind); -} - -/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */ -function isReplaceableKind(kind: number) { - return (10000 <= kind && kind < 20000) || [0, 3].includes(kind); -} - -/** Events are **ephemeral**, which means they are not expected to be stored by relays. */ -function isEphemeralKind(kind: number) { - return 20000 <= kind && kind < 30000; -} - -/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */ -function isParameterizedReplaceableKind(kind: number) { - return 30000 <= kind && kind < 40000; -} - -/** These events are only valid if published by the server keypair. */ -function isDittoInternalKind(kind: number) { - return kind === 30361; -} - -/** Classification of the event kind. */ -type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'; - -/** Determine the classification of this kind of event if known, or `unknown`. */ -function classifyKind(kind: number): KindClassification { - if (isRegularKind(kind)) return 'regular'; - if (isReplaceableKind(kind)) return 'replaceable'; - if (isEphemeralKind(kind)) return 'ephemeral'; - if (isParameterizedReplaceableKind(kind)) return 'parameterized'; - return 'unknown'; -} - -export { - classifyKind, - isDittoInternalKind, - isEphemeralKind, - isParameterizedReplaceableKind, - isRegularKind, - isReplaceableKind, - type KindClassification, -}; diff --git a/src/pipeline.ts b/src/pipeline.ts index 48e5c384..ec14179f 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; +import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; import { PipePolicy } from '@nostrify/nostrify/policies'; import Debug from '@soapbox/stickynotes/debug'; @@ -8,7 +8,6 @@ import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; import { RelayError } from '@/RelayError.ts'; import { updateStats } from '@/stats.ts'; @@ -103,7 +102,7 @@ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { - if (isEphemeralKind(event.kind)) return; + if (NKinds.ephemeral(event.kind)) return; const store = await Storages.db(); const [deletion] = await store.query( diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 19caf5a3..22101ef2 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,13 +1,13 @@ -import { NIP50, NostrEvent, NostrFilter, NSchema as n, NStore } from '@nostrify/nostrify'; -import Debug from '@soapbox/stickynotes/debug'; -import { Kysely, type SelectQueryBuilder } from 'kysely'; -import { sortEvents } from 'nostr-tools'; +// deno-lint-ignore-file require-await + +import { NDatabase, NIP50, NKinds, NostrEvent, NostrFilter, NSchema as n, NStore } from '@nostrify/nostrify'; +import { Stickynotes } from '@soapbox/stickynotes'; +import { Kysely } from 'kysely'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { normalizeFilters } from '@/filter.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; @@ -19,218 +19,131 @@ type TagCondition = ({ event, count, value }: { value: string; }) => boolean; -/** Conditions for when to index certain tags. */ -const tagConditions: Record = { - 'd': ({ event, count }) => count === 0 && isParameterizedReplaceableKind(event.kind), - 'e': ({ event, count, value }) => ((event.user && event.kind === 10003) || count < 15) && isNostrId(value), - 'L': ({ event, count }) => event.kind === 1985 || count === 0, - 'l': ({ event, count }) => event.kind === 1985 || count === 0, - 'media': ({ event, count, value }) => (event.user || count < 4) && isURL(value), - 'P': ({ count, value }) => count === 0 && isNostrId(value), - 'p': ({ event, count, value }) => (count < 15 || event.kind === 3) && isNostrId(value), - 'proxy': ({ count, value }) => count === 0 && isURL(value), - 'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value), - 't': ({ count, value }) => count < 5 && value.length < 50, - 'name': ({ event, count }) => event.kind === 30361 && count === 0, - 'role': ({ event, count }) => event.kind === 30361 && count === 0, -}; - -type EventQuery = SelectQueryBuilder; - /** SQLite database storage adapter for Nostr events. */ class EventsDB implements NStore { - #db: Kysely; - #debug = Debug('ditto:db:events'); - private protocol = Conf.databaseUrl.protocol; + private store: NDatabase; + private console = new Stickynotes('ditto:db:events'); - constructor(db: Kysely) { - this.#db = db; + /** Conditions for when to index certain tags. */ + static tagConditions: Record = { + 'd': ({ event, count }) => count === 0 && NKinds.parameterizedReplaceable(event.kind), + 'e': ({ event, count, value }) => ((event.user && event.kind === 10003) || count < 15) && isNostrId(value), + 'L': ({ event, count }) => event.kind === 1985 || count === 0, + 'l': ({ event, count }) => event.kind === 1985 || count === 0, + 'media': ({ event, count, value }) => (event.user || count < 4) && isURL(value), + 'P': ({ count, value }) => count === 0 && isNostrId(value), + 'p': ({ event, count, value }) => (count < 15 || event.kind === 3) && isNostrId(value), + 'proxy': ({ count, value }) => count === 0 && isURL(value), + 'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value), + 't': ({ count, value }) => count < 5 && value.length < 50, + 'name': ({ event, count }) => event.kind === 30361 && count === 0, + 'role': ({ event, count }) => event.kind === 30361 && count === 0, + }; + + constructor(private kysely: Kysely) { + this.store = new NDatabase(kysely, { + fts5: Conf.databaseUrl.protocol === 'sqlite:', + indexTags: EventsDB.indexTags, + searchText: EventsDB.searchText, + }); } /** Insert an event (and its tags) into the database. */ async event(event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise { event = purifyEvent(event); - this.#debug('EVENT', JSON.stringify(event)); - - if (isDittoInternalKind(event.kind) && event.pubkey !== Conf.pubkey) { - throw new Error('Internal events can only be stored by the server keypair'); - } - - return await this.#db.transaction().execute(async (trx) => { - /** Insert the event into the database. */ - async function addEvent() { - await trx.insertInto('nostr_events') - .values({ ...event, tags: JSON.stringify(event.tags) }) - .execute(); - } - - const protocol = this.protocol; - /** Add search data to the FTS table. */ - async function indexSearch() { - if (protocol !== 'sqlite:') return; - const searchContent = buildSearchContent(event); - if (!searchContent) return; - await trx.insertInto('nostr_fts5') - .values({ event_id: event.id, content: searchContent.substring(0, 1000) }) - .execute(); - } - - /** Index event tags depending on the conditions defined above. */ - async function indexTags() { - const tags = filterIndexableTags(event); - const rows = tags.map(([name, value]) => ({ event_id: event.id, name, value })); - - if (!tags.length) return; - await trx.insertInto('nostr_tags') - .values(rows) - .execute(); - } - - if (isReplaceableKind(event.kind)) { - const prevEvents = await this.getFilterQuery(trx, { kinds: [event.kind], authors: [event.pubkey] }).execute(); - for (const prevEvent of prevEvents) { - if (prevEvent.created_at >= event.created_at) { - throw new Error('Cannot replace an event with an older event'); - } - } - await this.deleteEventsTrx(trx, [{ kinds: [event.kind], authors: [event.pubkey] }]); - } - - if (isParameterizedReplaceableKind(event.kind)) { - const d = event.tags.find(([tag]) => tag === 'd')?.[1]; - if (d) { - const prevEvents = await this.getFilterQuery(trx, { kinds: [event.kind], authors: [event.pubkey], '#d': [d] }) - .execute(); - for (const prevEvent of prevEvents) { - if (prevEvent.created_at >= event.created_at) { - throw new Error('Cannot replace an event with an older event'); - } - } - await this.deleteEventsTrx(trx, [{ kinds: [event.kind], authors: [event.pubkey], '#d': [d] }]); - } - } - - // Run the queries. - await Promise.all([ - addEvent(), - indexTags(), - indexSearch(), - ]); - }).catch((error) => { - // Don't throw for duplicate events. - if (error.message.includes('UNIQUE constraint failed')) { - return; - } else { - throw error; - } - }); + this.console.debug('EVENT', JSON.stringify(event)); + return this.store.event(event); } - /** Build the query for a filter. */ - getFilterQuery(db: Kysely, filter: NostrFilter): EventQuery { - let query = db - .selectFrom('nostr_events') - .select([ - 'nostr_events.id', - 'nostr_events.kind', - 'nostr_events.pubkey', - 'nostr_events.content', - 'nostr_events.tags', - 'nostr_events.created_at', - 'nostr_events.sig', - ]) - .where('nostr_events.deleted_at', 'is', null); + /** Get events for filters from the database. */ + async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { + filters = await this.expandFilters(filters); - /** Whether we are querying for replaceable events by author. */ - const isAddrQuery = filter.authors && - filter.kinds && - filter.kinds.every((kind) => isReplaceableKind(kind) || isParameterizedReplaceableKind(kind)); + if (opts.signal?.aborted) return Promise.resolve([]); + if (!filters.length) return Promise.resolve([]); - // Avoid ORDER BY when querying for replaceable events by author. - if (!isAddrQuery) { - query = query.orderBy('nostr_events.created_at', 'desc'); - } + this.console.debug('REQ', JSON.stringify(filters)); - for (const [key, value] of Object.entries(filter)) { - if (value === undefined) continue; - - switch (key as keyof NostrFilter) { - case 'ids': - query = query.where('nostr_events.id', 'in', filter.ids!); - break; - case 'kinds': - query = query.where('nostr_events.kind', 'in', filter.kinds!); - break; - case 'authors': - query = query.where('nostr_events.pubkey', 'in', filter.authors!); - break; - case 'since': - query = query.where('nostr_events.created_at', '>=', filter.since!); - break; - case 'until': - query = query.where('nostr_events.created_at', '<=', filter.until!); - break; - case 'limit': - query = query.limit(filter.limit!); - break; - } - } - - const joinedQuery = query.leftJoin('nostr_tags', 'nostr_tags.event_id', 'nostr_events.id'); - - for (const [key, value] of Object.entries(filter)) { - if (key.startsWith('#') && Array.isArray(value)) { - const name = key.replace(/^#/, ''); - query = joinedQuery - .where('nostr_tags.name', '=', name) - .where('nostr_tags.value', 'in', value); - } - } - - if (filter.search && this.protocol === 'sqlite:') { - query = query - .innerJoin('nostr_fts5', 'nostr_fts5.event_id', 'nostr_events.id') - .where('nostr_fts5.content', 'match', JSON.stringify(filter.search)); - } - - return query; + return this.store.query(filters, opts); } - /** Combine filter queries into a single union query. */ - getEventsQuery(filters: NostrFilter[]) { - return filters - .map((filter) => this.#db.selectFrom(() => this.getFilterQuery(this.#db, filter).as('events')).selectAll()) - .reduce((result, query) => result.unionAll(query)); + /** Delete events based on filters from the database. */ + async remove(filters: NostrFilter[], _opts?: { signal?: AbortSignal }): Promise { + if (!filters.length) return Promise.resolve(); + this.console.debug('DELETE', JSON.stringify(filters)); + + return this.store.remove(filters); } - /** Query to get user events, joined by tags. */ - usersQuery() { - return this.getFilterQuery(this.#db, { kinds: [30361], authors: [Conf.pubkey] }) - .leftJoin('nostr_tags', 'nostr_tags.event_id', 'nostr_events.id') - .where('nostr_tags.name', '=', 'd') - .select('nostr_tags.value as d_tag') - .as('users'); + /** Get number of events that would be returned by filters. */ + async count( + filters: NostrFilter[], + opts: { signal?: AbortSignal } = {}, + ): Promise<{ count: number; approximate: boolean }> { + if (opts.signal?.aborted) return Promise.reject(abortError()); + if (!filters.length) return Promise.resolve({ count: 0, approximate: false }); + + this.console.debug('COUNT', JSON.stringify(filters)); + + return this.store.count(filters); + } + + /** Return only the tags that should be indexed. */ + static indexTags(event: DittoEvent): string[][] { + const tagCounts: Record = {}; + + function getCount(name: string) { + return tagCounts[name] || 0; + } + + function incrementCount(name: string) { + tagCounts[name] = getCount(name) + 1; + } + + function checkCondition(name: string, value: string, condition: TagCondition) { + return condition({ + event, + count: getCount(name), + value, + }); + } + + return event.tags.reduce((results, tag) => { + const [name, value] = tag; + const condition = EventsDB.tagConditions[name] as TagCondition | undefined; + + if (value && condition && value.length < 200 && checkCondition(name, value, condition)) { + results.push(tag); + } + + incrementCount(name); + return results; + }, []); + } + + /** Build a search index from the event. */ + static searchText(event: NostrEvent): string { + switch (event.kind) { + case 0: + return EventsDB.buildUserSearchContent(event); + case 1: + return event.content; + case 30009: + return EventsDB.buildTagsSearchContent(event.tags.filter(([t]) => t !== 'alt')); + default: + return ''; + } + } + + /** Build search content for a user. */ + static buildUserSearchContent(event: NostrEvent): string { + const { name, nip05, about } = n.json().pipe(n.metadata()).catch({}).parse(event.content); + return [name, nip05, about].filter(Boolean).join('\n'); + } + + /** Build search content from tag values. */ + static buildTagsSearchContent(tags: string[][]): string { + return tags.map(([_tag, value]) => value).join('\n'); } /** Converts filters to more performant, simpler filters that are better for SQLite. */ @@ -244,7 +157,7 @@ class EventsDB implements NStore { ) as { key: 'domain'; value: string } | undefined)?.value; if (domain) { - const query = this.#db + const query = this.kysely .selectFrom('pubkey_domains') .select('pubkey') .where('domain', '=', domain); @@ -268,166 +181,6 @@ class EventsDB implements NStore { return normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries. } - - /** Get events for filters from the database. */ - async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { - filters = await this.expandFilters(filters); - - if (opts.signal?.aborted) return Promise.resolve([]); - if (!filters.length) return Promise.resolve([]); - - this.#debug('REQ', JSON.stringify(filters)); - let query = this.getEventsQuery(filters); - - if (typeof opts.limit === 'number') { - query = query.limit(opts.limit); - } - - const events = (await query.execute()).map((row) => { - const event: DittoEvent = { - id: row.id, - kind: row.kind, - pubkey: row.pubkey, - content: row.content, - created_at: row.created_at, - tags: JSON.parse(row.tags), - sig: row.sig, - }; - - if (row.author_id) { - event.author = { - id: row.author_id, - kind: row.author_kind! as 0, - pubkey: row.author_pubkey!, - content: row.author_content!, - created_at: row.author_created_at!, - tags: JSON.parse(row.author_tags!), - sig: row.author_sig!, - }; - } - - if (typeof row.author_stats_followers_count === 'number') { - event.author_stats = { - followers_count: row.author_stats_followers_count, - following_count: row.author_stats_following_count!, - notes_count: row.author_stats_notes_count!, - }; - } - - if (typeof row.stats_replies_count === 'number') { - event.event_stats = { - replies_count: row.stats_replies_count, - reposts_count: row.stats_reposts_count!, - reactions_count: row.stats_reactions_count!, - }; - } - - return event; - }); - - return sortEvents(events); - } - - /** Delete events from each table. Should be run in a transaction! */ - async deleteEventsTrx(db: Kysely, filters: NostrFilter[]) { - if (!filters.length) return Promise.resolve(); - this.#debug('DELETE', JSON.stringify(filters)); - - const query = this.getEventsQuery(filters).clearSelect().select('id'); - - return await db.updateTable('nostr_events') - .where('id', 'in', () => query) - .set({ deleted_at: Math.floor(Date.now() / 1000) }) - .execute(); - } - - /** Delete events based on filters from the database. */ - async remove(filters: NostrFilter[], _opts?: { signal?: AbortSignal }): Promise { - if (!filters.length) return Promise.resolve(); - this.#debug('DELETE', JSON.stringify(filters)); - - await this.#db.transaction().execute((trx) => this.deleteEventsTrx(trx, filters)); - } - - /** Get number of events that would be returned by filters. */ - async count( - filters: NostrFilter[], - opts: { signal?: AbortSignal } = {}, - ): Promise<{ count: number; approximate: boolean }> { - if (opts.signal?.aborted) return Promise.reject(abortError()); - if (!filters.length) return Promise.resolve({ count: 0, approximate: false }); - - this.#debug('COUNT', JSON.stringify(filters)); - const query = this.getEventsQuery(filters); - - const [{ count }] = await query - .clearSelect() - .select((eb) => eb.fn.count('id').as('count')) - .execute(); - - return { - count: Number(count), - approximate: false, - }; - } -} - -/** Return only the tags that should be indexed. */ -function filterIndexableTags(event: DittoEvent): string[][] { - const tagCounts: Record = {}; - - function getCount(name: string) { - return tagCounts[name] || 0; - } - - function incrementCount(name: string) { - tagCounts[name] = getCount(name) + 1; - } - - function checkCondition(name: string, value: string, condition: TagCondition) { - return condition({ - event, - count: getCount(name), - value, - }); - } - - return event.tags.reduce((results, tag) => { - const [name, value] = tag; - const condition = tagConditions[name] as TagCondition | undefined; - - if (value && condition && value.length < 200 && checkCondition(name, value, condition)) { - results.push(tag); - } - - incrementCount(name); - return results; - }, []); -} - -/** Build a search index from the event. */ -function buildSearchContent(event: NostrEvent): string { - switch (event.kind) { - case 0: - return buildUserSearchContent(event); - case 1: - return event.content; - case 30009: - return buildTagsSearchContent(event.tags.filter(([t]) => t !== 'alt')); - default: - return ''; - } -} - -/** Build search content for a user. */ -function buildUserSearchContent(event: NostrEvent): string { - const { name, nip05, about } = n.json().pipe(n.metadata()).catch({}).parse(event.content); - return [name, nip05, about].filter(Boolean).join('\n'); -} - -/** Build search content from tag values. */ -function buildTagsSearchContent(tags: string[][]): string { - return tags.map(([_tag, value]) => value).join('\n'); } export { EventsDB }; From ae0ec7be7e57cff4f85ccf456dc9b4c4ce6bf3d4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 21:29:04 -0500 Subject: [PATCH 182/252] EventsDB: remove DittoEvent dependency --- src/storages/events-db.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 22101ef2..5f1acbb7 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -7,14 +7,13 @@ import { Kysely } from 'kysely'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { normalizeFilters } from '@/filter.ts'; -import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; /** Function to decide whether or not to index a tag. */ type TagCondition = ({ event, count, value }: { - event: DittoEvent; + event: NostrEvent; count: number; value: string; }) => boolean; @@ -27,10 +26,10 @@ class EventsDB implements NStore { /** Conditions for when to index certain tags. */ static tagConditions: Record = { 'd': ({ event, count }) => count === 0 && NKinds.parameterizedReplaceable(event.kind), - 'e': ({ event, count, value }) => ((event.user && event.kind === 10003) || count < 15) && isNostrId(value), + 'e': ({ event, count, value }) => ((event.kind === 10003) || count < 15) && isNostrId(value), 'L': ({ event, count }) => event.kind === 1985 || count === 0, 'l': ({ event, count }) => event.kind === 1985 || count === 0, - 'media': ({ event, count, value }) => (event.user || count < 4) && isURL(value), + 'media': ({ count, value }) => (count < 4) && isURL(value), 'P': ({ count, value }) => count === 0 && isNostrId(value), 'p': ({ event, count, value }) => (count < 15 || event.kind === 3) && isNostrId(value), 'proxy': ({ count, value }) => count === 0 && isURL(value), @@ -56,7 +55,7 @@ class EventsDB implements NStore { } /** Get events for filters from the database. */ - async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { + async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { filters = await this.expandFilters(filters); if (opts.signal?.aborted) return Promise.resolve([]); @@ -89,7 +88,7 @@ class EventsDB implements NStore { } /** Return only the tags that should be indexed. */ - static indexTags(event: DittoEvent): string[][] { + static indexTags(event: NostrEvent): string[][] { const tagCounts: Record = {}; function getCount(name: string) { From 137bd0dae07518b4ad347aa91bc322abde8b62d6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 22:19:33 -0500 Subject: [PATCH 183/252] adminAccountsController: fix type error with DittoEvent --- src/controllers/api/admin.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index b9464a3f..77571aa7 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -2,11 +2,12 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; -import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; -import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts'; import { addTag } from '@/tags.ts'; +import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts'; +import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; const adminAccountQuerySchema = z.object({ local: booleanParamSchema.optional(), @@ -49,7 +50,7 @@ const adminAccountsController: AppController = async (c) => { for (const event of events) { const d = event.tags.find(([name]) => name === 'd')?.[1]; - event.d_author = authors.find((author) => author.pubkey === d); + (event as DittoEvent).d_author = authors.find((author) => author.pubkey === d); } const accounts = await Promise.all( From 171350a34dc536f325b1cf69af958149b936775c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 08:08:30 -0500 Subject: [PATCH 184/252] Drop deleted_at column --- src/db/DittoTables.ts | 1 - src/db/migrations/020_drop_deleted_at.ts | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/db/migrations/020_drop_deleted_at.ts diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index 77e0b807..42d39ea9 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -30,7 +30,6 @@ interface EventRow { created_at: number; tags: string; sig: string; - deleted_at: number | null; } interface EventFTSRow { diff --git a/src/db/migrations/020_drop_deleted_at.ts b/src/db/migrations/020_drop_deleted_at.ts new file mode 100644 index 00000000..670f94d6 --- /dev/null +++ b/src/db/migrations/020_drop_deleted_at.ts @@ -0,0 +1,10 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.deleteFrom('nostr_events').where('deleted_at', 'is not', 'null').execute(); + await db.schema.alterTable('nostr_events').dropColumn('deleted_at').execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('nostr_events').addColumn('deleted_at', 'integer').execute(); +} From 7021b0d4fd9e8b327d06ae7b09a2f88c9602482e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 08:13:19 -0500 Subject: [PATCH 185/252] 'null' -> null --- src/db/migrations/020_drop_deleted_at.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/migrations/020_drop_deleted_at.ts b/src/db/migrations/020_drop_deleted_at.ts index 670f94d6..4894b9f5 100644 --- a/src/db/migrations/020_drop_deleted_at.ts +++ b/src/db/migrations/020_drop_deleted_at.ts @@ -1,7 +1,7 @@ import { Kysely } from 'kysely'; export async function up(db: Kysely): Promise { - await db.deleteFrom('nostr_events').where('deleted_at', 'is not', 'null').execute(); + await db.deleteFrom('nostr_events').where('deleted_at', 'is not', null).execute(); await db.schema.alterTable('nostr_events').dropColumn('deleted_at').execute(); } From 406baf8a1d8b7c1b90facec9ad95fae5cf8f2cec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 08:18:31 -0500 Subject: [PATCH 186/252] events-db -> EventsDB --- scripts/admin-event.ts | 2 +- scripts/admin-role.ts | 2 +- src/storages.ts | 2 +- src/storages/{events-db.test.ts => EventsDB.test.ts} | 2 +- src/storages/{events-db.ts => EventsDB.ts} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/storages/{events-db.test.ts => EventsDB.test.ts} (97%) rename src/storages/{events-db.ts => EventsDB.ts} (100%) diff --git a/scripts/admin-event.ts b/scripts/admin-event.ts index 29d3ae2e..ca942512 100644 --- a/scripts/admin-event.ts +++ b/scripts/admin-event.ts @@ -3,7 +3,7 @@ import { TextLineStream } from '@std/streams/text-line-stream'; import { DittoDB } from '@/db/DittoDB.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { EventsDB } from '@/storages/events-db.ts'; +import { EventsDB } from '@/storages/EventsDB.ts'; import { type EventStub } from '@/utils/api.ts'; import { nostrNow } from '@/utils.ts'; diff --git a/scripts/admin-role.ts b/scripts/admin-role.ts index 57a17e37..6e7bfc66 100644 --- a/scripts/admin-role.ts +++ b/scripts/admin-role.ts @@ -3,7 +3,7 @@ import { NSchema } from '@nostrify/nostrify'; import { DittoDB } from '@/db/DittoDB.ts'; import { Conf } from '@/config.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { EventsDB } from '@/storages/events-db.ts'; +import { EventsDB } from '@/storages/EventsDB.ts'; import { nostrNow } from '@/utils.ts'; const kysely = await DittoDB.getInstance(); diff --git a/src/storages.ts b/src/storages.ts index 1cf06c11..10d5b05a 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -4,7 +4,7 @@ import { RelayPoolWorker } from 'nostr-relaypool'; import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; -import { EventsDB } from '@/storages/events-db.ts'; +import { EventsDB } from '@/storages/EventsDB.ts'; import { Optimizer } from '@/storages/optimizer.ts'; import { PoolStore } from '@/storages/pool-store.ts'; import { Reqmeister } from '@/storages/reqmeister.ts'; diff --git a/src/storages/events-db.test.ts b/src/storages/EventsDB.test.ts similarity index 97% rename from src/storages/events-db.test.ts rename to src/storages/EventsDB.test.ts index d935e6bb..d1986b7d 100644 --- a/src/storages/events-db.test.ts +++ b/src/storages/EventsDB.test.ts @@ -5,7 +5,7 @@ import { DittoDB } from '@/db/DittoDB.ts'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; -import { EventsDB } from '@/storages/events-db.ts'; +import { EventsDB } from '@/storages/EventsDB.ts'; const kysely = await DittoDB.getInstance(); const eventsDB = new EventsDB(kysely); diff --git a/src/storages/events-db.ts b/src/storages/EventsDB.ts similarity index 100% rename from src/storages/events-db.ts rename to src/storages/EventsDB.ts From 4d3a9c6e2396026cd71c83f99b9ddccd004f5e65 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 10:09:31 -0500 Subject: [PATCH 187/252] stats: fix kysely screaming that we're awaiting a builder instance --- src/stats.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/stats.ts b/src/stats.ts index f8efe16c..92040710 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,6 +1,6 @@ import { NKinds, NostrEvent } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; -import { InsertQueryBuilder } from 'kysely'; +import { InsertQueryBuilder, Kysely } from 'kysely'; import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; @@ -37,8 +37,10 @@ async function updateStats(event: NostrEvent) { debug(JSON.stringify({ id: event.id, pubkey: event.pubkey, kind: event.kind, tags: event.tags, statDiffs })); } - if (pubkeyDiffs.length) queries.push(await authorStatsQuery(pubkeyDiffs)); - if (eventDiffs.length) queries.push(await eventStatsQuery(eventDiffs)); + const kysely = await DittoDB.getInstance(); + + if (pubkeyDiffs.length) queries.push(authorStatsQuery(kysely, pubkeyDiffs)); + if (eventDiffs.length) queries.push(eventStatsQuery(kysely, eventDiffs)); if (queries.length) { await Promise.all(queries.map((query) => query.execute())); @@ -102,7 +104,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr } /** Create an author stats query from the list of diffs. */ -async function authorStatsQuery(diffs: AuthorStatDiff[]) { +function authorStatsQuery(kysely: Kysely, diffs: AuthorStatDiff[]) { const values: DittoTables['author_stats'][] = diffs.map(([_, pubkey, stat, diff]) => { const row: DittoTables['author_stats'] = { pubkey, @@ -114,7 +116,6 @@ async function authorStatsQuery(diffs: AuthorStatDiff[]) { return row; }); - const kysely = await DittoDB.getInstance(); return kysely.insertInto('author_stats') .values(values) .onConflict((oc) => @@ -129,7 +130,7 @@ async function authorStatsQuery(diffs: AuthorStatDiff[]) { } /** Create an event stats query from the list of diffs. */ -async function eventStatsQuery(diffs: EventStatDiff[]) { +function eventStatsQuery(kysely: Kysely, diffs: EventStatDiff[]) { const values: DittoTables['event_stats'][] = diffs.map(([_, event_id, stat, diff]) => { const row: DittoTables['event_stats'] = { event_id, @@ -141,7 +142,6 @@ async function eventStatsQuery(diffs: EventStatDiff[]) { return row; }); - const kysely = await DittoDB.getInstance(); return kysely.insertInto('event_stats') .values(values) .onConflict((oc) => From e15779dcfd65fa46c16561cd16e514c3ef8e3cea Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 15 May 2024 13:06:07 -0300 Subject: [PATCH 188/252] docs: mark moderation and notifications as done --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f56cf83..6551f27d 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ For more info see: https://docs.soapbox.pub/ditto/ - [x] Like and comment on posts - [x] Share posts - [x] Reposts -- [ ] Notifications +- [x] Notifications - [x] Profiles - [ ] Search -- [ ] Moderation +- [x] Moderation - [ ] Zaps - [x] Customizable - [x] Open source From 19b2fd19e8646deb154b84ee99ce22c1844a9aa7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 16:30:03 -0500 Subject: [PATCH 189/252] Add a default favicon.ico --- static/favicon.ico | Bin 0 -> 15086 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/favicon.ico diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0046ea26153c08b24ef99b4ef9c85c7557f66f76 GIT binary patch literal 15086 zcmeHt2Ut|sw*PpOJo9p2UUJ{Pxi_(aU1N$#Okyl_Y_Wq%naVIj8HS+^FhiMPV5owC z4MY@BqzNbjiu8`4g4iG`#V$51)G5EW4{ARDeo6G2y!*cY`#c|e7U!J3_xkNM>#Vcq zw-${yiuM+5_G}uFy0rAEG}?DG8ckRC>G_8=S_irIA(7O5`?qPdVA5d*=|hIl*rZX{ zM~*2^#GB7I7Ik;J^Ryj_l(fKafva_pxFYbBy8}-bXQC^4|UR5&ye) zL^=!J=TA2Jl0Rc9o&Wi&Nc(TC4>;-b8=aTAwmGl!xgcT&o$=spI^yFPFAZ|pHzVDx zeANFY;%y%pj1o-L{|j%jzM<_jqd@Lw%c{8Fo8RV|ax~oaF5ub)fa@I%+u%g-qxZpa zdk&oA3gEG|2tJ!CI+RhzteeCQ|ME4sPhB+Hc8dNSo2mK=&Pd|04k@s4nY@!z;p^)1=c4071BOm4I1tOU!H2`1eYboU_8 z!?uB$kO5A53G5Cv!?m;%esw)iG(AMn$v!Ar`VrVXh`^IW2y7aHqG|xj{CnAd6#k96 zi~kxn!E{;Zc+<#`ai+zgV@*$mjWsj1MTEYUI;|^<| zFfd{g!P=7tySxT?Rdr$GsR!73aR@1w)!0c|)Fm~PZ5k-fX`yJ_c*w3UQZI+OYGg$G$D9i(XB`mELtjInqNt71E@(Az4-z zWQT7P+xH>hm>L1q>TACTf8Yee?*qpg6b61`Fr@s%5Fwu!A@~zxNX8q%ZjwIOQ}n?3 zd@)!?7O-ZDVCfqItF60Wn^g>-%64o$b06^?!w7Fzsr{OVy4@NE(nZz%E`p>{0wB$`P6b_4|#z%Szh{L1>_SE;IeUHoz#z2)+;dY1w}Hh}VD zBZQC^FvbW%9euDSEd*=2Ay_}Gfz>)YScn5)8Iu4`#u0c`{)Ct_4-wlwjDW^I`0u}t z;P{J$-l?}{IBmTC){A)7%;3+f>~5}OxpRe0-p}|M^E=*J_o-;vidp>2t3MOKYfTsmBXbeH zDF?!|W5n+sx5|nKbzcSV^L_kvjb{6d*K3m#>_Nola^f@2#OBP&3&5CR2&D27#iFkr(pS{jPSWlb_CW$X}5*ODAJh5SO0(b>AkTmze`$Qk? z_H}}zY=PJ2ZfwiyMckGyqzdX##xBPxZZ*334Y=oU0)x)2P`jOnhTzpmx^P=`7IAA2 zAj6{>+?BDgq;CT^AQKLI%i&mg{ee^6uq`q!yw;w_ebOQ?%Ga%R=aSfH1gk|1n7jGG zGGPw{rOohZeE@OQ1Ms$-22XJkfeGD+-qnRzaW&HE2T{%_KnuGB-P}s_+SOyoiTH=> zX=pv$p_N>M*5@*`zE{yAY{C}DBjB*fGbAI=|DH0~S6=CJuN`zRJ~TksiGTiZoo=$w zX7a*zD+>EeV_5#khPihj=t-Gyu4*IL`{9;<3tVL*9D|w>w4(!IA;++poq>$?dr@qi zjT6iQba0Asmsf>B$79gAoPt(-o?yR7aCbrLe-)73z)#ZC5ZEVy<4^XSdrHBt=<0K- z?w9T_9sIX{9yyKXKEw2T-W0vFRyqXx3}aZV;K1A`7_7ayaINcrC)ra8_jQBqO`ffw z6A0XP4uKwph&JDby=HMJu-JnJdN$fw1-Qj6MV~_*)S{CF`&nqE1iN21w6d$v%5MMx zw{Vu=w|7Oz9P@ZoTBf3vnTsnnNAS?T62s1o(0UNR zkr1Ev?}kQx4Vr+P&?s&Jircsn)B*3kRj{w>==H3A;9i{8^ICTIp1$bsc2f=Vt#y9@ zlkC4NmeH|J8U!|lz3wu^wT~eTZiJO>F1UW>kZ!Dlgr0z)MKWwzra;>2C={*Vi6(kF zE^-dxj$H``g>}%lw?OONM(o~2a9@Weum_r;+anEucW^V}I=qfvx#dvv(B?|gmDlq3 z;7^y2cA9D!%g~*tWlUNCE93Pr^9lfS5Am7WZg|%`gpYS6SR3|&>3$I6;8J*4ZiBa; zCpH>;VfT+gIB2#7)mF(k!^*~0-eEixR6^z21g#hG8{ZCs{TkFkJy0v}LZiGl(zN9N z-mu#}vDtT4e)-no;CX$f%QU+YBEuJ4u*F1z^pz1fx?u;JnEP;%n}<90#P-g__8!Fc(strEmxX7>*O+%z6n5-kPr)Egf_8+g~@6;H5U=rs!R^o-h~mub0A{=L*a1yCEt) z3(w>I{abx1`T`6$gR?9W4!+rN=kEY(jva*Gu%TQaz|O@o959PPrS)#Ka8*$FoHR>7i!WpR0}3+P~6;m*K+5dUI)L|S1bO*W`@E35$w|q zux>pM>!Y^7@o)pg#~%+1OYTK)Hi|eQ)(eF#V=H{!(!iZ-59X)laQoU45qd(TEDuDX z#a7g__MnZIiyp_L7!=hI+mo0hA@S0`lfp=_62A#0c*7{{56D`2p(TE#mS1;$9qiN? z{w!k#bJF}lYn^#mzi0!@Wx-@0b_DLndcbSer1*H{ObA=P@wl5wFua7x@Lv)Do0+S@ zov|FAU$4g|BN6tk3C24w#2n%~zl2{CbmzLt zultMF!7un~>EBq>_39Yo=fd)v#V~X70DX5VT&gd?>BL|wzj0`UeUG$^iS;y##ui=* zWZyc#dQ)+w872<)2_`W+8cb?*x zT_N_T{O76vQ_n4hU3t56Sl0bd?<*->?mw)1-)5F+33~$Zn|Ui>Ch-9~or;xL$vbK& zcUxia`&2*96#Bof)wM8}tqAF^aXg5)d2+Zd;9-ZQ7a~`Y*t6CX`xp@@;wR#SAQPR= zh3Iu5ar6JdPht=8e}eZ({2?)iJj;E<%x|4gz)r&j>jUurPK+(q;n?pLhh&G1NaJt9 zVMp@b5@zC(s1WyDD=|dyYrI;aA^xxNfAZdX68mpM71T2@9B@tE?{)gM-m?@or^U<- zyxB`NmftOdnOF)|S}w%(*CDO#&*tg*PPY1t?`JuScg5-}M_B2*!P3kR_I6v5AuhrR zVGWKu)uJS<9Q%VZknO!2`EI*W=aPYT*8=poRiNMf7}VtX)sWb)^&{~|eiiC~8&D~( zUmKELcI@*y^SgLiPr9?Mz;GQbezbx`cogi&Uf%EcBY1_^pJbZ`p5ZJB(Acb2!iE_I zjxZiJKIyOtEhKy81_Y*E#r7kF3oGcqzGHJ^aDgm+dx39ycbRvV`lM$*I>n{9FRsI& z=Sip~=b-Vupw;+y+*J8>Bo6xi^zB_?`D?{`%I?n&g;-(bW$mb8u$;iyo(QM%GYDuJ zgmQa3-2F=+k{^LkSp=tzv4IXZnAyjEPD7cB60D zJ!cnR*z8J!bM+<2PpA-jbdc~f_u-Ry3)1~J;D7KI!j9g@mihriG^SqM* zy!NZ_`vy_!+b2D8N1t`w|8|pA*lX`;ert_;(+|IwoRXUv9KHqoq%4T42~S7x2iI$$ ztkNR5T7!-CYQ&z|+nI|TxZkc#=bGA_wY3!A;|oKF?ET_2!qC)V zkI1|$ew!*chQ~Dj{blpL%6;FNrlV{p>y711GhlH(TXc{;&-^BPt%H^+Qh*t;3*6+x za5~rs4^pcksk{uoit8lbenTCUb3Nu&=6}{FYVy)Qg-$Th2_0`@89L51EOhLmcwZfp z2;OA9NY1AwXW0wQRSYX=CSv1^Dr`xu+rh7fX24aJ$KwF)QH2&@`< zg^@MpQ(Rd{_ZwHSLHZIsDl6 zp@)&@Rjq-os&v0*wL+YHZkIHyd?;Y=dC1EiLSC!B^=xgwCFK+JJ_{J5Ul#PSp+-sa z!rtQyz$0}8tgn{BdMyuDe&OJy7Qnam5+Yh2BCPB#l!toqL{c zw{mzDb^q56*+i4~ea0DN$w>``H_6LVc{avZOJHT;0PBeD5ELGRPs1IE58i<&nsDI3 zZ3qs#fK3sdNC@jhy0W7sV0|tlVb;ErXH04!ULFUgC=L!u>dO=u0Xc!I=1h;jnt^?D3EufQrd}fX$R6} z=aJ}r`eC_2xu$u;DYyvIVH4@%?99V7I0U@c2aXZojyRc?>MzkQry9fyui8ySLic`!(=&>!w5Xq5h#pej0 z+wmmtD8Gq(NiBG>hhSgae#yD!k?G5HyU$qs5qGlQL8@NFas><4F*`{v^ddx=-QY;8 z;1*Pe0Q-H2A-TA0vu$XwPDi)RVe|=V376kGqFW;;`KzaSx2l+SI38^~C$8yz>AGdJ zmc7fLtQSjiQ(B89R_&1&BYBsRyixG0 z=(hXR_;aGW!|KaLSTB~t;cyc?EAKXhuZ+9wxH<-|{5WvFX2I)Q77~{DqR1i+r){!u zM^FNl8~NU;{1sXIll%jfJ0UIjo|@!9tzM?zW$vnX*i#oKT9fa59m5ee83k}|>OJe4 zadAPwvhW5eeLKAMUBH|EBmBN&AbDi~%9x30wJ9)3koi z#`j)j%`f&zNy_FZwk7uoi%5ObaTS8kzg^26VruhMZG81w~8flJ+xC@jvp*kSO>cKI~}! z^(XH5AI5UNU%S%A+(qH&x@ot2*#1cW?G5vONwso<;X7etO=bm;FX7f127WuS5C#c=33n6;3+({mkWT*d`o+OKulJw|3*8?|Gadb$chg z+ckR_mQjNOx;g|NI_w)Lr-&&J_I|`UU~`sa0^<-G)$UIf}TX4vk+;YPilr zMe0Vk`Imv4+K^A`1jaqn?K)BaUGh65hQ7g3645S8x4O zl1Kh|%_HMKGj*bKT(wRIPF7gydkH10Lc0>lUUrXQ>rkU}hUT#csU7t1q-v+y34WvJ zbyNR~nL=NO*c3?8FZQ|w9vyPu(KHyk|3-eww&QDSy|P6&y=r1qK5ZTtq0sis4t1 Date: Wed, 15 May 2024 17:09:12 -0500 Subject: [PATCH 190/252] Use port 4036 by default --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 3d3e51b4..09437361 100644 --- a/src/config.ts +++ b/src/config.ts @@ -42,7 +42,7 @@ class Conf { } static get port() { - return parseInt(Deno.env.get('PORT') || '8000'); + return parseInt(Deno.env.get('PORT') || '4036'); } static get relay(): `wss://${string}` | `ws://${string}` { From 597946002dacdb6e15a1a896300d96156a2ea0ec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 18:38:51 -0500 Subject: [PATCH 191/252] Add a basic Dockerfile --- .dockerignore | 6 ++++++ Dockerfile | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0771aee5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.env +*.cpuprofile +*.swp +deno-test.xml + +/data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f8df8159 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM denoland/deno:1.43.3 +EXPOSE 4036 +WORKDIR /app +RUN mkdir -p data && chown -R deno data +USER deno +COPY . . +RUN deno cache src/server.ts +CMD deno task start From 8e68d13ff15ac5744a341a2d022ef8383085d0eb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 18:49:08 -0500 Subject: [PATCH 192/252] Let custom policy be configured with DITTO_POLICY --- src/config.ts | 4 ++++ src/pipeline.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 09437361..92bbdc3e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -219,6 +219,10 @@ class Conf { static get firehoseEnabled(): boolean { return optionalBooleanSchema.parse(Deno.env.get('FIREHOSE_ENABLED')) ?? true; } + /** Path to the custom policy module. Supports any value Deno's `import()` accepts, including relative path, absolute path, https:, npm:, and jsr:. */ + static get policy(): string { + return Deno.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).toString(); + } } const optionalBooleanSchema = z diff --git a/src/pipeline.ts b/src/pipeline.ts index ec14179f..83e39239 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -60,7 +60,7 @@ async function policyFilter(event: NostrEvent): Promise { ]; try { - const CustomPolicy = (await import('../data/policy.ts')).default; + const CustomPolicy = (await import(Conf.policy)).default; policies.push(new CustomPolicy()); } catch (_e) { debug('policy not found - https://docs.soapbox.pub/ditto/policies/'); From 8a672c93ecb94306b941e6776e04fcc8e2d754a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 18:53:30 -0500 Subject: [PATCH 193/252] Debug custom policies with ditto:policy --- src/config.ts | 2 +- src/pipeline.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index 92bbdc3e..589386f2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -219,7 +219,7 @@ class Conf { static get firehoseEnabled(): boolean { return optionalBooleanSchema.parse(Deno.env.get('FIREHOSE_ENABLED')) ?? true; } - /** Path to the custom policy module. Supports any value Deno's `import()` accepts, including relative path, absolute path, https:, npm:, and jsr:. */ + /** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */ static get policy(): string { return Deno.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).toString(); } diff --git a/src/pipeline.ts b/src/pipeline.ts index 83e39239..6162530a 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -2,6 +2,7 @@ import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; import { PipePolicy } from '@nostrify/nostrify/policies'; import Debug from '@soapbox/stickynotes/debug'; +import { Stickynotes } from '@soapbox/stickynotes'; import { sql } from 'kysely'; import { Conf } from '@/config.ts'; @@ -55,6 +56,8 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { + const console = new Stickynotes('ditto:policy'); + const policies: NPolicy[] = [ new MuteListPolicy(Conf.pubkey, await Storages.admin()), ]; @@ -62,14 +65,16 @@ async function policyFilter(event: NostrEvent): Promise { try { const CustomPolicy = (await import(Conf.policy)).default; policies.push(new CustomPolicy()); - } catch (_e) { - debug('policy not found - https://docs.soapbox.pub/ditto/policies/'); + console.info(`Using custom policy: ${Conf.policy}`); + } catch { + console.info('Custom policy not found '); } const policy = new PipePolicy(policies.reverse()); const result = await policy.call(event); - debug(JSON.stringify(result)); + console.debug(JSON.stringify(result)); + RelayError.assert(result); } From 6a1b8b0943606f98ecc9b8c84757159d43058841 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 19:29:58 -0500 Subject: [PATCH 194/252] policy: improve error handling --- src/pipeline.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index 6162530a..52cb5632 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -2,7 +2,6 @@ import { NKinds, NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; import { PipePolicy } from '@nostrify/nostrify/policies'; import Debug from '@soapbox/stickynotes/debug'; -import { Stickynotes } from '@soapbox/stickynotes'; import { sql } from 'kysely'; import { Conf } from '@/config.ts'; @@ -56,7 +55,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - const console = new Stickynotes('ditto:policy'); + const debug = Debug('ditto:policy'); const policies: NPolicy[] = [ new MuteListPolicy(Conf.pubkey, await Storages.admin()), @@ -65,17 +64,30 @@ async function policyFilter(event: NostrEvent): Promise { try { const CustomPolicy = (await import(Conf.policy)).default; policies.push(new CustomPolicy()); - console.info(`Using custom policy: ${Conf.policy}`); - } catch { - console.info('Custom policy not found '); + debug(`Using custom policy: ${Conf.policy}`); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') { + debug('Custom policy not found '); + } else { + console.error(`DITTO_POLICY (error importing policy): ${Conf.policy}`, e); + throw new RelayError('blocked', 'policy could not be loaded'); + } } const policy = new PipePolicy(policies.reverse()); - const result = await policy.call(event); - console.debug(JSON.stringify(result)); - - RelayError.assert(result); + try { + const result = await policy.call(event); + debug(JSON.stringify(result)); + RelayError.assert(result); + } catch (e) { + if (e instanceof RelayError) { + throw e; + } else { + console.error('POLICY ERROR:', e); + throw new RelayError('blocked', 'policy error'); + } + } } /** Encounter the event, and return whether it has already been encountered. */ From 9e9ab4088609f08703711d8443b51c34aabb2e14 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 20:19:49 -0500 Subject: [PATCH 195/252] Run the custom policy in a worker for security --- Dockerfile | 2 +- data/.gitignore | 3 ++- data/policy/.gitignore | 2 ++ deno.json | 2 +- src/config.ts | 2 +- src/pipeline.ts | 7 ++++--- src/workers/policy.ts | 23 +++++++++++++++++++++++ src/workers/policy.worker.ts | 19 +++++++++++++++++++ 8 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 data/policy/.gitignore create mode 100644 src/workers/policy.ts create mode 100644 src/workers/policy.worker.ts diff --git a/Dockerfile b/Dockerfile index f8df8159..a0e21946 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM denoland/deno:1.43.3 EXPOSE 4036 WORKDIR /app -RUN mkdir -p data && chown -R deno data +RUN mkdir -p data/policy && chown -R deno data USER deno COPY . . RUN deno cache src/server.ts diff --git a/data/.gitignore b/data/.gitignore index c96a04f0..3c46d845 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,2 +1,3 @@ * -!.gitignore \ No newline at end of file +!.gitignore +!/policy \ No newline at end of file diff --git a/data/policy/.gitignore b/data/policy/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/data/policy/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/deno.json b/deno.json index 1ead2b92..399f4743 100644 --- a/deno.json +++ b/deno.json @@ -13,7 +13,7 @@ "admin:role": "deno run -A scripts/admin-role.ts", "stats:recompute": "deno run -A scripts/stats-recompute.ts" }, - "unstable": ["ffi", "kv"], + "unstable": ["ffi", "kv", "worker-options"], "exclude": ["./public"], "imports": { "@/": "./src/", diff --git a/src/config.ts b/src/config.ts index 589386f2..5a30c0e6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -221,7 +221,7 @@ class Conf { } /** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */ static get policy(): string { - return Deno.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).toString(); + return Deno.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).pathname; } } diff --git a/src/pipeline.ts b/src/pipeline.ts index 52cb5632..6f487ed4 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -16,6 +16,7 @@ import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { eventAge, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; import { fetchWorker } from '@/workers/fetch.ts'; +import { policyWorker } from '@/workers/policy.ts'; import { TrendsWorker } from '@/workers/trends.ts'; import { verifyEventWorker } from '@/workers/verify.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; @@ -62,11 +63,11 @@ async function policyFilter(event: NostrEvent): Promise { ]; try { - const CustomPolicy = (await import(Conf.policy)).default; - policies.push(new CustomPolicy()); + await policyWorker.import(Conf.policy); + policies.push(policyWorker); debug(`Using custom policy: ${Conf.policy}`); } catch (e) { - if (e.code === 'ERR_MODULE_NOT_FOUND') { + if (e.message.includes('Module not found')) { debug('Custom policy not found '); } else { console.error(`DITTO_POLICY (error importing policy): ${Conf.policy}`, e); diff --git a/src/workers/policy.ts b/src/workers/policy.ts new file mode 100644 index 00000000..3cc03c9c --- /dev/null +++ b/src/workers/policy.ts @@ -0,0 +1,23 @@ +import * as Comlink from 'comlink'; + +import { Conf } from '@/config.ts'; +import type { CustomPolicy } from '@/workers/policy.worker.ts'; + +const policyDir = new URL('../../data/policy', import.meta.url).pathname; + +export const policyWorker = Comlink.wrap( + new Worker( + new URL('./policy.worker.ts', import.meta.url), + { + type: 'module', + deno: { + permissions: { + read: [Conf.policy, policyDir], + write: [policyDir], + net: 'inherit', + env: false, + }, + }, + }, + ), +); diff --git a/src/workers/policy.worker.ts b/src/workers/policy.worker.ts new file mode 100644 index 00000000..146a116c --- /dev/null +++ b/src/workers/policy.worker.ts @@ -0,0 +1,19 @@ +import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify'; +import { ReadOnlyPolicy } from '@nostrify/nostrify/policies'; +import * as Comlink from 'comlink'; + +export class CustomPolicy implements NPolicy { + private policy: NPolicy = new ReadOnlyPolicy(); + + // deno-lint-ignore require-await + async call(event: NostrEvent): Promise { + return this.policy.call(event); + } + + async import(path: string): Promise { + const Policy = (await import(path)).default; + this.policy = new Policy(); + } +} + +Comlink.expose(new CustomPolicy()); From 0b6b62f3b38ef5b1065d1b5e6a3ca9644a8bb72f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 20:27:54 -0500 Subject: [PATCH 196/252] policyWorker: import deno-safe-fetch --- deno.json | 1 + src/deps.ts | 2 +- src/workers/policy.worker.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 399f4743..c8eba2a5 100644 --- a/deno.json +++ b/deno.json @@ -34,6 +34,7 @@ "@std/media-types": "jsr:@std/media-types@^0.224.0", "@std/streams": "jsr:@std/streams@^0.223.0", "comlink": "npm:comlink@^4.4.1", + "deno-safe-fetch": "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts", "fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0", "formdata-helper": "npm:formdata-helper@^0.3.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", diff --git a/src/deps.ts b/src/deps.ts index 7a8fa9aa..46d8fecc 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,4 +1,4 @@ -import 'https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts'; +import 'deno-safe-fetch'; // @deno-types="npm:@types/lodash@4.14.194" export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; // @deno-types="npm:@types/mime@3.0.0" diff --git a/src/workers/policy.worker.ts b/src/workers/policy.worker.ts index 146a116c..4e4bcae6 100644 --- a/src/workers/policy.worker.ts +++ b/src/workers/policy.worker.ts @@ -1,3 +1,4 @@ +import 'deno-safe-fetch'; import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify'; import { ReadOnlyPolicy } from '@nostrify/nostrify/policies'; import * as Comlink from 'comlink'; From f14b64b003fd2849a1b59373a7d7837e1488fe2f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 15 May 2024 20:35:00 -0500 Subject: [PATCH 197/252] Remove useless policy dir --- Dockerfile | 2 +- data/.gitignore | 3 +-- data/policy/.gitignore | 2 -- src/workers/policy.ts | 6 ++---- 4 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 data/policy/.gitignore diff --git a/Dockerfile b/Dockerfile index a0e21946..f8df8159 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM denoland/deno:1.43.3 EXPOSE 4036 WORKDIR /app -RUN mkdir -p data/policy && chown -R deno data +RUN mkdir -p data && chown -R deno data USER deno COPY . . RUN deno cache src/server.ts diff --git a/data/.gitignore b/data/.gitignore index 3c46d845..c96a04f0 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,3 +1,2 @@ * -!.gitignore -!/policy \ No newline at end of file +!.gitignore \ No newline at end of file diff --git a/data/policy/.gitignore b/data/policy/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/data/policy/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/src/workers/policy.ts b/src/workers/policy.ts index 3cc03c9c..e3926675 100644 --- a/src/workers/policy.ts +++ b/src/workers/policy.ts @@ -3,8 +3,6 @@ import * as Comlink from 'comlink'; import { Conf } from '@/config.ts'; import type { CustomPolicy } from '@/workers/policy.worker.ts'; -const policyDir = new URL('../../data/policy', import.meta.url).pathname; - export const policyWorker = Comlink.wrap( new Worker( new URL('./policy.worker.ts', import.meta.url), @@ -12,8 +10,8 @@ export const policyWorker = Comlink.wrap( type: 'module', deno: { permissions: { - read: [Conf.policy, policyDir], - write: [policyDir], + read: [Conf.policy], + write: false, net: 'inherit', env: false, }, From 0acde23c46b5d04c19da317d7887c6f517628153 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 08:08:36 -0500 Subject: [PATCH 198/252] Port 8000 -> 4036 in all the places --- installation/ditto.conf | 2 +- src/config.ts | 2 +- src/storages/EventsDB.test.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/installation/ditto.conf b/installation/ditto.conf index 2a491734..afdf65c0 100644 --- a/installation/ditto.conf +++ b/installation/ditto.conf @@ -3,7 +3,7 @@ # Edit this file to change occurences of "example.com" to your own domain. upstream ditto { - server 127.0.0.1:8000; + server 127.0.0.1:4036; } upstream ipfs_gateway { diff --git a/src/config.ts b/src/config.ts index 5a30c0e6..6107547e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -55,7 +55,7 @@ class Conf { } /** Origin of the Ditto server, including the protocol and port. */ static get localDomain() { - return Deno.env.get('LOCAL_DOMAIN') || 'http://localhost:8000'; + return Deno.env.get('LOCAL_DOMAIN') || 'http://localhost:4036'; } /** URL to an external Nostr viewer. */ static get externalDomain() { diff --git a/src/storages/EventsDB.test.ts b/src/storages/EventsDB.test.ts index d1986b7d..10f0d117 100644 --- a/src/storages/EventsDB.test.ts +++ b/src/storages/EventsDB.test.ts @@ -33,15 +33,15 @@ Deno.test('query events with domain search filter', async () => { await eventsDB.event(event1); assertEquals(await eventsDB.query([{}]), [event1]); - assertEquals(await eventsDB.query([{ search: 'domain:localhost:8000' }]), []); + assertEquals(await eventsDB.query([{ search: 'domain:localhost:4036' }]), []); assertEquals(await eventsDB.query([{ search: '' }]), [event1]); await kysely .insertInto('pubkey_domains') - .values({ pubkey: event1.pubkey, domain: 'localhost:8000', last_updated_at: event1.created_at }) + .values({ pubkey: event1.pubkey, domain: 'localhost:4036', last_updated_at: event1.created_at }) .execute(); - assertEquals(await eventsDB.query([{ kinds: [1], search: 'domain:localhost:8000' }]), [event1]); + assertEquals(await eventsDB.query([{ kinds: [1], search: 'domain:localhost:4036' }]), [event1]); assertEquals(await eventsDB.query([{ kinds: [1], search: 'domain:example.com' }]), []); }); From 4b07f2a12a6b31fcfd062e477e35f58811afba75 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 08:10:13 -0500 Subject: [PATCH 199/252] Actually, set default LOCAL_DOMAIN based on PORT --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 6107547e..6fe62b9e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -55,7 +55,7 @@ class Conf { } /** Origin of the Ditto server, including the protocol and port. */ static get localDomain() { - return Deno.env.get('LOCAL_DOMAIN') || 'http://localhost:4036'; + return Deno.env.get('LOCAL_DOMAIN') || `http://localhost:${Conf.port}`; } /** URL to an external Nostr viewer. */ static get externalDomain() { From e61cbecb3e840100d6a4561a795f6ccab17d5cb4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 16 May 2024 10:29:14 -0300 Subject: [PATCH 200/252] refactor(unreblog): update error messages and query with Storages.db() --- src/controllers/api/statuses.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 98173b0b..984dfdbc 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -261,21 +261,19 @@ const reblogStatusController: AppController = async (c) => { const unreblogStatusController: AppController = async (c) => { const eventId = c.req.param('id'); const pubkey = await c.get('signer')?.getPublicKey()!; - - const event = await getEvent(eventId, { kind: 1 }); - - if (!event) { - return c.json({ error: 'Event not found.' }, 404); - } - const store = await Storages.db(); + const [event] = await store.query([{ ids: [eventId], kinds: [1] }]); + if (!event) { + return c.json({ error: 'Record not found' }, 404); + } + const [repostedEvent] = await store.query( [{ kinds: [6], authors: [pubkey], '#e': [event.id], limit: 1 }], ); if (!repostedEvent) { - return c.json({ error: 'Event not found.' }, 404); + return c.json({ error: 'Record not found' }, 404); } await createEvent({ From 2ede439005fab791af01ebe36d63a0755b5c68d3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 09:27:22 -0500 Subject: [PATCH 201/252] Upgrade Nostrify to v0.19.1, fix phantom deletions --- deno.json | 2 +- src/db/DittoDB.ts | 2 +- src/pipeline.ts | 21 ------- src/storages/EventsDB.test.ts | 106 +++++++++++++++++++++++++++++++--- src/storages/EventsDB.ts | 10 ++++ 5 files changed, 110 insertions(+), 31 deletions(-) diff --git a/deno.json b/deno.json index c8eba2a5..25ec2ed2 100644 --- a/deno.json +++ b/deno.json @@ -21,7 +21,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.0", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.1", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/db/DittoDB.ts b/src/db/DittoDB.ts index 9c3b280b..68fdc627 100644 --- a/src/db/DittoDB.ts +++ b/src/db/DittoDB.ts @@ -41,7 +41,7 @@ export class DittoDB { } /** Migrate the database to the latest version. */ - private static async migrate(kysely: Kysely) { + static async migrate(kysely: Kysely) { const migrator = new Migrator({ db: kysely, provider: new FileMigrationProvider({ diff --git a/src/pipeline.ts b/src/pipeline.ts index 6f487ed4..6e4ebb11 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -45,7 +45,6 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - if (event.kind === 5) { - const ids = getTagSet(event.tags, 'e'); - const store = await Storages.db(); - - if (event.pubkey === Conf.pubkey) { - await store.remove([{ ids: [...ids] }], { signal }); - } else { - const events = await store.query( - [{ ids: [...ids], authors: [event.pubkey] }], - { signal }, - ); - - const deleteIds = events.map(({ id }) => id); - await store.remove([{ ids: deleteIds }], { signal }); - } - } -} - /** Track whenever a hashtag is used, for processing trending tags. */ async function trackHashtags(event: NostrEvent): Promise { const date = nostrDate(event.created_at); diff --git a/src/storages/EventsDB.test.ts b/src/storages/EventsDB.test.ts index 10f0d117..95c446c7 100644 --- a/src/storages/EventsDB.test.ts +++ b/src/storages/EventsDB.test.ts @@ -1,22 +1,39 @@ +import { Database as Sqlite } from '@db/sqlite'; +import { DenoSqlite3Dialect } from '@soapbox/kysely-deno-sqlite'; import { assertEquals, assertRejects } from '@std/assert'; +import { Kysely } from 'kysely'; +import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; +import { DittoTables } from '@/db/DittoTables.ts'; +import { EventsDB } from '@/storages/EventsDB.ts'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; -import { EventsDB } from '@/storages/EventsDB.ts'; - -const kysely = await DittoDB.getInstance(); -const eventsDB = new EventsDB(kysely); +/** Create in-memory database for testing. */ +const createDB = async () => { + const kysely = new Kysely({ + dialect: new DenoSqlite3Dialect({ + database: new Sqlite(':memory:'), + }), + }); + const eventsDB = new EventsDB(kysely); + await DittoDB.migrate(kysely); + return { eventsDB, kysely }; +}; Deno.test('count filters', async () => { + const { eventsDB } = await createDB(); + assertEquals((await eventsDB.count([{ kinds: [1] }])).count, 0); await eventsDB.event(event1); assertEquals((await eventsDB.count([{ kinds: [1] }])).count, 1); }); Deno.test('insert and filter events', async () => { + const { eventsDB } = await createDB(); + await eventsDB.event(event1); assertEquals(await eventsDB.query([{ kinds: [1] }]), [event1]); @@ -30,6 +47,8 @@ Deno.test('insert and filter events', async () => { }); Deno.test('query events with domain search filter', async () => { + const { eventsDB, kysely } = await createDB(); + await eventsDB.event(event1); assertEquals(await eventsDB.query([{}]), [event1]); @@ -46,13 +65,84 @@ Deno.test('query events with domain search filter', async () => { }); Deno.test('delete events', async () => { - await eventsDB.event(event1); - assertEquals(await eventsDB.query([{ kinds: [1] }]), [event1]); - await eventsDB.remove([{ kinds: [1] }]); - assertEquals(await eventsDB.query([{ kinds: [1] }]), []); + const { eventsDB } = await createDB(); + + const [one, two] = [ + { id: '1', kind: 1, pubkey: 'abc', content: 'hello world', created_at: 1, sig: '', tags: [] }, + { id: '2', kind: 1, pubkey: 'abc', content: 'yolo fam', created_at: 2, sig: '', tags: [] }, + ]; + + await eventsDB.event(one); + await eventsDB.event(two); + + // Sanity check + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two, one]); + + await eventsDB.event({ + kind: 5, + pubkey: one.pubkey, + tags: [['e', one.id]], + created_at: 0, + content: '', + id: '', + sig: '', + }); + + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two]); +}); + +Deno.test("user cannot delete another user's event", async () => { + const { eventsDB } = await createDB(); + + const event = { id: '1', kind: 1, pubkey: 'abc', content: 'hello world', created_at: 1, sig: '', tags: [] }; + await eventsDB.event(event); + + // Sanity check + assertEquals(await eventsDB.query([{ kinds: [1] }]), [event]); + + await eventsDB.event({ + kind: 5, + pubkey: 'def', // different pubkey + tags: [['e', event.id]], + created_at: 0, + content: '', + id: '', + sig: '', + }); + + assertEquals(await eventsDB.query([{ kinds: [1] }]), [event]); +}); + +Deno.test('admin can delete any event', async () => { + const { eventsDB } = await createDB(); + + const [one, two] = [ + { id: '1', kind: 1, pubkey: 'abc', content: 'hello world', created_at: 1, sig: '', tags: [] }, + { id: '2', kind: 1, pubkey: 'abc', content: 'yolo fam', created_at: 2, sig: '', tags: [] }, + ]; + + await eventsDB.event(one); + await eventsDB.event(two); + + // Sanity check + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two, one]); + + await eventsDB.event({ + kind: 5, + pubkey: Conf.pubkey, // Admin pubkey + tags: [['e', one.id]], + created_at: 0, + content: '', + id: '', + sig: '', + }); + + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two]); }); Deno.test('inserting replaceable events', async () => { + const { eventsDB } = await createDB(); + assertEquals((await eventsDB.count([{ kinds: [0], authors: [event0.pubkey] }])).count, 0); await eventsDB.event(event0); diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 5f1acbb7..f2789d33 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -8,6 +8,7 @@ import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { normalizeFilters } from '@/filter.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; +import { getTagSet } from '@/tags.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; @@ -51,9 +52,18 @@ class EventsDB implements NStore { async event(event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise { event = purifyEvent(event); this.console.debug('EVENT', JSON.stringify(event)); + await this.deleteEventsAdmin(event); return this.store.event(event); } + /** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */ + async deleteEventsAdmin(event: NostrEvent): Promise { + if (event.kind === 5 && event.pubkey === Conf.pubkey) { + const ids = getTagSet(event.tags, 'e'); + await this.remove([{ ids: [...ids] }]); + } + } + /** Get events for filters from the database. */ async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { filters = await this.expandFilters(filters); From 4df2c7ba9c69b259d2a98e3e46b2ac6652efb45e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 10:29:14 -0500 Subject: [PATCH 202/252] Improve EventsDB error handling --- src/pipeline.ts | 13 ++------- src/storages/EventsDB.test.ts | 52 ++++++++++++++++++++++++++++++----- src/storages/EventsDB.ts | 29 +++++++++++++++++-- src/test.ts | 16 +++++++++++ 4 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/pipeline.ts b/src/pipeline.ts index 6e4ebb11..15d495e7 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -122,17 +122,8 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise { @@ -140,16 +143,51 @@ Deno.test('admin can delete any event', async () => { assertEquals(await eventsDB.query([{ kinds: [1] }]), [two]); }); +Deno.test('throws a RelayError when inserting an event deleted by the admin', async () => { + const { eventsDB } = await createDB(); + + const event = genEvent(); + await eventsDB.event(event); + + const deletion = genEvent({ kind: 5, tags: [['e', event.id]] }, Conf.seckey); + await eventsDB.event(deletion); + + await assertRejects( + () => eventsDB.event(event), + RelayError, + 'event deleted by admin', + ); +}); + +Deno.test('throws a RelayError when inserting an event deleted by a user', async () => { + const { eventsDB } = await createDB(); + + const sk = generateSecretKey(); + + const event = genEvent({}, sk); + await eventsDB.event(event); + + const deletion = genEvent({ kind: 5, tags: [['e', event.id]] }, sk); + await eventsDB.event(deletion); + + await assertRejects( + () => eventsDB.event(event), + RelayError, + 'event deleted by user', + ); +}); + Deno.test('inserting replaceable events', async () => { const { eventsDB } = await createDB(); - assertEquals((await eventsDB.count([{ kinds: [0], authors: [event0.pubkey] }])).count, 0); + const event = event0; + await eventsDB.event(event); - await eventsDB.event(event0); - await assertRejects(() => eventsDB.event(event0)); - assertEquals((await eventsDB.count([{ kinds: [0], authors: [event0.pubkey] }])).count, 1); + const olderEvent = { ...event, id: '123', created_at: event.created_at - 1 }; + await eventsDB.event(olderEvent); + assertEquals(await eventsDB.query([{ kinds: [0], authors: [event.pubkey] }]), [event]); - const changeEvent = { ...event0, id: '123', created_at: event0.created_at + 1 }; - await eventsDB.event(changeEvent); - assertEquals(await eventsDB.query([{ kinds: [0] }]), [changeEvent]); + const newerEvent = { ...event, id: '123', created_at: event.created_at + 1 }; + await eventsDB.event(newerEvent); + assertEquals(await eventsDB.query([{ kinds: [0] }]), [newerEvent]); }); diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index f2789d33..aac8e522 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -11,6 +11,7 @@ import { purifyEvent } from '@/storages/hydrate.ts'; import { getTagSet } from '@/tags.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; +import { RelayError } from '@/RelayError.ts'; /** Function to decide whether or not to index a tag. */ type TagCondition = ({ event, count, value }: { @@ -52,12 +53,36 @@ class EventsDB implements NStore { async event(event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise { event = purifyEvent(event); this.console.debug('EVENT', JSON.stringify(event)); + + if (await this.isDeletedAdmin(event)) { + throw new RelayError('blocked', 'event deleted by admin'); + } + await this.deleteEventsAdmin(event); - return this.store.event(event); + + try { + await this.store.event(event); + } catch (e) { + if (e.message === 'Cannot add a deleted event') { + throw new RelayError('blocked', 'event deleted by user'); + } else if (e.message === 'Cannot replace an event with an older event') { + return; + } else { + this.console.debug('ERROR', e.message); + } + } + } + + /** Check if an event has been deleted by the admin. */ + private async isDeletedAdmin(event: NostrEvent): Promise { + const [deletion] = await this.query([ + { kinds: [5], authors: [Conf.pubkey], '#e': [event.id], limit: 1 }, + ]); + return !!deletion; } /** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */ - async deleteEventsAdmin(event: NostrEvent): Promise { + private async deleteEventsAdmin(event: NostrEvent): Promise { if (event.kind === 5 && event.pubkey === Conf.pubkey) { const ids = getTagSet(event.tags, 'e'); await this.remove([{ ids: [...ids] }]); diff --git a/src/test.ts b/src/test.ts index 45862828..ea9c8fa4 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,7 +1,23 @@ import { NostrEvent } from '@nostrify/nostrify'; +import { finalizeEvent, generateSecretKey } from 'nostr-tools'; + +import { purifyEvent } from '@/storages/hydrate.ts'; /** Import an event fixture by name in tests. */ export async function eventFixture(name: string): Promise { const result = await import(`~/fixtures/events/${name}.json`, { with: { type: 'json' } }); return structuredClone(result.default); } + +/** Generate an event for use in tests. */ +export function genEvent(t: Partial = {}, sk: Uint8Array = generateSecretKey()): NostrEvent { + const event = finalizeEvent({ + kind: 255, + created_at: 0, + content: '', + tags: [], + ...t, + }, sk); + + return purifyEvent(event); +} From 031a3eac04b686dc53202fdc0177c90d1b82d1de Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 10:30:54 -0500 Subject: [PATCH 203/252] EventsDB.test: import order --- src/storages/EventsDB.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/EventsDB.test.ts b/src/storages/EventsDB.test.ts index 939406ae..2f343791 100644 --- a/src/storages/EventsDB.test.ts +++ b/src/storages/EventsDB.test.ts @@ -2,6 +2,7 @@ import { Database as Sqlite } from '@db/sqlite'; import { DenoSqlite3Dialect } from '@soapbox/kysely-deno-sqlite'; import { assertEquals, assertRejects } from '@std/assert'; import { Kysely } from 'kysely'; +import { generateSecretKey } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; @@ -12,7 +13,6 @@ import { genEvent } from '@/test.ts'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; -import { generateSecretKey } from 'nostr-tools'; /** Create in-memory database for testing. */ const createDB = async () => { From 6c3f0849b231400dea4b78f0ea9b2a0fbb943d85 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 12:57:01 -0500 Subject: [PATCH 204/252] Upgrade Nostrify to v0.19.2, fix crash on mixed filters --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 25ec2ed2..b783f646 100644 --- a/deno.json +++ b/deno.json @@ -21,7 +21,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.1", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.2", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", From 00d4bf23448826fffdcacdc449270ba86a4e911e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 15:42:32 -0500 Subject: [PATCH 205/252] Upgrade Nostrify to v0.20.0, enable Postgres FTS --- deno.json | 2 +- src/db/migrations/020_pgfts.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/db/migrations/020_pgfts.ts diff --git a/deno.json b/deno.json index b783f646..e8719a46 100644 --- a/deno.json +++ b/deno.json @@ -21,7 +21,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.2", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.20.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/db/migrations/020_pgfts.ts b/src/db/migrations/020_pgfts.ts new file mode 100644 index 00000000..8b3cfa0c --- /dev/null +++ b/src/db/migrations/020_pgfts.ts @@ -0,0 +1,19 @@ +import { Kysely, sql } from 'kysely'; + +import { Conf } from '@/config.ts'; + +export async function up(db: Kysely): Promise { + if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + await db.schema.createTable('nostr_pgfts') + .ifNotExists() + .addColumn('event_id', 'text', (c) => c.primaryKey().references('nostr_events.id').onDelete('cascade')) + .addColumn('search_vec', sql`tsvector`, (c) => c.notNull()) + .execute(); + } +} + +export async function down(db: Kysely): Promise { + if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + await db.schema.dropTable('nostr_pgfts').ifExists().execute(); + } +} From baa698688094195ed3533c50b0eb2450a06180ae Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 15:48:22 -0500 Subject: [PATCH 206/252] EventsDB: enable fts conditionally based on DATABASE_URL --- src/storages/EventsDB.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index aac8e522..ef51a892 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -42,8 +42,17 @@ class EventsDB implements NStore { }; constructor(private kysely: Kysely) { + let fts: 'sqlite' | 'postgres' | undefined; + + if (Conf.databaseUrl.protocol === 'sqlite:') { + fts = 'sqlite'; + } + if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + fts = 'postgres'; + } + this.store = new NDatabase(kysely, { - fts5: Conf.databaseUrl.protocol === 'sqlite:', + fts, indexTags: EventsDB.indexTags, searchText: EventsDB.searchText, }); From 5aacbe7af5717b2d032064dbd18fd333e55cf8fe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 16 May 2024 18:53:04 -0500 Subject: [PATCH 207/252] Fix media uploads due to 'awaiting' a query builder instance --- src/controllers/api/statuses.ts | 4 +++- src/db/unattached-media.ts | 15 +++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 984dfdbc..00f9a98e 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { addTag, deleteTag } from '@/tags.ts'; @@ -56,6 +57,7 @@ const statusController: AppController = async (c) => { const createStatusController: AppController = async (c) => { const body = await parseBody(c.req.raw); const result = createStatusSchema.safeParse(body); + const kysely = await DittoDB.getInstance(); if (!result.success) { return c.json({ error: 'Bad request', schema: result.error }, 400); @@ -92,7 +94,7 @@ const createStatusController: AppController = async (c) => { const viewerPubkey = await c.get('signer')?.getPublicKey(); if (data.media_ids?.length) { - const media = await getUnattachedMediaByIds(data.media_ids) + const media = await getUnattachedMediaByIds(kysely, data.media_ids) .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) .then((media) => media.map(({ url, data }) => ['media', url, data])); diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 80636283..cee1e3a3 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -1,6 +1,8 @@ +import { Kysely } from 'kysely'; import uuid62 from 'uuid62'; import { DittoDB } from '@/db/DittoDB.ts'; +import { DittoTables } from '@/db/DittoTables.ts'; import { type MediaData } from '@/schemas/nostr.ts'; interface UnattachedMedia { @@ -28,8 +30,7 @@ async function insertUnattachedMedia(media: Omit) { return kysely.selectFrom('unattached_media') .select([ 'unattached_media.id', @@ -41,9 +42,8 @@ async function selectUnattachedMediaQuery() { } /** Find attachments that exist but aren't attached to any events. */ -async function getUnattachedMedia(until: Date) { - const query = await selectUnattachedMediaQuery(); - return query +function getUnattachedMedia(kysely: Kysely, until: Date) { + return selectUnattachedMediaQuery(kysely) .leftJoin('nostr_tags', 'unattached_media.url', 'nostr_tags.value') .where('uploaded_at', '<', until.getTime()) .execute(); @@ -58,10 +58,9 @@ async function deleteUnattachedMediaByUrl(url: string) { } /** Get unattached media by IDs. */ -async function getUnattachedMediaByIds(ids: string[]) { +async function getUnattachedMediaByIds(kysely: Kysely, ids: string[]) { if (!ids.length) return []; - const query = await selectUnattachedMediaQuery(); - return query + return await selectUnattachedMediaQuery(kysely) .where('id', 'in', ids) .execute(); } From 2aee2e6bf6facc883b1364af8f630ec88a962cfa Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 17 May 2024 09:45:19 -0300 Subject: [PATCH 208/252] fix(renderReblog): render account from pubkey if there is no kind 0 --- src/views/mastodon/statuses.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 776f0169..aefe258b 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -123,8 +123,6 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< async function renderReblog(event: DittoEvent, opts: RenderStatusOpts) { const { viewerPubkey } = opts; - if (!event.author) return; - const repostId = event.tags.find(([name]) => name === 'e')?.[1]; if (!repostId) return; @@ -134,7 +132,7 @@ async function renderReblog(event: DittoEvent, opts: RenderStatusOpts) { return { id: event.id, - account: await renderAccount(event.author), + account: event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey), reblogged: true, reblog, }; From 4cc1d13d44b50dc31ad9af93f050eb19ab9d3b83 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 17 May 2024 11:25:17 -0300 Subject: [PATCH 209/252] fix: render followers & following list when no kind 0 --- src/views.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/views.ts b/src/views.ts index a7375429..863dd30c 100644 --- a/src/views.ts +++ b/src/views.ts @@ -5,6 +5,7 @@ import { renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; +import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; /** Render account objects for the author of each event. */ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal = AbortSignal.timeout(1000)) { @@ -24,7 +25,13 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal .then((events) => hydrateEvents({ events, store, signal })); const accounts = await Promise.all( - authors.map((event) => renderAccount(event)), + Array.from(pubkeys).map(async (pubkey) => { + const event = authors.find((event) => event.pubkey === pubkey); + if (event) { + return await renderAccount(event); + } + return await accountFromPubkey(pubkey); + }), ); return paginated(c, events, accounts); @@ -39,7 +46,13 @@ async function renderAccounts(c: AppContext, authors: string[], signal = AbortSi .then((events) => hydrateEvents({ events, store, signal })); const accounts = await Promise.all( - events.map((event) => renderAccount(event)), + authors.map(async (pubkey) => { + const event = events.find((event) => event.pubkey === pubkey); + if (event) { + return await renderAccount(event); + } + return await accountFromPubkey(pubkey); + }), ); return paginated(c, events, accounts); From 251500fba1b82de21098776c4d98bfd0d6a40660 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 11:39:21 -0500 Subject: [PATCH 210/252] Never let stats be less than 0 --- src/storages/hydrate.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 8d2d3027..9b958416 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -251,11 +251,19 @@ async function gatherAuthorStats(events: DittoEvent[]): Promise ({ + pubkey: row.pubkey, + followers_count: Math.max(0, row.followers_count), + following_count: Math.max(0, row.following_count), + notes_count: Math.max(0, row.notes_count), + })); } /** Collect event stats from the events. */ @@ -271,11 +279,19 @@ async function gatherEventStats(events: DittoEvent[]): Promise ({ + event_id: row.event_id, + reposts_count: Math.max(0, row.reposts_count), + reactions_count: Math.max(0, row.reactions_count), + replies_count: Math.max(0, row.replies_count), + })); } /** Return a normalized event without any non-standard keys. */ From a39910fa984e5f4896aa860143af07c1044e0271 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 13:12:40 -0500 Subject: [PATCH 211/252] Add a function to recalculate author stats --- scripts/stats-recompute.ts | 25 ++----------------------- src/stats.ts | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/scripts/stats-recompute.ts b/scripts/stats-recompute.ts index dcb0bc07..4037a85b 100644 --- a/scripts/stats-recompute.ts +++ b/scripts/stats-recompute.ts @@ -1,8 +1,6 @@ import { nip19 } from 'nostr-tools'; -import { DittoDB } from '@/db/DittoDB.ts'; -import { DittoTables } from '@/db/DittoTables.ts'; -import { Storages } from '@/storages.ts'; +import { refreshAuthorStats } from '@/stats.ts'; let pubkey: string; try { @@ -17,23 +15,4 @@ try { Deno.exit(1); } -const store = await Storages.db(); -const kysely = await DittoDB.getInstance(); - -const [followList] = await store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]); - -const authorStats: DittoTables['author_stats'] = { - pubkey, - followers_count: (await store.count([{ kinds: [3], '#p': [pubkey] }])).count, - following_count: followList?.tags.filter(([name]) => name === 'p')?.length ?? 0, - notes_count: (await store.count([{ kinds: [1], authors: [pubkey] }])).count, -}; - -await kysely.insertInto('author_stats') - .values(authorStats) - .onConflict((oc) => - oc - .column('pubkey') - .doUpdateSet(authorStats) - ) - .execute(); +await refreshAuthorStats(pubkey); diff --git a/src/stats.ts b/src/stats.ts index 92040710..74242f7a 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,11 +1,12 @@ -import { NKinds, NostrEvent } from '@nostrify/nostrify'; +import { NKinds, NostrEvent, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { InsertQueryBuilder, Kysely } from 'kysely'; +import { SetRequired } from 'type-fest'; import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Storages } from '@/storages.ts'; -import { findReplyTag } from '@/tags.ts'; +import { findReplyTag, getTagSet } from '@/tags.ts'; type AuthorStat = keyof Omit; type EventStat = keyof Omit; @@ -216,4 +217,35 @@ function getFollowDiff(event: NostrEvent, prev?: NostrEvent): AuthorStatDiff[] { ]; } -export { updateStats }; +/** Refresh the author's stats in the database. */ +async function refreshAuthorStats(pubkey: string): Promise { + const store = await Storages.db(); + const stats = await countAuthorStats(store, pubkey); + + const kysely = await DittoDB.getInstance(); + await kysely.insertInto('author_stats') + .values(stats) + .onConflict((oc) => oc.column('pubkey').doUpdateSet(stats)) + .execute(); +} + +/** Calculate author stats from the database. */ +async function countAuthorStats( + store: SetRequired, + pubkey: string, +): Promise { + const [{ count: followers_count }, { count: notes_count }, [followList]] = await Promise.all([ + store.count([{ kinds: [3], '#p': [pubkey] }]), + store.count([{ kinds: [1], authors: [pubkey] }]), + store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]), + ]); + + return { + pubkey, + followers_count, + following_count: getTagSet(followList?.tags ?? [], 'p').size, + notes_count, + }; +} + +export { refreshAuthorStats, updateStats }; From 6995bd2b292810913c6c8d5877ea8f76b91e6b42 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 16:23:23 -0500 Subject: [PATCH 212/252] Upgrade Deno to the latest version --- .gitlab-ci.yml | 2 +- .tool-versions | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9dee457..8e728884 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: denoland/deno:1.41.3 +image: denoland/deno:1.43.4 default: interruptible: true diff --git a/.tool-versions b/.tool-versions index a13fd5ff..9bbaf96d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -deno 1.41.3 \ No newline at end of file +deno 1.43.4 \ No newline at end of file From ae9516b445332df74aa817a26e5be522e243a923 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 16:23:38 -0500 Subject: [PATCH 213/252] refreshAuthorStats: return the stats --- src/stats.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stats.ts b/src/stats.ts index 74242f7a..08d4bb99 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -218,7 +218,7 @@ function getFollowDiff(event: NostrEvent, prev?: NostrEvent): AuthorStatDiff[] { } /** Refresh the author's stats in the database. */ -async function refreshAuthorStats(pubkey: string): Promise { +async function refreshAuthorStats(pubkey: string): Promise { const store = await Storages.db(); const stats = await countAuthorStats(store, pubkey); @@ -227,6 +227,8 @@ async function refreshAuthorStats(pubkey: string): Promise { .values(stats) .onConflict((oc) => oc.column('pubkey').doUpdateSet(stats)) .execute(); + + return stats; } /** Calculate author stats from the database. */ From 17b633019339506d2414bc27056531088544499a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 16:59:45 -0500 Subject: [PATCH 214/252] Downgrade Deno to v1.43.3 due to TypeScript issues --- .gitlab-ci.yml | 2 +- .tool-versions | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e728884..b2140dbf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: denoland/deno:1.43.4 +image: denoland/deno:1.43.3 default: interruptible: true diff --git a/.tool-versions b/.tool-versions index 9bbaf96d..b3e19cd6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -deno 1.43.4 \ No newline at end of file +deno 1.43.3 \ No newline at end of file From 5c2e3450a9ae2b06645f570ef2059109120a648f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 17:50:30 -0500 Subject: [PATCH 215/252] Refresh author stats: less naive way --- src/storages/hydrate.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 9b958416..7b964d77 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -1,10 +1,12 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; +import { LRUCache } from 'lru-cache'; import { matchFilter } from 'nostr-tools'; import { DittoDB } from '@/db/DittoDB.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Conf } from '@/config.ts'; +import { refreshAuthorStats } from '@/stats.ts'; interface HydrateOpts { events: DittoEvent[]; @@ -55,6 +57,8 @@ async function hydrateEvents(opts: HydrateOpts): Promise { events: await gatherEventStats(cache), }; + requestMissingAuthorStats(events, stats.authors); + // Dedupe events. const results = [...new Map(cache.map((event) => [event.id, event])).values()]; @@ -266,6 +270,31 @@ async function gatherAuthorStats(events: DittoEvent[]): Promise( + events + .filter((event) => event.kind === 0) + .map((event) => event.pubkey), + ); + + const missing = pubkeys.difference( + new Set(stats.map((stat) => stat.pubkey)), + ); + + for (const pubkey of missing) { + refreshAuthorStatsDebounced(pubkey); + } +} + +const lru = new LRUCache({ max: 1000 }); + +/** Calls `refreshAuthorStats` only once per author. */ +function refreshAuthorStatsDebounced(pubkey: string): void { + if (lru.get(pubkey)) return; + lru.set(pubkey, true); + refreshAuthorStats(pubkey).catch(() => {}); +} + /** Collect event stats from the events. */ async function gatherEventStats(events: DittoEvent[]): Promise { const ids = new Set( From bf479d01625497eac9190286eaf383f381ca606b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 18:26:55 -0500 Subject: [PATCH 216/252] Move refreshAuthorStatsDebounced to stats.ts --- src/stats.ts | 12 +++++++++++- src/storages/hydrate.ts | 16 +++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/stats.ts b/src/stats.ts index 08d4bb99..43647818 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,6 +1,7 @@ import { NKinds, NostrEvent, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { InsertQueryBuilder, Kysely } from 'kysely'; +import { LRUCache } from 'lru-cache'; import { SetRequired } from 'type-fest'; import { DittoDB } from '@/db/DittoDB.ts'; @@ -250,4 +251,13 @@ async function countAuthorStats( }; } -export { refreshAuthorStats, updateStats }; +const lru = new LRUCache({ max: 1000 }); + +/** Calls `refreshAuthorStats` only once per author. */ +function refreshAuthorStatsDebounced(pubkey: string): void { + if (lru.get(pubkey)) return; + lru.set(pubkey, true); + refreshAuthorStats(pubkey).catch(() => {}); +} + +export { refreshAuthorStats, refreshAuthorStatsDebounced, updateStats }; diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 7b964d77..e5c488e3 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -1,12 +1,11 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; -import { LRUCache } from 'lru-cache'; import { matchFilter } from 'nostr-tools'; import { DittoDB } from '@/db/DittoDB.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Conf } from '@/config.ts'; -import { refreshAuthorStats } from '@/stats.ts'; +import { refreshAuthorStatsDebounced } from '@/stats.ts'; interface HydrateOpts { events: DittoEvent[]; @@ -57,7 +56,7 @@ async function hydrateEvents(opts: HydrateOpts): Promise { events: await gatherEventStats(cache), }; - requestMissingAuthorStats(events, stats.authors); + refreshMissingAuthorStats(events, stats.authors); // Dedupe events. const results = [...new Map(cache.map((event) => [event.id, event])).values()]; @@ -270,7 +269,7 @@ async function gatherAuthorStats(events: DittoEvent[]): Promise( events .filter((event) => event.kind === 0) @@ -286,15 +285,6 @@ function requestMissingAuthorStats(events: NostrEvent[], stats: DittoTables['aut } } -const lru = new LRUCache({ max: 1000 }); - -/** Calls `refreshAuthorStats` only once per author. */ -function refreshAuthorStatsDebounced(pubkey: string): void { - if (lru.get(pubkey)) return; - lru.set(pubkey, true); - refreshAuthorStats(pubkey).catch(() => {}); -} - /** Collect event stats from the events. */ async function gatherEventStats(events: DittoEvent[]): Promise { const ids = new Set( From 23a366081fa19e4e75d411641b34ec4ff97e2fb4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 18:42:45 -0500 Subject: [PATCH 217/252] stats: maybe refresh stats when updating --- src/stats.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stats.ts b/src/stats.ts index 43647818..9f0d2573 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -39,6 +39,8 @@ async function updateStats(event: NostrEvent) { debug(JSON.stringify({ id: event.id, pubkey: event.pubkey, kind: event.kind, tags: event.tags, statDiffs })); } + pubkeyDiffs.forEach(([_, pubkey]) => refreshAuthorStatsDebounced(pubkey)); + const kysely = await DittoDB.getInstance(); if (pubkeyDiffs.length) queries.push(authorStatsQuery(kysely, pubkeyDiffs)); From f9a0055e78a582e26e88458bdc8ba1f5eaa6c798 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 May 2024 19:00:56 -0500 Subject: [PATCH 218/252] stats: add a Semaphore when refreshing author stats --- deno.json | 1 + src/stats.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/deno.json b/deno.json index e8719a46..84d5351b 100644 --- a/deno.json +++ b/deno.json @@ -20,6 +20,7 @@ "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@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/nostrify": "jsr:@nostrify/nostrify@^0.20.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", diff --git a/src/stats.ts b/src/stats.ts index 9f0d2573..71124961 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,3 +1,4 @@ +import { Semaphore } from '@lambdalisue/async'; import { NKinds, NostrEvent, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { InsertQueryBuilder, Kysely } from 'kysely'; @@ -253,13 +254,19 @@ async function countAuthorStats( }; } -const lru = new LRUCache({ max: 1000 }); +const authorStatsSemaphore = new Semaphore(10); +const refreshedAuthors = new LRUCache({ max: 1000 }); /** Calls `refreshAuthorStats` only once per author. */ function refreshAuthorStatsDebounced(pubkey: string): void { - if (lru.get(pubkey)) return; - lru.set(pubkey, true); - refreshAuthorStats(pubkey).catch(() => {}); + if (refreshedAuthors.get(pubkey)) { + return; + } + + refreshedAuthors.set(pubkey, true); + + authorStatsSemaphore + .lock(() => refreshAuthorStats(pubkey).catch(() => {})); } export { refreshAuthorStats, refreshAuthorStatsDebounced, updateStats }; From 3e93b42251db66e24ef25ba7bd3d2cc6aa91d0cb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 08:13:37 -0500 Subject: [PATCH 219/252] stats: add a debug call --- src/stats.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stats.ts b/src/stats.ts index 71124961..256c570e 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -264,6 +264,7 @@ function refreshAuthorStatsDebounced(pubkey: string): void { } refreshedAuthors.set(pubkey, true); + debug('refreshing author stats:', pubkey); authorStatsSemaphore .lock(() => refreshAuthorStats(pubkey).catch(() => {})); From 6ac4c072a6caeb9216c9c818e53e70d1249d69eb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 08:20:43 -0500 Subject: [PATCH 220/252] Fix crash decoding url --- src/note.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/note.ts b/src/note.ts index 5603c539..1c5c70cd 100644 --- a/src/note.ts +++ b/src/note.ts @@ -18,7 +18,7 @@ const linkifyOpts: linkify.Opts = { return `#${tag}`; }, url: ({ content }) => { - if (nip21.test(content)) { + try { const { decoded } = nip21.parse(content); const pubkey = getDecodedPubkey(decoded); if (pubkey) { @@ -28,7 +28,7 @@ const linkifyOpts: linkify.Opts = { } else { return ''; } - } else { + } catch { return `${content}`; } }, From 4c87e723c06c3df9bc059282c790d59bd72d53b6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 10:56:34 -0500 Subject: [PATCH 221/252] Bump nostrify to v0.21.1 --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 84d5351b..e40dd24c 100644 --- a/deno.json +++ b/deno.json @@ -22,7 +22,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/nostrify": "jsr:@nostrify/nostrify@^0.20.0", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.21.1", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", From f97064afb4bc1a6c05e430f4d4315ca94ed4f179 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 11:35:29 -0500 Subject: [PATCH 222/252] Remove dependency on npm:mime, switch to @std/media-types --- deno.json | 2 +- src/deps.ts | 2 -- src/note.ts | 9 +++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/deno.json b/deno.json index e40dd24c..54ba4680 100644 --- a/deno.json +++ b/deno.json @@ -32,7 +32,7 @@ "@std/dotenv": "jsr:@std/dotenv@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", "@std/json": "jsr:@std/json@^0.223.0", - "@std/media-types": "jsr:@std/media-types@^0.224.0", + "@std/media-types": "jsr:@std/media-types@^0.224.1", "@std/streams": "jsr:@std/streams@^0.223.0", "comlink": "npm:comlink@^4.4.1", "deno-safe-fetch": "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts", diff --git a/src/deps.ts b/src/deps.ts index 46d8fecc..12be07f1 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,8 +1,6 @@ import 'deno-safe-fetch'; // @deno-types="npm:@types/lodash@4.14.194" export { default as lodash } from 'https://esm.sh/lodash@4.17.21'; -// @deno-types="npm:@types/mime@3.0.0" -export { default as mime } from 'npm:mime@^3.0.0'; // @deno-types="npm:@types/sanitize-html@2.9.0" export { default as sanitizeHtml } from 'npm:sanitize-html@^2.11.0'; export { diff --git a/src/note.ts b/src/note.ts index 1c5c70cd..7dc39f5a 100644 --- a/src/note.ts +++ b/src/note.ts @@ -1,10 +1,10 @@ +import { typeByExtension } from '@std/media-types'; import 'linkify-plugin-hashtag'; import linkifyStr from 'linkify-string'; import linkify from 'linkifyjs'; import { nip19, nip21 } from 'nostr-tools'; import { Conf } from '@/config.ts'; -import { mime } from '@/deps.ts'; import { type DittoAttachment } from '@/views/mastodon/attachments.ts'; linkify.registerCustomProtocol('nostr', true); @@ -87,12 +87,13 @@ function isLinkURL(link: Link): boolean { return link.type === 'url'; } -/** `npm:mime` treats `.com` as a file extension, so parse the full URL to get its path first. */ +/** Get the extension from the URL, then get its type. */ function getUrlMimeType(url: string): string | undefined { try { const { pathname } = new URL(url); - return mime.getType(pathname) || undefined; - } catch (_e) { + const ext = pathname.split('.').pop() ?? ''; + return typeByExtension(ext); + } catch { return undefined; } } From 5997ff0fff38bf79486818a7e0f9ceae2182e4b6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 11:52:33 -0500 Subject: [PATCH 223/252] Create utils/media.ts, move some code from note.ts there --- src/note.ts | 25 ++++++------------------- src/utils/media.test.ts | 17 +++++++++++++++++ src/utils/media.ts | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 src/utils/media.test.ts create mode 100644 src/utils/media.ts diff --git a/src/note.ts b/src/note.ts index 7dc39f5a..71dc0174 100644 --- a/src/note.ts +++ b/src/note.ts @@ -1,10 +1,10 @@ -import { typeByExtension } from '@std/media-types'; import 'linkify-plugin-hashtag'; import linkifyStr from 'linkify-string'; import linkify from 'linkifyjs'; import { nip19, nip21 } from 'nostr-tools'; import { Conf } from '@/config.ts'; +import { getUrlMediaType, isPermittedMediaType } from '@/utils/media.ts'; import { type DittoAttachment } from '@/views/mastodon/attachments.ts'; linkify.registerCustomProtocol('nostr', true); @@ -60,16 +60,14 @@ function parseNoteContent(content: string): ParsedNoteContent { function getMediaLinks(links: Link[]): DittoAttachment[] { return links.reduce((acc, link) => { - const mimeType = getUrlMimeType(link.href); - if (!mimeType) return acc; + const mediaType = getUrlMediaType(link.href); + if (!mediaType) return acc; - const [baseType, _subType] = mimeType.split('/'); - - if (['audio', 'image', 'video'].includes(baseType)) { + if (isPermittedMediaType(mediaType, ['audio', 'image', 'video'])) { acc.push({ url: link.href, data: { - mime: mimeType, + mime: mediaType, }, }); } @@ -79,7 +77,7 @@ function getMediaLinks(links: Link[]): DittoAttachment[] { } function isNonMediaLink({ href }: Link): boolean { - return /^https?:\/\//.test(href) && !getUrlMimeType(href); + return /^https?:\/\//.test(href) && !getUrlMediaType(href); } /** Ensures the Link is a URL so it can be parsed. */ @@ -87,17 +85,6 @@ function isLinkURL(link: Link): boolean { return link.type === 'url'; } -/** Get the extension from the URL, then get its type. */ -function getUrlMimeType(url: string): string | undefined { - try { - const { pathname } = new URL(url); - const ext = pathname.split('.').pop() ?? ''; - return typeByExtension(ext); - } catch { - return undefined; - } -} - /** Get pubkey from decoded bech32 entity, or undefined if not applicable. */ function getDecodedPubkey(decoded: nip19.DecodeResult): string | undefined { switch (decoded.type) { diff --git a/src/utils/media.test.ts b/src/utils/media.test.ts new file mode 100644 index 00000000..e88e97da --- /dev/null +++ b/src/utils/media.test.ts @@ -0,0 +1,17 @@ +import { assertEquals } from '@std/assert'; + +import { getUrlMediaType, isPermittedMediaType } from '@/utils/media.ts'; + +Deno.test('getUrlMediaType', () => { + assertEquals(getUrlMediaType('https://example.com/image.png'), 'image/png'); + assertEquals(getUrlMediaType('https://example.com/index.html'), 'text/html'); + assertEquals(getUrlMediaType('https://example.com/yolo'), undefined); + assertEquals(getUrlMediaType('https://example.com/'), undefined); +}); + +Deno.test('isPermittedMediaType', () => { + assertEquals(isPermittedMediaType('image/png', ['image', 'video']), true); + assertEquals(isPermittedMediaType('video/webm', ['image', 'video']), true); + assertEquals(isPermittedMediaType('audio/ogg', ['image', 'video']), false); + assertEquals(isPermittedMediaType('application/json', ['image', 'video']), false); +}); diff --git a/src/utils/media.ts b/src/utils/media.ts new file mode 100644 index 00000000..9c0ea9e3 --- /dev/null +++ b/src/utils/media.ts @@ -0,0 +1,24 @@ +import { typeByExtension } from '@std/media-types'; + +/** Get media type of the filename in the URL by its extension, if any. */ +export function getUrlMediaType(url: string): string | undefined { + try { + const { pathname } = new URL(url); + const ext = pathname.split('.').pop() ?? ''; + return typeByExtension(ext); + } catch { + return undefined; + } +} + +/** + * Check if the base type matches any of the permitted types. + * + * ```ts + * isPermittedMediaType('image/png', ['image', 'video']); // true + * ``` + */ +export function isPermittedMediaType(mediaType: string, permitted: string[]): boolean { + const [baseType, _subType] = mediaType.split('/'); + return permitted.includes(baseType); +} From 942260aa54f61a96a43492f886328df4cfa29dc4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 11:53:33 -0500 Subject: [PATCH 224/252] note.ts -> utils/note.ts --- src/{ => utils}/note.ts | 0 src/views/mastodon/statuses.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{ => utils}/note.ts (100%) diff --git a/src/note.ts b/src/utils/note.ts similarity index 100% rename from src/note.ts rename to src/utils/note.ts diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index aefe258b..1b0ebe85 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -4,10 +4,10 @@ import { nip19 } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; import { nostrDate } from '@/utils.ts'; +import { getMediaLinks, parseNoteContent } from '@/utils/note.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; From c8f9483795f12d8e79470c6142be88f8879f398d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 11:56:22 -0500 Subject: [PATCH 225/252] Add note.test.ts --- src/utils/note.test.ts | 28 ++++++++++++++++++++++++++++ src/utils/note.ts | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/utils/note.test.ts diff --git a/src/utils/note.test.ts b/src/utils/note.test.ts new file mode 100644 index 00000000..d123050f --- /dev/null +++ b/src/utils/note.test.ts @@ -0,0 +1,28 @@ +import { assertEquals } from '@std/assert'; + +import { getMediaLinks, parseNoteContent } from '@/utils/note.ts'; + +Deno.test('parseNoteContent', () => { + const { html, links, firstUrl } = parseNoteContent('Hello, world!'); + assertEquals(html, 'Hello, world!'); + assertEquals(links, []); + assertEquals(firstUrl, undefined); +}); + +Deno.test('getMediaLinks', () => { + const links = [ + { href: 'https://example.com/image.png' }, + { href: 'https://example.com/index.html' }, + { href: 'https://example.com/yolo' }, + { href: 'https://example.com/' }, + ]; + const mediaLinks = getMediaLinks(links); + assertEquals(mediaLinks, [ + { + url: 'https://example.com/image.png', + data: { + mime: 'image/png', + }, + }, + ]); +}); diff --git a/src/utils/note.ts b/src/utils/note.ts index 71dc0174..580c2072 100644 --- a/src/utils/note.ts +++ b/src/utils/note.ts @@ -58,7 +58,7 @@ function parseNoteContent(content: string): ParsedNoteContent { }; } -function getMediaLinks(links: Link[]): DittoAttachment[] { +function getMediaLinks(links: Pick[]): DittoAttachment[] { return links.reduce((acc, link) => { const mediaType = getUrlMediaType(link.href); if (!mediaType) return acc; From 7d34b9401e8b191e91801d205c85c3685a876769 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 13:22:20 -0500 Subject: [PATCH 226/252] Support imeta tags --- src/controllers/api/statuses.ts | 2 +- src/db/unattached-media.ts | 3 +-- src/schemas/nostr.ts | 25 +-------------------- src/upload.ts | 37 ++++++++++++++++++++----------- src/views/mastodon/attachments.ts | 16 ++++++++----- src/views/mastodon/statuses.ts | 12 ++++++---- 6 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 00f9a98e..8904b2b8 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -96,7 +96,7 @@ const createStatusController: AppController = async (c) => { if (data.media_ids?.length) { const media = await getUnattachedMediaByIds(kysely, data.media_ids) .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) - .then((media) => media.map(({ url, data }) => ['media', url, data])); + .then((media) => media.map(({ data }) => ['imeta', ...data])); tags.push(...media); } diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index cee1e3a3..0628278d 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -3,13 +3,12 @@ import uuid62 from 'uuid62'; import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { type MediaData } from '@/schemas/nostr.ts'; interface UnattachedMedia { id: string; pubkey: string; url: string; - data: MediaData; + data: string[][]; // NIP-94 tags uploaded_at: number; } diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index a42b9f07..d8aa29a4 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -9,27 +9,12 @@ const signedEventSchema = n.event() .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine(verifyEvent, 'Event signature is invalid'); -/** Media data schema from `"media"` tags. */ -const mediaDataSchema = z.object({ - blurhash: z.string().optional().catch(undefined), - cid: z.string().optional().catch(undefined), - description: z.string().max(200).optional().catch(undefined), - height: z.number().int().positive().optional().catch(undefined), - mime: z.string().optional().catch(undefined), - name: z.string().optional().catch(undefined), - size: z.number().int().positive().optional().catch(undefined), - width: z.number().int().positive().optional().catch(undefined), -}); - /** Kind 0 content schema for the Ditto server admin user. */ const serverMetaSchema = n.metadata().and(z.object({ tagline: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined), })); -/** Media data from `"media"` tags. */ -type MediaData = z.infer; - /** NIP-11 Relay Information Document. */ const relayInfoDocSchema = z.object({ name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined), @@ -47,12 +32,4 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url() /** NIP-30 custom emoji tag. */ type EmojiTag = z.infer; -export { - type EmojiTag, - emojiTagSchema, - type MediaData, - mediaDataSchema, - relayInfoDocSchema, - serverMetaSchema, - signedEventSchema, -}; +export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, serverMetaSchema, signedEventSchema }; diff --git a/src/upload.ts b/src/upload.ts index 632dbabf..4f5fd14f 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -8,26 +8,37 @@ interface FileMeta { } /** Upload a file, track it in the database, and return the resulting media object. */ -async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal) { - const { name, type, size } = file; +async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise { + const { type, size } = file; const { pubkey, description } = meta; if (file.size > Conf.maxUploadSize) { throw new Error('File size is too large.'); } - const { url } = await uploader.upload(file, { signal }); + const { url, sha256, cid } = await uploader.upload(file, { signal }); - return insertUnattachedMedia({ - pubkey, - url, - data: { - name, - size, - description, - mime: type, - }, - }); + const data: string[][] = [ + ['url', url], + ['m', type], + ['size', size.toString()], + ]; + + if (sha256) { + data.push(['x', sha256]); + } + + if (cid) { + data.push(['cid', cid]); + } + + if (description) { + data.push(['alt', description]); + } + + await insertUnattachedMedia({ pubkey, url, data }); + + return data; } export { uploadFile }; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 3ea989e6..18fe0319 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -6,15 +6,21 @@ type DittoAttachment = TypeFest.SetOptional name === 'm')?.[1]; + const alt = data.find(([name]) => name === 'alt')?.[1]; + const cid = data.find(([name]) => name === 'cid')?.[1]; + const blurhash = data.find(([name]) => name === 'blurhash')?.[1]; + return { - id: id ?? url ?? data.cid, - type: getAttachmentType(data.mime ?? ''), + id: id ?? url, + type: getAttachmentType(m ?? ''), url, preview_url: url, remote_url: null, - description: data.description ?? '', - blurhash: data.blurhash || null, - cid: data.cid, + description: alt ?? '', + blurhash: blurhash || null, + cid: cid, }; } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 1b0ebe85..8428e9a1 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent } from '@nostrify/nostrify'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; import { nip19 } from 'nostr-tools'; @@ -12,7 +12,6 @@ import { unfurlCardCached } from '@/utils/unfurl.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; -import { mediaDataSchema } from '@/schemas/nostr.ts'; interface RenderStatusOpts { viewerPubkey?: string; @@ -80,8 +79,13 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< const mediaLinks = getMediaLinks(links); const mediaTags: DittoAttachment[] = event.tags - .filter((tag) => tag[0] === 'media') - .map(([_, url, json]) => ({ url, data: n.json().pipe(mediaDataSchema).parse(json) })); + .filter(([name]) => name === 'imeta') + .map(([_, ...entries]) => { + const data = entries.map((entry) => entry.split(' ')); + const url = data.find(([name]) => name === 'url')?.[1]; + return { url, data }; + }) + .filter((media): media is DittoAttachment => !!media.url); const media = [...mediaLinks, ...mediaTags]; From 611a94bdcf640efcd22615ff12d1f911fbf61cc4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 14:32:50 -0500 Subject: [PATCH 227/252] Fix uploading (almost) --- src/controllers/api/statuses.ts | 12 +++++++++--- src/db/unattached-media.ts | 9 +++++++++ src/utils/api.ts | 2 ++ src/utils/note.ts | 16 +++++++--------- src/views/mastodon/attachments.ts | 14 +++++++------- src/views/mastodon/statuses.ts | 13 ++++--------- 6 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 8904b2b8..d8186f59 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; -import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; +import { getUnattachedMediaByUrls } from '@/db/unattached-media.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { addTag, deleteTag } from '@/tags.ts'; import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; @@ -94,9 +94,15 @@ const createStatusController: AppController = async (c) => { const viewerPubkey = await c.get('signer')?.getPublicKey(); if (data.media_ids?.length) { - const media = await getUnattachedMediaByIds(kysely, data.media_ids) + const media = await getUnattachedMediaByUrls(kysely, data.media_ids) .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) - .then((media) => media.map(({ data }) => ['imeta', ...data])); + .then((media) => + media.map(({ data }) => { + const tags: string[][] = JSON.parse(data); + const values: string[] = tags.map((tag) => tag.join(' ')); + return ['imeta', ...values]; + }) + ); tags.push(...media); } diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 0628278d..397a1f77 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -64,6 +64,14 @@ async function getUnattachedMediaByIds(kysely: Kysely, ids: string[ .execute(); } +/** Get unattached media by URLs. */ +async function getUnattachedMediaByUrls(kysely: Kysely, urls: string[]) { + if (!urls.length) return []; + return await selectUnattachedMediaQuery(kysely) + .where('url', 'in', urls) + .execute(); +} + /** Delete rows as an event with media is being created. */ async function deleteAttachedMedia(pubkey: string, urls: string[]): Promise { if (!urls.length) return; @@ -79,6 +87,7 @@ export { deleteUnattachedMediaByUrl, getUnattachedMedia, getUnattachedMediaByIds, + getUnattachedMediaByUrls, insertUnattachedMedia, type UnattachedMedia, }; diff --git a/src/utils/api.ts b/src/utils/api.ts index dceede7a..c54f5aad 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -29,6 +29,8 @@ async function createEvent(t: EventStub, c: AppContext): Promise { }); } + console.log(t); + const event = await signer.signEvent({ content: '', created_at: nostrNow(), diff --git a/src/utils/note.ts b/src/utils/note.ts index 580c2072..20cb83aa 100644 --- a/src/utils/note.ts +++ b/src/utils/note.ts @@ -5,7 +5,6 @@ import { nip19, nip21 } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { getUrlMediaType, isPermittedMediaType } from '@/utils/media.ts'; -import { type DittoAttachment } from '@/views/mastodon/attachments.ts'; linkify.registerCustomProtocol('nostr', true); linkify.registerCustomProtocol('wss'); @@ -58,18 +57,17 @@ function parseNoteContent(content: string): ParsedNoteContent { }; } -function getMediaLinks(links: Pick[]): DittoAttachment[] { - return links.reduce((acc, link) => { +/** Returns a matrix of tags. Each item is a list of NIP-94 tags representing a file. */ +function getMediaLinks(links: Pick[]): string[][][] { + return links.reduce((acc, link) => { const mediaType = getUrlMediaType(link.href); if (!mediaType) return acc; if (isPermittedMediaType(mediaType, ['audio', 'image', 'video'])) { - acc.push({ - url: link.href, - data: { - mime: mediaType, - }, - }); + acc.push([ + ['url', link.href], + ['m', mediaType], + ]); } return acc; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 18fe0319..8922985f 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -4,16 +4,16 @@ import { UnattachedMedia } from '@/db/unattached-media.ts'; type DittoAttachment = TypeFest.SetOptional; -function renderAttachment(media: DittoAttachment) { - const { id, data, url } = media; +function renderAttachment(tags: string[][]) { + const url = tags.find(([name]) => name === 'url')?.[1]; - const m = data.find(([name]) => name === 'm')?.[1]; - const alt = data.find(([name]) => name === 'alt')?.[1]; - const cid = data.find(([name]) => name === 'cid')?.[1]; - const blurhash = data.find(([name]) => name === 'blurhash')?.[1]; + const m = tags.find(([name]) => name === 'm')?.[1]; + const alt = tags.find(([name]) => name === 'alt')?.[1]; + const cid = tags.find(([name]) => name === 'cid')?.[1]; + const blurhash = tags.find(([name]) => name === 'blurhash')?.[1]; return { - id: id ?? url, + id: url, type: getAttachmentType(m ?? ''), url, preview_url: url, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 8428e9a1..889c23dd 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -10,7 +10,7 @@ import { nostrDate } from '@/utils.ts'; import { getMediaLinks, parseNoteContent } from '@/utils/note.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; -import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; +import { renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; interface RenderStatusOpts { @@ -78,16 +78,11 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< const mediaLinks = getMediaLinks(links); - const mediaTags: DittoAttachment[] = event.tags + const imeta: string[][][] = event.tags .filter(([name]) => name === 'imeta') - .map(([_, ...entries]) => { - const data = entries.map((entry) => entry.split(' ')); - const url = data.find(([name]) => name === 'url')?.[1]; - return { url, data }; - }) - .filter((media): media is DittoAttachment => !!media.url); + .map(([_, ...entries]) => entries.map((entry) => entry.split(' '))); - const media = [...mediaLinks, ...mediaTags]; + const media = [...mediaLinks, ...imeta]; return { id: event.id, From e7d350a0e305a58191331b6664866419d215802f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 14:54:10 -0500 Subject: [PATCH 228/252] Fix uploading by URL --- deno.json | 1 - src/controllers/api/statuses.ts | 4 ++-- src/db/unattached-media.ts | 25 +++++-------------------- src/upload.ts | 11 ++++++++++- src/utils/api.ts | 2 -- src/views/mastodon/attachments.ts | 6 +++--- 6 files changed, 20 insertions(+), 29 deletions(-) diff --git a/deno.json b/deno.json index 54ba4680..946b4e02 100644 --- a/deno.json +++ b/deno.json @@ -54,7 +54,6 @@ "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", "unfurl.js": "npm:unfurl.js@^6.4.0", - "uuid62": "npm:uuid62@^1.0.2", "zod": "npm:zod@^3.23.5", "~/fixtures/": "./fixtures/" }, diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index d8186f59..f7a603fe 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; -import { getUnattachedMediaByUrls } from '@/db/unattached-media.ts'; +import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { addTag, deleteTag } from '@/tags.ts'; import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; @@ -94,7 +94,7 @@ const createStatusController: AppController = async (c) => { const viewerPubkey = await c.get('signer')?.getPublicKey(); if (data.media_ids?.length) { - const media = await getUnattachedMediaByUrls(kysely, data.media_ids) + const media = await getUnattachedMediaByIds(kysely, data.media_ids) .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) .then((media) => media.map(({ data }) => { diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 397a1f77..0ab46b61 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -1,5 +1,4 @@ import { Kysely } from 'kysely'; -import uuid62 from 'uuid62'; import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; @@ -8,24 +7,19 @@ interface UnattachedMedia { id: string; pubkey: string; url: string; - data: string[][]; // NIP-94 tags + /** NIP-94 tags. */ + data: string[][]; uploaded_at: number; } /** Add unattached media into the database. */ -async function insertUnattachedMedia(media: Omit) { - const result = { - id: uuid62.v4(), - uploaded_at: Date.now(), - ...media, - }; - +async function insertUnattachedMedia(media: UnattachedMedia) { const kysely = await DittoDB.getInstance(); await kysely.insertInto('unattached_media') - .values({ ...result, data: JSON.stringify(media.data) }) + .values({ ...media, data: JSON.stringify(media.data) }) .execute(); - return result; + return media; } /** Select query for unattached media. */ @@ -64,14 +58,6 @@ async function getUnattachedMediaByIds(kysely: Kysely, ids: string[ .execute(); } -/** Get unattached media by URLs. */ -async function getUnattachedMediaByUrls(kysely: Kysely, urls: string[]) { - if (!urls.length) return []; - return await selectUnattachedMediaQuery(kysely) - .where('url', 'in', urls) - .execute(); -} - /** Delete rows as an event with media is being created. */ async function deleteAttachedMedia(pubkey: string, urls: string[]): Promise { if (!urls.length) return; @@ -87,7 +73,6 @@ export { deleteUnattachedMediaByUrl, getUnattachedMedia, getUnattachedMediaByIds, - getUnattachedMediaByUrls, insertUnattachedMedia, type UnattachedMedia, }; diff --git a/src/upload.ts b/src/upload.ts index 4f5fd14f..d815bd83 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -36,7 +36,16 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro data.push(['alt', description]); } - await insertUnattachedMedia({ pubkey, url, data }); + const uuid = crypto.randomUUID(); + data.push(['uuid', uuid]); + + await insertUnattachedMedia({ + id: uuid, + pubkey, + url, + data, + uploaded_at: Date.now(), + }); return data; } diff --git a/src/utils/api.ts b/src/utils/api.ts index c54f5aad..dceede7a 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -29,8 +29,6 @@ async function createEvent(t: EventStub, c: AppContext): Promise { }); } - console.log(t); - const event = await signer.signEvent({ content: '', created_at: nostrNow(), diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 8922985f..1dbcda95 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -5,15 +5,15 @@ import { UnattachedMedia } from '@/db/unattached-media.ts'; type DittoAttachment = TypeFest.SetOptional; function renderAttachment(tags: string[][]) { - const url = tags.find(([name]) => name === 'url')?.[1]; - const m = tags.find(([name]) => name === 'm')?.[1]; + const url = tags.find(([name]) => name === 'url')?.[1]; const alt = tags.find(([name]) => name === 'alt')?.[1]; const cid = tags.find(([name]) => name === 'cid')?.[1]; + const uuid = tags.find(([name]) => name === 'uuid')?.[1]; const blurhash = tags.find(([name]) => name === 'blurhash')?.[1]; return { - id: url, + id: uuid, type: getAttachmentType(m ?? ''), url, preview_url: url, From 91ea4577f1549be74b8f036293ca3fa0a56d7bd5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 14:58:48 -0500 Subject: [PATCH 229/252] Filter out attachments with no url --- src/views/mastodon/attachments.ts | 4 +++- src/views/mastodon/statuses.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 1dbcda95..9f8e5c35 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -12,8 +12,10 @@ function renderAttachment(tags: string[][]) { const uuid = tags.find(([name]) => name === 'uuid')?.[1]; const blurhash = tags.find(([name]) => name === 'blurhash')?.[1]; + if (!url) return; + return { - id: uuid, + id: uuid ?? url, type: getAttachmentType(m ?? ''), url, preview_url: url, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 889c23dd..16ba4822 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -106,7 +106,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< pinned: Boolean(pinEvent), reblog: null, application: null, - media_attachments: media.map(renderAttachment), + media_attachments: media.map(renderAttachment).filter(Boolean), mentions, tags: [], emojis: renderEmojis(event), From b1b341d3b8a947b2a07325bfa07f2060c01cadcd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 15:29:12 -0500 Subject: [PATCH 230/252] Insert media URL into text --- src/controllers/api/statuses.ts | 27 +++++++++++++-------------- src/db/unattached-media.ts | 10 ++++++++-- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index f7a603fe..e620b930 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -91,21 +91,14 @@ const createStatusController: AppController = async (c) => { tags.push(['subject', data.spoiler_text]); } - const viewerPubkey = await c.get('signer')?.getPublicKey(); + const media = data.media_ids?.length ? await getUnattachedMediaByIds(kysely, data.media_ids) : []; - if (data.media_ids?.length) { - const media = await getUnattachedMediaByIds(kysely, data.media_ids) - .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) - .then((media) => - media.map(({ data }) => { - const tags: string[][] = JSON.parse(data); - const values: string[] = tags.map((tag) => tag.join(' ')); - return ['imeta', ...values]; - }) - ); + const imeta: string[][] = media.map(({ data }) => { + const values: string[] = data.map((tag) => tag.join(' ')); + return ['imeta', ...values]; + }); - tags.push(...media); - } + tags.push(...imeta); const pubkeys = new Set(); @@ -137,9 +130,15 @@ const createStatusController: AppController = async (c) => { tags.push(['t', match[1]]); } + const mediaUrls: string[] = media + .map(({ data }) => data.find(([name]) => name === 'url')?.[1]) + .filter((url): url is string => Boolean(url)); + + const mediaCompat: string = mediaUrls.length ? ['', '', ...mediaUrls].join('\n') : ''; + const event = await createEvent({ kind: 1, - content, + content: content + mediaCompat, tags, }, c); diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index 0ab46b61..0e0aeea6 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -51,11 +51,17 @@ async function deleteUnattachedMediaByUrl(url: string) { } /** Get unattached media by IDs. */ -async function getUnattachedMediaByIds(kysely: Kysely, ids: string[]) { +async function getUnattachedMediaByIds(kysely: Kysely, ids: string[]): Promise { if (!ids.length) return []; - return await selectUnattachedMediaQuery(kysely) + + const results = await selectUnattachedMediaQuery(kysely) .where('id', 'in', ids) .execute(); + + return results.map((row) => ({ + ...row, + data: JSON.parse(row.data), + })); } /** Delete rows as an event with media is being created. */ From c8b999a1f7a808e89aab968c24ae2bf467fb4a8f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 15:36:17 -0500 Subject: [PATCH 231/252] imeta: don't get attachment ID from a tag --- src/upload.ts | 13 ++++--------- src/views/mastodon/attachments.ts | 14 +++++--------- src/views/mastodon/statuses.ts | 2 +- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/upload.ts b/src/upload.ts index d815bd83..40184f09 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -1,5 +1,5 @@ import { Conf } from '@/config.ts'; -import { insertUnattachedMedia } from '@/db/unattached-media.ts'; +import { insertUnattachedMedia, UnattachedMedia } from '@/db/unattached-media.ts'; import { configUploader as uploader } from '@/uploaders/config.ts'; interface FileMeta { @@ -8,7 +8,7 @@ interface FileMeta { } /** Upload a file, track it in the database, and return the resulting media object. */ -async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise { +async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise { const { type, size } = file; const { pubkey, description } = meta; @@ -36,18 +36,13 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro data.push(['alt', description]); } - const uuid = crypto.randomUUID(); - data.push(['uuid', uuid]); - - await insertUnattachedMedia({ - id: uuid, + return insertUnattachedMedia({ + id: crypto.randomUUID(), pubkey, url, data, uploaded_at: Date.now(), }); - - return data; } export { uploadFile }; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 9f8e5c35..273b460d 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -1,21 +1,17 @@ -import * as TypeFest from 'type-fest'; +/** Render Mastodon media attachment. */ +function renderAttachment(media: { id?: string; data: string[][] }) { + const { id, data: tags } = media; -import { UnattachedMedia } from '@/db/unattached-media.ts'; - -type DittoAttachment = TypeFest.SetOptional; - -function renderAttachment(tags: string[][]) { const m = tags.find(([name]) => name === 'm')?.[1]; const url = tags.find(([name]) => name === 'url')?.[1]; const alt = tags.find(([name]) => name === 'alt')?.[1]; const cid = tags.find(([name]) => name === 'cid')?.[1]; - const uuid = tags.find(([name]) => name === 'uuid')?.[1]; const blurhash = tags.find(([name]) => name === 'blurhash')?.[1]; if (!url) return; return { - id: uuid ?? url, + id: id ?? url, type: getAttachmentType(m ?? ''), url, preview_url: url, @@ -40,4 +36,4 @@ function getAttachmentType(mime: string): string { } } -export { type DittoAttachment, renderAttachment }; +export { renderAttachment }; diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 16ba4822..c674e16d 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -106,7 +106,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< pinned: Boolean(pinEvent), reblog: null, application: null, - media_attachments: media.map(renderAttachment).filter(Boolean), + media_attachments: media.map((m) => renderAttachment({ data: m })).filter(Boolean), mentions, tags: [], emojis: renderEmojis(event), From cbf0bc35940b87c84d30feb1a91d0c900ac47cd5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 15:46:28 -0500 Subject: [PATCH 232/252] Fix note test --- src/utils/note.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/utils/note.test.ts b/src/utils/note.test.ts index d123050f..9c8fad7e 100644 --- a/src/utils/note.test.ts +++ b/src/utils/note.test.ts @@ -17,12 +17,8 @@ Deno.test('getMediaLinks', () => { { href: 'https://example.com/' }, ]; const mediaLinks = getMediaLinks(links); - assertEquals(mediaLinks, [ - { - url: 'https://example.com/image.png', - data: { - mime: 'image/png', - }, - }, - ]); + assertEquals(mediaLinks, [[ + ['url', 'https://example.com/image.png'], + ['m', 'image/png'], + ]]); }); From c89be75e5b9f240c8eba9fa180cad7ebc6301d76 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 16:22:24 -0500 Subject: [PATCH 233/252] Add a nostr.build uploader --- fixtures/nostrbuild-gif.json | 34 ++++++++++++++++++++++++++++++++++ fixtures/nostrbuild-mp3.json | 29 +++++++++++++++++++++++++++++ src/config.ts | 4 ++++ src/schemas/nostrbuild.ts | 18 ++++++++++++++++++ src/upload.ts | 6 +++++- src/uploaders/config.ts | 3 +++ src/uploaders/nostrbuild.ts | 33 +++++++++++++++++++++++++++++++++ src/uploaders/types.ts | 2 ++ 8 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 fixtures/nostrbuild-gif.json create mode 100644 fixtures/nostrbuild-mp3.json create mode 100644 src/schemas/nostrbuild.ts create mode 100644 src/uploaders/nostrbuild.ts diff --git a/fixtures/nostrbuild-gif.json b/fixtures/nostrbuild-gif.json new file mode 100644 index 00000000..49a969af --- /dev/null +++ b/fixtures/nostrbuild-gif.json @@ -0,0 +1,34 @@ +{ + "status": "success", + "message": "Upload successful.", + "data": [ + { + "input_name": "APIv2", + "name": "e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "sha256": "0a71f1c9dd982079bc52e96403368209cbf9507c5f6956134686f56e684b6377", + "original_sha256": "e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3", + "type": "picture", + "mime": "image/gif", + "size": 1796276, + "blurhash": "LGH-S^Vwm]x]04kX-qR-R]SL5FxZ", + "dimensions": { + "width": 360, + "height": 216 + }, + "dimensionsString": "360x216", + "url": "https://image.nostr.build/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "thumbnail": "https://image.nostr.build/thumb/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "responsive": { + "240p": "https://image.nostr.build/resp/240p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "360p": "https://image.nostr.build/resp/360p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "480p": "https://image.nostr.build/resp/480p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "720p": "https://image.nostr.build/resp/720p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "1080p": "https://image.nostr.build/resp/1080p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif" + }, + "metadata": { + "date:create": "2024-05-18T02:11:39+00:00", + "date:modify": "2024-05-18T02:11:39+00:00" + } + } + ] +} \ No newline at end of file diff --git a/fixtures/nostrbuild-mp3.json b/fixtures/nostrbuild-mp3.json new file mode 100644 index 00000000..42a60b44 --- /dev/null +++ b/fixtures/nostrbuild-mp3.json @@ -0,0 +1,29 @@ +{ + "status": "success", + "message": "Upload successful.", + "data": [ + { + "id": 0, + "input_name": "APIv2", + "name": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "url": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "thumbnail": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "responsive": { + "240p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "360p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "480p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "720p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "1080p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3" + }, + "blurhash": "", + "sha256": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725", + "original_sha256": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725", + "type": "video", + "mime": "audio/mpeg", + "size": 1519616, + "metadata": [], + "dimensions": [], + "dimensionsString": "0x0" + } + ] +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 6fe62b9e..f3b472ea 100644 --- a/src/config.ts +++ b/src/config.ts @@ -136,6 +136,10 @@ class Conf { return Deno.env.get('IPFS_API_URL') || 'http://localhost:5001'; }, }; + /** nostr.build API endpoint when the `nostrbuild` uploader is used. */ + static get nostrbuildEndpoint(): string { + return Deno.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; + } /** Module to upload files with. */ static get uploader() { return Deno.env.get('DITTO_UPLOADER'); diff --git a/src/schemas/nostrbuild.ts b/src/schemas/nostrbuild.ts new file mode 100644 index 00000000..c9fd6802 --- /dev/null +++ b/src/schemas/nostrbuild.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +export const nostrbuildFileSchema = z.object({ + name: z.string(), + url: z.string().url(), + thumbnail: z.string(), + blurhash: z.string(), + sha256: z.string(), + mime: z.string(), + dimensions: z.object({ + width: z.number(), + height: z.number(), + }), +}); + +export const nostrbuildSchema = z.object({ + data: nostrbuildFileSchema.array().min(1), +}); diff --git a/src/upload.ts b/src/upload.ts index 40184f09..5b43f397 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -16,7 +16,7 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro throw new Error('File size is too large.'); } - const { url, sha256, cid } = await uploader.upload(file, { signal }); + const { url, sha256, cid, blurhash } = await uploader.upload(file, { signal }); const data: string[][] = [ ['url', url], @@ -32,6 +32,10 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro data.push(['cid', cid]); } + if (blurhash) { + data.push(['blurhash', blurhash]); + } + if (description) { data.push(['alt', description]); } diff --git a/src/uploaders/config.ts b/src/uploaders/config.ts index 5b4c7aff..8ce22b6a 100644 --- a/src/uploaders/config.ts +++ b/src/uploaders/config.ts @@ -2,6 +2,7 @@ import { Conf } from '@/config.ts'; import { ipfsUploader } from '@/uploaders/ipfs.ts'; import { localUploader } from '@/uploaders/local.ts'; +import { nostrbuildUploader } from '@/uploaders/nostrbuild.ts'; import { s3Uploader } from '@/uploaders/s3.ts'; import type { Uploader } from './types.ts'; @@ -25,6 +26,8 @@ function uploader() { return ipfsUploader; case 'local': return localUploader; + case 'nostrbuild': + return nostrbuildUploader; default: throw new Error('No `DITTO_UPLOADER` configured. Uploads are disabled.'); } diff --git a/src/uploaders/nostrbuild.ts b/src/uploaders/nostrbuild.ts new file mode 100644 index 00000000..d9eed242 --- /dev/null +++ b/src/uploaders/nostrbuild.ts @@ -0,0 +1,33 @@ +import { Conf } from '@/config.ts'; +import { nostrbuildSchema } from '@/schemas/nostrbuild.ts'; + +import type { Uploader } from './types.ts'; + +/** nostr.build uploader. */ +export const nostrbuildUploader: Uploader = { + async upload(file) { + const formData = new FormData(); + formData.append('fileToUpload', file); + + const response = await fetch(Conf.nostrbuildEndpoint, { + method: 'POST', + body: formData, + }); + + const json = await response.json(); + console.log(JSON.stringify(json)); + + const [data] = nostrbuildSchema.parse(json).data; + + return { + id: data.url, + sha256: data.sha256, + url: data.url, + blurhash: data.blurhash, + }; + }, + // deno-lint-ignore require-await + async delete(): Promise { + return; + }, +}; diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index c514ad1b..ac5bf05b 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -14,6 +14,8 @@ interface UploadResult { url: string; /** SHA-256 hash of the file. */ sha256?: string; + /** Blurhash of the file. */ + blurhash?: string; /** IPFS CID of the file. */ cid?: string; } From ce49c500ae2fe62f6a83607055803aa18034c714 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 16:47:47 -0500 Subject: [PATCH 234/252] renderStatus: fix duplicated attachments --- src/views/mastodon/statuses.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index c674e16d..c707ebba 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -76,13 +76,11 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< const cw = event.tags.find(isCWTag); const subject = event.tags.find((tag) => tag[0] === 'subject'); - const mediaLinks = getMediaLinks(links); - const imeta: string[][][] = event.tags .filter(([name]) => name === 'imeta') .map(([_, ...entries]) => entries.map((entry) => entry.split(' '))); - const media = [...mediaLinks, ...imeta]; + const media = imeta.length ? imeta : getMediaLinks(links); return { id: event.id, From 353111051a79a37aa3b7be1c44b4c54ffbeb9904 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 16:53:17 -0500 Subject: [PATCH 235/252] Use dimensions from nostr.build --- src/schemas/nostrbuild.ts | 3 ++- src/upload.ts | 6 +++++- src/uploaders/nostrbuild.ts | 4 ++-- src/uploaders/types.ts | 4 ++++ src/views/mastodon/attachments.ts | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/schemas/nostrbuild.ts b/src/schemas/nostrbuild.ts index c9fd6802..db9f6074 100644 --- a/src/schemas/nostrbuild.ts +++ b/src/schemas/nostrbuild.ts @@ -6,11 +6,12 @@ export const nostrbuildFileSchema = z.object({ thumbnail: z.string(), blurhash: z.string(), sha256: z.string(), + original_sha256: z.string(), mime: z.string(), dimensions: z.object({ width: z.number(), height: z.number(), - }), + }).optional().catch(undefined), }); export const nostrbuildSchema = z.object({ diff --git a/src/upload.ts b/src/upload.ts index 5b43f397..1da5a7de 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -16,7 +16,7 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro throw new Error('File size is too large.'); } - const { url, sha256, cid, blurhash } = await uploader.upload(file, { signal }); + const { url, sha256, cid, blurhash, width, height } = await uploader.upload(file, { signal }); const data: string[][] = [ ['url', url], @@ -24,6 +24,10 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro ['size', size.toString()], ]; + if (typeof width === 'number' && typeof height === 'number') { + data.push(['dim', `${width}x${height}`]); + } + if (sha256) { data.push(['x', sha256]); } diff --git a/src/uploaders/nostrbuild.ts b/src/uploaders/nostrbuild.ts index d9eed242..d9d08650 100644 --- a/src/uploaders/nostrbuild.ts +++ b/src/uploaders/nostrbuild.ts @@ -15,8 +15,6 @@ export const nostrbuildUploader: Uploader = { }); const json = await response.json(); - console.log(JSON.stringify(json)); - const [data] = nostrbuildSchema.parse(json).data; return { @@ -24,6 +22,8 @@ export const nostrbuildUploader: Uploader = { sha256: data.sha256, url: data.url, blurhash: data.blurhash, + width: data.dimensions?.width, + height: data.dimensions?.height, }; }, // deno-lint-ignore require-await diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index ac5bf05b..e423028c 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -18,6 +18,10 @@ interface UploadResult { blurhash?: string; /** IPFS CID of the file. */ cid?: string; + /** Width of the file, if applicable. */ + width?: number; + /** Height of the file, if applicable. */ + height?: number; } export type { Uploader }; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 273b460d..0b1b8eb1 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -6,10 +6,23 @@ function renderAttachment(media: { id?: string; data: string[][] }) { const url = tags.find(([name]) => name === 'url')?.[1]; const alt = tags.find(([name]) => name === 'alt')?.[1]; const cid = tags.find(([name]) => name === 'cid')?.[1]; + const dim = tags.find(([name]) => name === 'dim')?.[1]; const blurhash = tags.find(([name]) => name === 'blurhash')?.[1]; if (!url) return; + const [width, height] = dim?.split('x').map(Number) ?? [null, null]; + + const meta = (typeof width === 'number' && typeof height === 'number') + ? { + original: { + width, + height, + aspect: width / height, + }, + } + : undefined; + return { id: id ?? url, type: getAttachmentType(m ?? ''), @@ -18,6 +31,7 @@ function renderAttachment(media: { id?: string; data: string[][] }) { remote_url: null, description: alt ?? '', blurhash: blurhash || null, + meta, cid: cid, }; } From e5595d34be546a99d5594eb0af185cd4119f4c88 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 17:08:30 -0500 Subject: [PATCH 236/252] Strip imeta links from the end of the content --- src/utils/note.ts | 28 +++++++++++++++++++++++++++- src/views/mastodon/statuses.ts | 4 ++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/utils/note.ts b/src/utils/note.ts index 20cb83aa..03da2def 100644 --- a/src/utils/note.ts +++ b/src/utils/note.ts @@ -57,6 +57,32 @@ function parseNoteContent(content: string): ParsedNoteContent { }; } +/** Remove imeta links. */ +function stripimeta(content: string, tags: string[][]): string { + const imeta = tags.filter(([name]) => name === 'imeta'); + + if (!imeta.length) { + return content; + } + + const urls = new Set( + imeta.map(([, ...values]) => values.map((v) => v.split(' ')).find(([name]) => name === 'url')?.[1]), + ); + + const lines = content.split('\n').reverse(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line === '' || urls.has(line)) { + lines.splice(i, 1); + } else { + break; + } + } + + return lines.reverse().join('\n'); +} + /** Returns a matrix of tags. Each item is a list of NIP-94 tags representing a file. */ function getMediaLinks(links: Pick[]): string[][][] { return links.reduce((acc, link) => { @@ -93,4 +119,4 @@ function getDecodedPubkey(decoded: nip19.DecodeResult): string | undefined { } } -export { getMediaLinks, parseNoteContent }; +export { getMediaLinks, parseNoteContent, stripimeta }; diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index c707ebba..a06aac21 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -7,7 +7,7 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; import { nostrDate } from '@/utils.ts'; -import { getMediaLinks, parseNoteContent } from '@/utils/note.ts'; +import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderAttachment } from '@/views/mastodon/attachments.ts'; @@ -46,7 +46,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< [{ kinds: [0], authors: mentionedPubkeys, limit: mentionedPubkeys.length }], ); - const { html, links, firstUrl } = parseNoteContent(event.content); + const { html, links, firstUrl } = parseNoteContent(stripimeta(event.content, event.tags)); const [mentions, card, relatedEvents] = await Promise .all([ From 6090c4a6d9a0de134015ab8a02aaf8884631dc7f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 18:23:04 -0500 Subject: [PATCH 237/252] Make Uploaders return NIP-94 tags --- src/upload.ts | 30 ++++-------------------------- src/uploaders/config.ts | 4 ++-- src/uploaders/ipfs.ts | 11 ++++++----- src/uploaders/local.ts | 11 ++++++----- src/uploaders/nostrbuild.ts | 26 ++++++++++++++------------ src/uploaders/s3.ts | 12 +++++++----- src/uploaders/types.ts | 22 ++-------------------- 7 files changed, 41 insertions(+), 75 deletions(-) diff --git a/src/upload.ts b/src/upload.ts index 1da5a7de..cd9d2bb6 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -9,46 +9,24 @@ interface FileMeta { /** Upload a file, track it in the database, and return the resulting media object. */ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise { - const { type, size } = file; const { pubkey, description } = meta; if (file.size > Conf.maxUploadSize) { throw new Error('File size is too large.'); } - const { url, sha256, cid, blurhash, width, height } = await uploader.upload(file, { signal }); - - const data: string[][] = [ - ['url', url], - ['m', type], - ['size', size.toString()], - ]; - - if (typeof width === 'number' && typeof height === 'number') { - data.push(['dim', `${width}x${height}`]); - } - - if (sha256) { - data.push(['x', sha256]); - } - - if (cid) { - data.push(['cid', cid]); - } - - if (blurhash) { - data.push(['blurhash', blurhash]); - } + const tags = await uploader.upload(file, { signal }); + const url = tags[0][1]; if (description) { - data.push(['alt', description]); + tags.push(['alt', description]); } return insertUnattachedMedia({ id: crypto.randomUUID(), pubkey, url, - data, + data: tags, uploaded_at: Date.now(), }); } diff --git a/src/uploaders/config.ts b/src/uploaders/config.ts index 8ce22b6a..3f3aac74 100644 --- a/src/uploaders/config.ts +++ b/src/uploaders/config.ts @@ -12,8 +12,8 @@ const configUploader: Uploader = { upload(file, opts) { return uploader().upload(file, opts); }, - delete(id, opts) { - return uploader().delete(id, opts); + async delete(id, opts) { + return await uploader().delete?.(id, opts); }, }; diff --git a/src/uploaders/ipfs.ts b/src/uploaders/ipfs.ts index 21619b5b..b83dc2e3 100644 --- a/src/uploaders/ipfs.ts +++ b/src/uploaders/ipfs.ts @@ -32,11 +32,12 @@ const ipfsUploader: Uploader = { const { Hash: cid } = ipfsAddResponseSchema.parse(await response.json()); - return { - id: cid, - cid, - url: new URL(`/ipfs/${cid}`, Conf.mediaDomain).toString(), - }; + return [ + ['url', new URL(`/ipfs/${cid}`, Conf.mediaDomain).toString()], + ['m', file.type], + ['cid', cid], + ['size', file.size.toString()], + ]; }, async delete(cid, opts) { const url = new URL('/api/v0/pin/rm', Conf.ipfs.apiUrl); diff --git a/src/uploaders/local.ts b/src/uploaders/local.ts index a2381a3b..d5cd46ad 100644 --- a/src/uploaders/local.ts +++ b/src/uploaders/local.ts @@ -22,11 +22,12 @@ const localUploader: Uploader = { const url = new URL(mediaDomain); const path = url.pathname === '/' ? filename : join(url.pathname, filename); - return { - id: filename, - sha256, - url: new URL(path, url).toString(), - }; + return [ + ['url', new URL(path, url).toString()], + ['m', file.type], + ['x', sha256], + ['size', file.size.toString()], + ]; }, async delete(id) { await Deno.remove(join(Conf.uploadsDir, id)); diff --git a/src/uploaders/nostrbuild.ts b/src/uploaders/nostrbuild.ts index d9d08650..8bca331b 100644 --- a/src/uploaders/nostrbuild.ts +++ b/src/uploaders/nostrbuild.ts @@ -17,17 +17,19 @@ export const nostrbuildUploader: Uploader = { const json = await response.json(); const [data] = nostrbuildSchema.parse(json).data; - return { - id: data.url, - sha256: data.sha256, - url: data.url, - blurhash: data.blurhash, - width: data.dimensions?.width, - height: data.dimensions?.height, - }; - }, - // deno-lint-ignore require-await - async delete(): Promise { - return; + const tags: [['url', string], ...string[][]] = [ + ['url', data.url], + ['m', data.mime], + ['x', data.sha256], + ['ox', data.original_sha256], + ['size', file.size.toString()], + ['blurhash', data.blurhash], + ]; + + if (data.dimensions) { + tags.push(['dim', `${data.dimensions.width}x${data.dimensions.height}`]); + } + + return tags; }, }; diff --git a/src/uploaders/s3.ts b/src/uploaders/s3.ts index 267d8172..aaff8c82 100644 --- a/src/uploaders/s3.ts +++ b/src/uploaders/s3.ts @@ -25,12 +25,14 @@ const s3Uploader: Uploader = { const { pathStyle, bucket } = Conf.s3; const path = (pathStyle && bucket) ? join(bucket, filename) : filename; + const url = new URL(path, Conf.mediaDomain).toString(); - return { - id: filename, - sha256, - url: new URL(path, Conf.mediaDomain).toString(), - }; + return [ + ['url', url], + ['m', file.type], + ['x', sha256], + ['size', file.size.toString()], + ]; }, async delete(id) { await client().deleteObject(id); diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index e423028c..81b8a0a7 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -1,27 +1,9 @@ /** Modular uploader interface, to support uploading to different backends. */ interface Uploader { /** Upload the file to the backend. */ - upload(file: File, opts?: { signal?: AbortSignal }): Promise; + upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]>; /** Delete the file from the backend. */ - delete(cid: string, opts?: { signal?: AbortSignal }): Promise; -} - -/** Return value from the uploader after uploading a file. */ -interface UploadResult { - /** File ID specific to the uploader, so it can later be referenced or deleted. */ - id: string; - /** URL where the file can be accessed. */ - url: string; - /** SHA-256 hash of the file. */ - sha256?: string; - /** Blurhash of the file. */ - blurhash?: string; - /** IPFS CID of the file. */ - cid?: string; - /** Width of the file, if applicable. */ - width?: number; - /** Height of the file, if applicable. */ - height?: number; + delete?(cid: string, opts?: { signal?: AbortSignal }): Promise; } export type { Uploader }; From 82c03dcb56f9d9235a4d15d1ebe41a696bd92546 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 22:00:24 -0500 Subject: [PATCH 238/252] Rewrite all the uploaders --- src/app.ts | 5 ++ src/controllers/api/accounts.ts | 9 +++- src/controllers/api/media.ts | 7 ++- src/interfaces/DittoUploader.ts | 3 ++ src/middleware/uploaderMiddleware.ts | 27 ++++++++++ src/schemas/nostrbuild.ts | 19 ------- src/upload.ts | 12 +++-- src/uploaders/DenoUploader.ts | 44 ++++++++++++++++ src/uploaders/IPFSUploader.ts | 70 ++++++++++++++++++++++++++ src/uploaders/NostrBuildUploader.ts | 65 ++++++++++++++++++++++++ src/uploaders/{s3.ts => S3Uploader.ts} | 40 +++++++++------ src/uploaders/config.ts | 36 ------------- src/uploaders/ipfs.ts | 57 --------------------- src/uploaders/local.ts | 37 -------------- src/uploaders/nostrbuild.ts | 35 ------------- src/uploaders/types.ts | 9 ---- 16 files changed, 260 insertions(+), 215 deletions(-) create mode 100644 src/interfaces/DittoUploader.ts create mode 100644 src/middleware/uploaderMiddleware.ts delete mode 100644 src/schemas/nostrbuild.ts create mode 100644 src/uploaders/DenoUploader.ts create mode 100644 src/uploaders/IPFSUploader.ts create mode 100644 src/uploaders/NostrBuildUploader.ts rename src/uploaders/{s3.ts => S3Uploader.ts} (58%) delete mode 100644 src/uploaders/config.ts delete mode 100644 src/uploaders/ipfs.ts delete mode 100644 src/uploaders/local.ts delete mode 100644 src/uploaders/nostrbuild.ts delete mode 100644 src/uploaders/types.ts diff --git a/src/app.ts b/src/app.ts index 059e883c..ddc99904 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,6 +81,7 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; +import { DittoUploader } from '@/interfaces/DittoUploader.ts'; import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; @@ -89,11 +90,14 @@ import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { blockController } from '@/controllers/api/accounts.ts'; import { unblockController } from '@/controllers/api/accounts.ts'; +import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts'; interface AppEnv extends HonoEnv { Variables: { /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ signer?: NostrSigner; + /** Uploader for the user to upload files. */ + uploader?: DittoUploader; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; /** Store */ @@ -129,6 +133,7 @@ app.use( cspMiddleware(), cors({ origin: '*', exposeHeaders: ['link'] }), signerMiddleware, + uploaderMiddleware, auth98Middleware(), storeMiddleware, ); diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 5c26ba51..d67fd888 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -202,6 +202,7 @@ const updateCredentialsSchema = z.object({ const updateCredentialsController: AppController = async (c) => { const pubkey = await c.get('signer')?.getPublicKey()!; + const uploader = c.get('uploader'); const body = await parseBody(c.req.raw); const result = updateCredentialsSchema.safeParse(body); @@ -220,9 +221,13 @@ const updateCredentialsController: AppController = async (c) => { nip05, } = result.data; + if ((avatarFile || headerFile) && !uploader) { + return c.json({ error: 'No uploader configured.' }, 500); + } + const [avatar, header] = await Promise.all([ - avatarFile ? uploadFile(avatarFile, { pubkey }) : undefined, - headerFile ? uploadFile(headerFile, { pubkey }) : undefined, + (avatarFile && uploader) ? uploadFile(uploader, avatarFile, { pubkey }) : undefined, + (headerFile && uploader) ? uploadFile(uploader, headerFile, { pubkey }) : undefined, ]); meta.name = display_name ?? meta.name; diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index 33b79810..101b7767 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -14,6 +14,11 @@ const mediaBodySchema = z.object({ }); const mediaController: AppController = async (c) => { + const uploader = c.get('uploader'); + if (!uploader) { + return c.json({ error: 'No uploader configured.' }, 500); + } + const pubkey = await c.get('signer')?.getPublicKey()!; const result = mediaBodySchema.safeParse(await parseBody(c.req.raw)); const { signal } = c.req.raw; @@ -24,7 +29,7 @@ const mediaController: AppController = async (c) => { try { const { file, description } = result.data; - const media = await uploadFile(file, { pubkey, description }, signal); + const media = await uploadFile(uploader, file, { pubkey, description }, signal); return c.json(renderAttachment(media)); } catch (e) { console.error(e); diff --git a/src/interfaces/DittoUploader.ts b/src/interfaces/DittoUploader.ts new file mode 100644 index 00000000..08cbf504 --- /dev/null +++ b/src/interfaces/DittoUploader.ts @@ -0,0 +1,3 @@ +export interface DittoUploader { + upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]>; +} diff --git a/src/middleware/uploaderMiddleware.ts b/src/middleware/uploaderMiddleware.ts new file mode 100644 index 00000000..8279a122 --- /dev/null +++ b/src/middleware/uploaderMiddleware.ts @@ -0,0 +1,27 @@ +import { AppMiddleware } from '@/app.ts'; +import { Conf } from '@/config.ts'; +import { DenoUploader } from '@/uploaders/DenoUploader.ts'; +import { IPFSUploader } from '@/uploaders/IPFSUploader.ts'; +import { NostrBuildUploader } from '@/uploaders/NostrBuildUploader.ts'; +import { S3Uploader } from '@/uploaders/S3Uploader.ts'; +import { fetchWorker } from '@/workers/fetch.ts'; + +/** Set an uploader for the user. */ +export const uploaderMiddleware: AppMiddleware = async (c, next) => { + switch (Conf.uploader) { + case 's3': + c.set('uploader', new S3Uploader(Conf.s3)); + break; + case 'ipfs': + c.set('uploader', new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker })); + break; + case 'local': + c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir })); + break; + case 'nostrbuild': + c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, fetch: fetchWorker })); + break; + } + + await next(); +}; diff --git a/src/schemas/nostrbuild.ts b/src/schemas/nostrbuild.ts deleted file mode 100644 index db9f6074..00000000 --- a/src/schemas/nostrbuild.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from 'zod'; - -export const nostrbuildFileSchema = z.object({ - name: z.string(), - url: z.string().url(), - thumbnail: z.string(), - blurhash: z.string(), - sha256: z.string(), - original_sha256: z.string(), - mime: z.string(), - dimensions: z.object({ - width: z.number(), - height: z.number(), - }).optional().catch(undefined), -}); - -export const nostrbuildSchema = z.object({ - data: nostrbuildFileSchema.array().min(1), -}); diff --git a/src/upload.ts b/src/upload.ts index cd9d2bb6..0d1a085f 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -1,14 +1,18 @@ import { Conf } from '@/config.ts'; import { insertUnattachedMedia, UnattachedMedia } from '@/db/unattached-media.ts'; -import { configUploader as uploader } from '@/uploaders/config.ts'; - +import { DittoUploader } from '@/interfaces/DittoUploader.ts'; interface FileMeta { pubkey: string; description?: string; } /** Upload a file, track it in the database, and return the resulting media object. */ -async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise { +export async function uploadFile( + uploader: DittoUploader, + file: File, + meta: FileMeta, + signal?: AbortSignal, +): Promise { const { pubkey, description } = meta; if (file.size > Conf.maxUploadSize) { @@ -30,5 +34,3 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro uploaded_at: Date.now(), }); } - -export { uploadFile }; diff --git a/src/uploaders/DenoUploader.ts b/src/uploaders/DenoUploader.ts new file mode 100644 index 00000000..6c2e6d48 --- /dev/null +++ b/src/uploaders/DenoUploader.ts @@ -0,0 +1,44 @@ +import { join } from 'node:path'; + +import { crypto } from '@std/crypto'; +import { encodeHex } from '@std/encoding/hex'; +import { extensionsByType } from '@std/media-types'; + +import { DittoUploader } from '@/interfaces/DittoUploader.ts'; + +export interface DenoUploaderOpts { + baseUrl: string; + dir: string; +} + +/** Local Deno filesystem uploader. */ +export class DenoUploader implements DittoUploader { + constructor(private opts: DenoUploaderOpts) {} + + async upload(file: File): Promise<[['url', string], ...string[][]]> { + const { dir, baseUrl } = this.opts; + + const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); + const ext = extensionsByType(file.type)?.[0] ?? 'bin'; + const filename = `${sha256}.${ext}`; + + await Deno.mkdir(dir, { recursive: true }); + await Deno.writeFile(join(dir, filename), file.stream()); + + const url = new URL(baseUrl); + const path = url.pathname === '/' ? filename : join(url.pathname, filename); + + return [ + ['url', new URL(path, url).toString()], + ['m', file.type], + ['x', sha256], + ['size', file.size.toString()], + ]; + } + + async delete(filename: string) { + const { dir } = this.opts; + const path = join(dir, filename); + await Deno.remove(path); + } +} diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts new file mode 100644 index 00000000..ceb4e82e --- /dev/null +++ b/src/uploaders/IPFSUploader.ts @@ -0,0 +1,70 @@ +import { z } from 'zod'; + +import { DittoUploader } from '@/interfaces/DittoUploader.ts'; + +export interface IPFSUploaderOpts { + baseUrl: string; + apiUrl?: string; + fetch?: typeof fetch; +} + +/** + * IPFS uploader. It expects an IPFS node up and running. + * It will try to connect to `http://localhost:5001` by default, + * and upload the file using the REST API. + */ +export class IPFSUploader implements DittoUploader { + private baseUrl: string; + private apiUrl: string; + private fetch: typeof fetch; + + constructor(opts: IPFSUploaderOpts) { + this.baseUrl = opts.baseUrl; + this.apiUrl = opts.apiUrl ?? 'http://localhost:5001'; + this.fetch = opts.fetch ?? globalThis.fetch; + } + + async upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]> { + const url = new URL('/api/v0/add', this.apiUrl); + + const formData = new FormData(); + formData.append('file', file); + + const response = await this.fetch(url, { + method: 'POST', + body: formData, + signal: opts?.signal, + }); + + const { Hash: cid } = IPFSUploader.schema().parse(await response.json()); + + return [ + ['url', new URL(`/ipfs/${cid}`, this.baseUrl).toString()], + ['m', file.type], + ['cid', cid], + ['size', file.size.toString()], + ]; + } + + async delete(cid: string, opts?: { signal?: AbortSignal }): Promise { + const url = new URL('/api/v0/pin/rm', this.apiUrl); + + const query = new URLSearchParams(); + query.set('arg', cid); + url.search = query.toString(); + + await this.fetch(url, { + method: 'POST', + signal: opts?.signal, + }); + } + + /** Response schema for POST `/api/v0/add`. */ + static schema() { + return z.object({ + Name: z.string(), + Hash: z.string(), + Size: z.string(), + }); + } +} diff --git a/src/uploaders/NostrBuildUploader.ts b/src/uploaders/NostrBuildUploader.ts new file mode 100644 index 00000000..7e164481 --- /dev/null +++ b/src/uploaders/NostrBuildUploader.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; + +import { DittoUploader } from '@/interfaces/DittoUploader.ts'; + +export interface NostrBuildUploaderOpts { + endpoint?: string; + fetch?: typeof fetch; +} + +/** Upload files to nostr.build or another compatible server. */ +export class NostrBuildUploader implements DittoUploader { + constructor(private opts: NostrBuildUploaderOpts) {} + + async upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]> { + const { endpoint = 'https://nostr.build/api/v2/upload/files', fetch = globalThis.fetch } = this.opts; + + const formData = new FormData(); + formData.append('fileToUpload', file); + + const response = await fetch(endpoint, { + method: 'POST', + body: formData, + signal: opts?.signal, + }); + + const json = await response.json(); + const [data] = NostrBuildUploader.schema().parse(json).data; + + const tags: [['url', string], ...string[][]] = [ + ['url', data.url], + ['m', data.mime], + ['x', data.sha256], + ['ox', data.original_sha256], + ['size', data.size.toString()], + ]; + + if (data.dimensions) { + tags.push(['dim', `${data.dimensions.width}x${data.dimensions.height}`]); + } + + if (data.blurhash) { + tags.push(['blurhash', data.blurhash]); + } + + return tags; + } + + /** nostr.build API response schema. */ + private static schema() { + return z.object({ + data: z.object({ + url: z.string().url(), + blurhash: z.string().optional().catch(undefined), + sha256: z.string(), + original_sha256: z.string(), + mime: z.string(), + size: z.number(), + dimensions: z.object({ + width: z.number(), + height: z.number(), + }).optional().catch(undefined), + }).array().min(1), + }); + } +} diff --git a/src/uploaders/s3.ts b/src/uploaders/S3Uploader.ts similarity index 58% rename from src/uploaders/s3.ts rename to src/uploaders/S3Uploader.ts index aaff8c82..f210ce87 100644 --- a/src/uploaders/s3.ts +++ b/src/uploaders/S3Uploader.ts @@ -6,17 +6,34 @@ import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; +import { DittoUploader } from '@/interfaces/DittoUploader.ts'; -import type { Uploader } from './types.ts'; +export interface S3UploaderOpts { + endPoint: string; + region: string; + accessKey?: string; + secretKey?: string; + bucket?: string; + pathStyle?: boolean; + port?: number; + sessionToken?: string; + useSSL?: boolean; +} /** S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. */ -const s3Uploader: Uploader = { - async upload(file) { +export class S3Uploader implements DittoUploader { + private client: S3Client; + + constructor(opts: S3UploaderOpts) { + this.client = new S3Client(opts); + } + + async upload(file: File): Promise<[['url', string], ...string[][]]> { const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); const ext = extensionsByType(file.type)?.[0] ?? 'bin'; const filename = `${sha256}.${ext}`; - await client().putObject(filename, file.stream(), { + await this.client.putObject(filename, file.stream(), { metadata: { 'Content-Type': file.type, 'x-amz-acl': 'public-read', @@ -24,6 +41,7 @@ const s3Uploader: Uploader = { }); const { pathStyle, bucket } = Conf.s3; + const path = (pathStyle && bucket) ? join(bucket, filename) : filename; const url = new URL(path, Conf.mediaDomain).toString(); @@ -33,15 +51,9 @@ const s3Uploader: Uploader = { ['x', sha256], ['size', file.size.toString()], ]; - }, - async delete(id) { - await client().deleteObject(id); - }, -}; + } -/** Build S3 client from config. */ -function client() { - return new S3Client({ ...Conf.s3 }); + async delete(objectName: string) { + await this.client.deleteObject(objectName); + } } - -export { s3Uploader }; diff --git a/src/uploaders/config.ts b/src/uploaders/config.ts deleted file mode 100644 index 3f3aac74..00000000 --- a/src/uploaders/config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Conf } from '@/config.ts'; - -import { ipfsUploader } from '@/uploaders/ipfs.ts'; -import { localUploader } from '@/uploaders/local.ts'; -import { nostrbuildUploader } from '@/uploaders/nostrbuild.ts'; -import { s3Uploader } from '@/uploaders/s3.ts'; - -import type { Uploader } from './types.ts'; - -/** Meta-uploader determined from configuration. */ -const configUploader: Uploader = { - upload(file, opts) { - return uploader().upload(file, opts); - }, - async delete(id, opts) { - return await uploader().delete?.(id, opts); - }, -}; - -/** Get the uploader module based on configuration. */ -function uploader() { - switch (Conf.uploader) { - case 's3': - return s3Uploader; - case 'ipfs': - return ipfsUploader; - case 'local': - return localUploader; - case 'nostrbuild': - return nostrbuildUploader; - default: - throw new Error('No `DITTO_UPLOADER` configured. Uploads are disabled.'); - } -} - -export { configUploader }; diff --git a/src/uploaders/ipfs.ts b/src/uploaders/ipfs.ts deleted file mode 100644 index b83dc2e3..00000000 --- a/src/uploaders/ipfs.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { z } from 'zod'; - -import { Conf } from '@/config.ts'; -import { fetchWorker } from '@/workers/fetch.ts'; - -import type { Uploader } from './types.ts'; - -/** Response schema for POST `/api/v0/add`. */ -const ipfsAddResponseSchema = z.object({ - Name: z.string(), - Hash: z.string(), - Size: z.string(), -}); - -/** - * IPFS uploader. It expects an IPFS node up and running. - * It will try to connect to `http://localhost:5001` by default, - * and upload the file using the REST API. - */ -const ipfsUploader: Uploader = { - async upload(file, opts) { - const url = new URL('/api/v0/add', Conf.ipfs.apiUrl); - - const formData = new FormData(); - formData.append('file', file); - - const response = await fetchWorker(url, { - method: 'POST', - body: formData, - signal: opts?.signal, - }); - - const { Hash: cid } = ipfsAddResponseSchema.parse(await response.json()); - - return [ - ['url', new URL(`/ipfs/${cid}`, Conf.mediaDomain).toString()], - ['m', file.type], - ['cid', cid], - ['size', file.size.toString()], - ]; - }, - async delete(cid, opts) { - const url = new URL('/api/v0/pin/rm', Conf.ipfs.apiUrl); - - const query = new URLSearchParams(); - query.set('arg', cid); - - url.search = query.toString(); - - await fetchWorker(url, { - method: 'POST', - signal: opts?.signal, - }); - }, -}; - -export { ipfsUploader }; diff --git a/src/uploaders/local.ts b/src/uploaders/local.ts deleted file mode 100644 index d5cd46ad..00000000 --- a/src/uploaders/local.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { join } from 'node:path'; - -import { crypto } from '@std/crypto'; -import { encodeHex } from '@std/encoding/hex'; -import { extensionsByType } from '@std/media-types'; - -import { Conf } from '@/config.ts'; - -import type { Uploader } from './types.ts'; - -/** Local filesystem uploader. */ -const localUploader: Uploader = { - async upload(file) { - const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); - const ext = extensionsByType(file.type)?.[0] ?? 'bin'; - const filename = `${sha256}.${ext}`; - - await Deno.mkdir(Conf.uploadsDir, { recursive: true }); - await Deno.writeFile(join(Conf.uploadsDir, filename), file.stream()); - - const { mediaDomain } = Conf; - const url = new URL(mediaDomain); - const path = url.pathname === '/' ? filename : join(url.pathname, filename); - - return [ - ['url', new URL(path, url).toString()], - ['m', file.type], - ['x', sha256], - ['size', file.size.toString()], - ]; - }, - async delete(id) { - await Deno.remove(join(Conf.uploadsDir, id)); - }, -}; - -export { localUploader }; diff --git a/src/uploaders/nostrbuild.ts b/src/uploaders/nostrbuild.ts deleted file mode 100644 index 8bca331b..00000000 --- a/src/uploaders/nostrbuild.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Conf } from '@/config.ts'; -import { nostrbuildSchema } from '@/schemas/nostrbuild.ts'; - -import type { Uploader } from './types.ts'; - -/** nostr.build uploader. */ -export const nostrbuildUploader: Uploader = { - async upload(file) { - const formData = new FormData(); - formData.append('fileToUpload', file); - - const response = await fetch(Conf.nostrbuildEndpoint, { - method: 'POST', - body: formData, - }); - - const json = await response.json(); - const [data] = nostrbuildSchema.parse(json).data; - - const tags: [['url', string], ...string[][]] = [ - ['url', data.url], - ['m', data.mime], - ['x', data.sha256], - ['ox', data.original_sha256], - ['size', file.size.toString()], - ['blurhash', data.blurhash], - ]; - - if (data.dimensions) { - tags.push(['dim', `${data.dimensions.width}x${data.dimensions.height}`]); - } - - return tags; - }, -}; diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts deleted file mode 100644 index 81b8a0a7..00000000 --- a/src/uploaders/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** Modular uploader interface, to support uploading to different backends. */ -interface Uploader { - /** Upload the file to the backend. */ - upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]>; - /** Delete the file from the backend. */ - delete?(cid: string, opts?: { signal?: AbortSignal }): Promise; -} - -export type { Uploader }; From 6542d6a77789dd1a662696a7142497334fe9df5a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 22:04:43 -0500 Subject: [PATCH 239/252] Move uploader.ts to utils, make it kind of like api.ts --- src/controllers/api/accounts.ts | 11 +++-------- src/controllers/api/media.ts | 9 ++------- src/{ => utils}/upload.ts | 12 ++++++++++-- 3 files changed, 15 insertions(+), 17 deletions(-) rename src/{ => utils}/upload.ts (75%) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index d67fd888..4777f56f 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -8,7 +8,7 @@ import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts'; -import { uploadFile } from '@/upload.ts'; +import { uploadFile } from '@/utils/upload.ts'; import { nostrNow } from '@/utils.ts'; import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; import { lookupAccount } from '@/utils/lookup.ts'; @@ -202,7 +202,6 @@ const updateCredentialsSchema = z.object({ const updateCredentialsController: AppController = async (c) => { const pubkey = await c.get('signer')?.getPublicKey()!; - const uploader = c.get('uploader'); const body = await parseBody(c.req.raw); const result = updateCredentialsSchema.safeParse(body); @@ -221,13 +220,9 @@ const updateCredentialsController: AppController = async (c) => { nip05, } = result.data; - if ((avatarFile || headerFile) && !uploader) { - return c.json({ error: 'No uploader configured.' }, 500); - } - const [avatar, header] = await Promise.all([ - (avatarFile && uploader) ? uploadFile(uploader, avatarFile, { pubkey }) : undefined, - (headerFile && uploader) ? uploadFile(uploader, headerFile, { pubkey }) : undefined, + avatarFile ? uploadFile(c, avatarFile, { pubkey }) : undefined, + headerFile ? uploadFile(c, headerFile, { pubkey }) : undefined, ]); meta.name = display_name ?? meta.name; diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index 101b7767..71b3e782 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -4,7 +4,7 @@ import { AppController } from '@/app.ts'; import { fileSchema } from '@/schema.ts'; import { parseBody } from '@/utils/api.ts'; import { renderAttachment } from '@/views/mastodon/attachments.ts'; -import { uploadFile } from '@/upload.ts'; +import { uploadFile } from '@/utils/upload.ts'; const mediaBodySchema = z.object({ file: fileSchema, @@ -14,11 +14,6 @@ const mediaBodySchema = z.object({ }); const mediaController: AppController = async (c) => { - const uploader = c.get('uploader'); - if (!uploader) { - return c.json({ error: 'No uploader configured.' }, 500); - } - const pubkey = await c.get('signer')?.getPublicKey()!; const result = mediaBodySchema.safeParse(await parseBody(c.req.raw)); const { signal } = c.req.raw; @@ -29,7 +24,7 @@ const mediaController: AppController = async (c) => { try { const { file, description } = result.data; - const media = await uploadFile(uploader, file, { pubkey, description }, signal); + const media = await uploadFile(c, file, { pubkey, description }, signal); return c.json(renderAttachment(media)); } catch (e) { console.error(e); diff --git a/src/upload.ts b/src/utils/upload.ts similarity index 75% rename from src/upload.ts rename to src/utils/upload.ts index 0d1a085f..c4f2fc58 100644 --- a/src/upload.ts +++ b/src/utils/upload.ts @@ -1,6 +1,7 @@ +import { AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import { insertUnattachedMedia, UnattachedMedia } from '@/db/unattached-media.ts'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; +import { HTTPException } from 'hono'; interface FileMeta { pubkey: string; description?: string; @@ -8,11 +9,18 @@ interface FileMeta { /** Upload a file, track it in the database, and return the resulting media object. */ export async function uploadFile( - uploader: DittoUploader, + c: AppContext, file: File, meta: FileMeta, signal?: AbortSignal, ): Promise { + const uploader = c.get('uploader'); + if (!uploader) { + throw new HTTPException(500, { + res: c.json({ error: 'No uploader configured.' }), + }); + } + const { pubkey, description } = meta; if (file.size > Conf.maxUploadSize) { From 24659d8edb0cf1daf9c20090382c50a59994c561 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 22:11:54 -0500 Subject: [PATCH 240/252] IPFSUploader: make schema private --- src/uploaders/IPFSUploader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index ceb4e82e..9141e784 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -60,7 +60,7 @@ export class IPFSUploader implements DittoUploader { } /** Response schema for POST `/api/v0/add`. */ - static schema() { + private static schema() { return z.object({ Name: z.string(), Hash: z.string(), From acef173ac465c40b60919dbcdcb842ede7e0ff39 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 22:15:33 -0500 Subject: [PATCH 241/252] Do things the boilerplatey way just for consistency --- src/uploaders/DenoUploader.ts | 19 +++++++++++-------- src/uploaders/NostrBuildUploader.ts | 12 ++++++++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/uploaders/DenoUploader.ts b/src/uploaders/DenoUploader.ts index 6c2e6d48..e2224ab5 100644 --- a/src/uploaders/DenoUploader.ts +++ b/src/uploaders/DenoUploader.ts @@ -13,19 +13,23 @@ export interface DenoUploaderOpts { /** Local Deno filesystem uploader. */ export class DenoUploader implements DittoUploader { - constructor(private opts: DenoUploaderOpts) {} + baseUrl: string; + dir: string; + + constructor(opts: DenoUploaderOpts) { + this.baseUrl = opts.baseUrl; + this.dir = opts.dir; + } async upload(file: File): Promise<[['url', string], ...string[][]]> { - const { dir, baseUrl } = this.opts; - const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); const ext = extensionsByType(file.type)?.[0] ?? 'bin'; const filename = `${sha256}.${ext}`; - await Deno.mkdir(dir, { recursive: true }); - await Deno.writeFile(join(dir, filename), file.stream()); + await Deno.mkdir(this.dir, { recursive: true }); + await Deno.writeFile(join(this.dir, filename), file.stream()); - const url = new URL(baseUrl); + const url = new URL(this.baseUrl); const path = url.pathname === '/' ? filename : join(url.pathname, filename); return [ @@ -37,8 +41,7 @@ export class DenoUploader implements DittoUploader { } async delete(filename: string) { - const { dir } = this.opts; - const path = join(dir, filename); + const path = join(this.dir, filename); await Deno.remove(path); } } diff --git a/src/uploaders/NostrBuildUploader.ts b/src/uploaders/NostrBuildUploader.ts index 7e164481..ff4a4f0e 100644 --- a/src/uploaders/NostrBuildUploader.ts +++ b/src/uploaders/NostrBuildUploader.ts @@ -9,15 +9,19 @@ export interface NostrBuildUploaderOpts { /** Upload files to nostr.build or another compatible server. */ export class NostrBuildUploader implements DittoUploader { - constructor(private opts: NostrBuildUploaderOpts) {} + private endpoint: string; + private fetch: typeof fetch; + + constructor(opts: NostrBuildUploaderOpts) { + this.endpoint = opts.endpoint ?? 'https://nostr.build/api/v2/upload/files'; + this.fetch = opts.fetch ?? globalThis.fetch; + } async upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]> { - const { endpoint = 'https://nostr.build/api/v2/upload/files', fetch = globalThis.fetch } = this.opts; - const formData = new FormData(); formData.append('fileToUpload', file); - const response = await fetch(endpoint, { + const response = await this.fetch(this.endpoint, { method: 'POST', body: formData, signal: opts?.signal, From 5523c3fc0eeca605cdbf5691a37b7581001b0928 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 23:02:59 -0500 Subject: [PATCH 242/252] verifyCredentials: wait up to 5 seconds --- src/controllers/api/accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 4777f56f..f66e0aca 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -47,7 +47,7 @@ const createAccountController: AppController = async (c) => { const verifyCredentialsController: AppController = async (c) => { const pubkey = await c.get('signer')?.getPublicKey()!; - const event = await getAuthor(pubkey, { relations: ['author_stats'] }); + const event = await getAuthor(pubkey, { signal: AbortSignal.timeout(5000) }); if (event) { return c.json(await renderAccount(event, { withSource: true })); } else { From 7f5179efcac065ac902a1b84f03d610a9f9e5f63 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 09:13:53 -0500 Subject: [PATCH 243/252] renderAttachment: guess mime from url --- src/views/mastodon/attachments.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 0b1b8eb1..2d658041 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -1,9 +1,12 @@ +import { getUrlMediaType } from '@/utils/media.ts'; + /** Render Mastodon media attachment. */ function renderAttachment(media: { id?: string; data: string[][] }) { const { id, data: tags } = media; - const m = tags.find(([name]) => name === 'm')?.[1]; const url = tags.find(([name]) => name === 'url')?.[1]; + + const m = tags.find(([name]) => name === 'm')?.[1] ?? getUrlMediaType(url!); const alt = tags.find(([name]) => name === 'alt')?.[1]; const cid = tags.find(([name]) => name === 'cid')?.[1]; const dim = tags.find(([name]) => name === 'dim')?.[1]; From 540bd058a2b727df2c8e6e6af4e04ad7057f8a11 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 11:33:59 -0500 Subject: [PATCH 244/252] Fix NIP-27 mentions --- src/controllers/api/statuses.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index e620b930..291d970c 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,5 +1,6 @@ import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import ISO6391 from 'iso-639-1'; +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; @@ -111,7 +112,11 @@ const createStatusController: AppController = async (c) => { pubkeys.add(pubkey); } - return `nostr:${pubkey}`; + try { + return `nostr:${nip19.npubEncode(pubkey)}`; + } catch { + return match; + } }); // Explicit addressing From 9754e29603d404d20d6db3a2c435570266f507bb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 11:45:42 -0500 Subject: [PATCH 245/252] accountSearchController: respect the `limit` param --- src/controllers/api/accounts.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index f66e0aca..39161ef1 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -86,25 +86,35 @@ const accountLookupController: AppController = async (c) => { } }; -const accountSearchController: AppController = async (c) => { - const q = c.req.query('q'); +const accountSearchQuerySchema = z.object({ + q: z.string().transform(decodeURIComponent), + resolve: booleanParamSchema.optional().transform(Boolean), + following: z.boolean().default(false), + limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)), +}); - if (!q) { - return c.json({ error: 'Missing `q` query parameter.' }, 422); +const accountSearchController: AppController = async (c) => { + const result = accountSearchQuerySchema.safeParse(c.req.query()); + const { signal } = c.req.raw; + + if (!result.success) { + return c.json({ error: 'Bad request', schema: result.error }, 422); } + const { q, limit } = result.data; + const query = decodeURIComponent(q); const store = await Storages.search(); const [event, events] = await Promise.all([ lookupAccount(query), - store.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), + store.query([{ kinds: [0], search: query, limit }], { signal }), ]); const results = await hydrateEvents({ events: event ? [event, ...events] : events, store, - signal: c.req.raw.signal, + signal, }); if ((results.length < 1) && query.match(/npub1\w+/)) { From 7c5b7c5d835e23cd5fc2d2354f2d9a970e6e2d3b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 15:38:42 -0500 Subject: [PATCH 246/252] Upgrade Nostrify to v0.22.0 --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 946b4e02..5567cd24 100644 --- a/deno.json +++ b/deno.json @@ -22,7 +22,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/nostrify": "jsr:@nostrify/nostrify@^0.21.1", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.22.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", From f0b247130f46875275c814da6386952d08038071 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 15:42:45 -0500 Subject: [PATCH 247/252] Add support for Blossom uploader --- src/config.ts | 4 ++++ src/middleware/uploaderMiddleware.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/config.ts b/src/config.ts index f3b472ea..cc149983 100644 --- a/src/config.ts +++ b/src/config.ts @@ -140,6 +140,10 @@ class Conf { static get nostrbuildEndpoint(): string { return Deno.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; } + /** Default Blossom servers to use when the `blossom` uploader is set. */ + static get blossomServers(): string[] { + return Deno.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/']; + } /** Module to upload files with. */ static get uploader() { return Deno.env.get('DITTO_UPLOADER'); diff --git a/src/middleware/uploaderMiddleware.ts b/src/middleware/uploaderMiddleware.ts index 8279a122..b0ee570a 100644 --- a/src/middleware/uploaderMiddleware.ts +++ b/src/middleware/uploaderMiddleware.ts @@ -1,3 +1,5 @@ +import { BlossomUploader } from '@nostrify/nostrify/uploaders'; + import { AppMiddleware } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DenoUploader } from '@/uploaders/DenoUploader.ts'; @@ -8,6 +10,8 @@ import { fetchWorker } from '@/workers/fetch.ts'; /** Set an uploader for the user. */ export const uploaderMiddleware: AppMiddleware = async (c, next) => { + const signer = c.get('signer'); + switch (Conf.uploader) { case 's3': c.set('uploader', new S3Uploader(Conf.s3)); @@ -21,6 +25,11 @@ export const uploaderMiddleware: AppMiddleware = async (c, next) => { case 'nostrbuild': c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, fetch: fetchWorker })); break; + case 'blossom': + if (signer) { + c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: fetchWorker })); + } + break; } await next(); From 0541287f0e0b6a5a2c8b10a2835e66b26f88d26c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 15:43:24 -0500 Subject: [PATCH 248/252] Replace our NostrBuildUploader with the one from Nostrify --- src/middleware/uploaderMiddleware.ts | 5 +- src/uploaders/NostrBuildUploader.ts | 69 ---------------------------- 2 files changed, 2 insertions(+), 72 deletions(-) delete mode 100644 src/uploaders/NostrBuildUploader.ts diff --git a/src/middleware/uploaderMiddleware.ts b/src/middleware/uploaderMiddleware.ts index b0ee570a..38e8aceb 100644 --- a/src/middleware/uploaderMiddleware.ts +++ b/src/middleware/uploaderMiddleware.ts @@ -1,10 +1,9 @@ -import { BlossomUploader } from '@nostrify/nostrify/uploaders'; +import { BlossomUploader, NostrBuildUploader } from '@nostrify/nostrify/uploaders'; import { AppMiddleware } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DenoUploader } from '@/uploaders/DenoUploader.ts'; import { IPFSUploader } from '@/uploaders/IPFSUploader.ts'; -import { NostrBuildUploader } from '@/uploaders/NostrBuildUploader.ts'; import { S3Uploader } from '@/uploaders/S3Uploader.ts'; import { fetchWorker } from '@/workers/fetch.ts'; @@ -23,7 +22,7 @@ export const uploaderMiddleware: AppMiddleware = async (c, next) => { c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir })); break; case 'nostrbuild': - c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, fetch: fetchWorker })); + c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: fetchWorker })); break; case 'blossom': if (signer) { diff --git a/src/uploaders/NostrBuildUploader.ts b/src/uploaders/NostrBuildUploader.ts deleted file mode 100644 index ff4a4f0e..00000000 --- a/src/uploaders/NostrBuildUploader.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { z } from 'zod'; - -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; - -export interface NostrBuildUploaderOpts { - endpoint?: string; - fetch?: typeof fetch; -} - -/** Upload files to nostr.build or another compatible server. */ -export class NostrBuildUploader implements DittoUploader { - private endpoint: string; - private fetch: typeof fetch; - - constructor(opts: NostrBuildUploaderOpts) { - this.endpoint = opts.endpoint ?? 'https://nostr.build/api/v2/upload/files'; - this.fetch = opts.fetch ?? globalThis.fetch; - } - - async upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]> { - const formData = new FormData(); - formData.append('fileToUpload', file); - - const response = await this.fetch(this.endpoint, { - method: 'POST', - body: formData, - signal: opts?.signal, - }); - - const json = await response.json(); - const [data] = NostrBuildUploader.schema().parse(json).data; - - const tags: [['url', string], ...string[][]] = [ - ['url', data.url], - ['m', data.mime], - ['x', data.sha256], - ['ox', data.original_sha256], - ['size', data.size.toString()], - ]; - - if (data.dimensions) { - tags.push(['dim', `${data.dimensions.width}x${data.dimensions.height}`]); - } - - if (data.blurhash) { - tags.push(['blurhash', data.blurhash]); - } - - return tags; - } - - /** nostr.build API response schema. */ - private static schema() { - return z.object({ - data: z.object({ - url: z.string().url(), - blurhash: z.string().optional().catch(undefined), - sha256: z.string(), - original_sha256: z.string(), - mime: z.string(), - size: z.number(), - dimensions: z.object({ - width: z.number(), - height: z.number(), - }).optional().catch(undefined), - }).array().min(1), - }); - } -} From 6f6e87525e9b7124e52af8fc2451e57cdbd62828 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 19 May 2024 15:57:04 -0500 Subject: [PATCH 249/252] Remove DittoUploader interface in favor of NUploader --- src/app.ts | 5 ++--- src/interfaces/DittoUploader.ts | 3 --- src/uploaders/DenoUploader.ts | 5 ++--- src/uploaders/IPFSUploader.ts | 5 ++--- src/uploaders/S3Uploader.ts | 4 ++-- 5 files changed, 8 insertions(+), 14 deletions(-) delete mode 100644 src/interfaces/DittoUploader.ts diff --git a/src/app.ts b/src/app.ts index ddc99904..5300b485 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NostrSigner, NStore } from '@nostrify/nostrify'; +import { NostrEvent, NostrSigner, NStore, NUploader } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; @@ -81,7 +81,6 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; @@ -97,7 +96,7 @@ interface AppEnv extends HonoEnv { /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ signer?: NostrSigner; /** Uploader for the user to upload files. */ - uploader?: DittoUploader; + uploader?: NUploader; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; /** Store */ diff --git a/src/interfaces/DittoUploader.ts b/src/interfaces/DittoUploader.ts deleted file mode 100644 index 08cbf504..00000000 --- a/src/interfaces/DittoUploader.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DittoUploader { - upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]>; -} diff --git a/src/uploaders/DenoUploader.ts b/src/uploaders/DenoUploader.ts index e2224ab5..fd30d8c6 100644 --- a/src/uploaders/DenoUploader.ts +++ b/src/uploaders/DenoUploader.ts @@ -1,18 +1,17 @@ import { join } from 'node:path'; +import { NUploader } from '@nostrify/nostrify'; import { crypto } from '@std/crypto'; import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; - export interface DenoUploaderOpts { baseUrl: string; dir: string; } /** Local Deno filesystem uploader. */ -export class DenoUploader implements DittoUploader { +export class DenoUploader implements NUploader { baseUrl: string; dir: string; diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 9141e784..7bf5165b 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,7 +1,6 @@ +import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; - export interface IPFSUploaderOpts { baseUrl: string; apiUrl?: string; @@ -13,7 +12,7 @@ export interface IPFSUploaderOpts { * It will try to connect to `http://localhost:5001` by default, * and upload the file using the REST API. */ -export class IPFSUploader implements DittoUploader { +export class IPFSUploader implements NUploader { private baseUrl: string; private apiUrl: string; private fetch: typeof fetch; diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index f210ce87..b74796ab 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -1,12 +1,12 @@ import { join } from 'node:path'; import { S3Client } from '@bradenmacdonald/s3-lite-client'; +import { NUploader } from '@nostrify/nostrify'; import { crypto } from '@std/crypto'; import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; export interface S3UploaderOpts { endPoint: string; @@ -21,7 +21,7 @@ export interface S3UploaderOpts { } /** S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. */ -export class S3Uploader implements DittoUploader { +export class S3Uploader implements NUploader { private client: S3Client; constructor(opts: S3UploaderOpts) { From 7b099ee5659f278dfe49802e79c9e878ae4d69a1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 20 May 2024 11:39:31 -0500 Subject: [PATCH 250/252] EventsDB: don't index the user's bio for kind 0 events --- src/storages/EventsDB.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index ef51a892..5a3839a5 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -180,8 +180,8 @@ class EventsDB implements NStore { /** Build search content for a user. */ static buildUserSearchContent(event: NostrEvent): string { - const { name, nip05, about } = n.json().pipe(n.metadata()).catch({}).parse(event.content); - return [name, nip05, about].filter(Boolean).join('\n'); + const { name, nip05 } = n.json().pipe(n.metadata()).catch({}).parse(event.content); + return [name, nip05].filter(Boolean).join('\n'); } /** Build search content from tag values. */ From 98fd4babcebc2d61506be13a370d32758e5e49c4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 20 May 2024 14:46:53 -0300 Subject: [PATCH 251/252] test(EventsDB): use eventFixture() --- src/storages/EventsDB.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/storages/EventsDB.test.ts b/src/storages/EventsDB.test.ts index 2f343791..16b429d4 100644 --- a/src/storages/EventsDB.test.ts +++ b/src/storages/EventsDB.test.ts @@ -9,10 +9,7 @@ import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { RelayError } from '@/RelayError.ts'; import { EventsDB } from '@/storages/EventsDB.ts'; -import { genEvent } from '@/test.ts'; - -import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; -import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; +import { eventFixture, genEvent } from '@/test.ts'; /** Create in-memory database for testing. */ const createDB = async () => { @@ -28,6 +25,7 @@ const createDB = async () => { Deno.test('count filters', async () => { const { eventsDB } = await createDB(); + const event1 = await eventFixture('event-1'); assertEquals((await eventsDB.count([{ kinds: [1] }])).count, 0); await eventsDB.event(event1); @@ -37,6 +35,7 @@ Deno.test('count filters', async () => { Deno.test('insert and filter events', async () => { const { eventsDB } = await createDB(); + const event1 = await eventFixture('event-1'); await eventsDB.event(event1); assertEquals(await eventsDB.query([{ kinds: [1] }]), [event1]); @@ -52,6 +51,7 @@ Deno.test('insert and filter events', async () => { Deno.test('query events with domain search filter', async () => { const { eventsDB, kysely } = await createDB(); + const event1 = await eventFixture('event-1'); await eventsDB.event(event1); assertEquals(await eventsDB.query([{}]), [event1]); @@ -180,7 +180,7 @@ Deno.test('throws a RelayError when inserting an event deleted by a user', async Deno.test('inserting replaceable events', async () => { const { eventsDB } = await createDB(); - const event = event0; + const event = await eventFixture('event-0'); await eventsDB.event(event); const olderEvent = { ...event, id: '123', created_at: event.created_at - 1 }; From 6861dc1d57c8c9822a1b86a1e251643b6b8e8293 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 20 May 2024 12:49:01 -0500 Subject: [PATCH 252/252] Fix crash parsing Lightning URL Fixes https://gitlab.com/soapbox-pub/ditto/-/issues/139 --- src/utils/lnurl.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/lnurl.ts b/src/utils/lnurl.ts index ea5ce8a6..af344f22 100644 --- a/src/utils/lnurl.ts +++ b/src/utils/lnurl.ts @@ -28,8 +28,12 @@ function getLnurl({ lud06, lud16 }: { lud06?: string; lud16?: string }, limit?: if (lud16) { const [name, host] = lud16.split('@'); if (name && host) { - const url = new URL(`/.well-known/lnurlp/${name}`, `https://${host}`); - return LNURL.encode(url, limit); + try { + const url = new URL(`/.well-known/lnurlp/${name}`, `https://${host}`); + return LNURL.encode(url, limit); + } catch { + return; + } } } }