From 616c405f0f54ec4ed5a586a0f25c3395543d6b2a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Jun 2024 14:14:59 -0500 Subject: [PATCH 1/8] Add an endpoint to request a NIP-05 name --- src/app.ts | 3 ++- src/controllers/api/ditto.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index bb1c5b6b..4378445f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,7 +30,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 { adminRelaysController, adminSetRelaysController, inviteRequestController } 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'; @@ -239,6 +239,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/ditto/nip05', requireSigner, inviteRequestController); app.post('/api/v1/ditto/zap', requireSigner, zapController); app.post('/api/v1/reports', requireSigner, reportController); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index df4f210e..cf08cd3b 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -5,6 +5,7 @@ import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { createEvent } from '@/utils/api.ts'; const markerSchema = z.enum(['read', 'write']); @@ -58,3 +59,25 @@ function renderRelays(event: NostrEvent): RelayEntity[] { return acc; }, [] as RelayEntity[]); } + +const inviteRequestSchema = z.object({ + nip05: z.string().email(), + reason: z.string().max(500).optional(), +}); + +export const inviteRequestController: AppController = async (c) => { + const { nip05, reason } = inviteRequestSchema.parse(await c.req.json()); + + await createEvent({ + kind: 3036, + content: reason, + tags: [ + ['r', nip05], + ['L', 'nip05.domain'], + ['l', nip05.split('@')[1], 'nip05.domain'], + ['p', Conf.pubkey], + ], + }, c); + + return new Response('', { status: 204 }); +}; From ab2e9d8dd7ef126cc0c1d099d216966746ff3a60 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Jun 2024 14:19:53 -0500 Subject: [PATCH 2/8] nostr.json: determine nip05 grant based on kind 30360 events --- src/controllers/well-known/nostr.ts | 5 ++++- src/utils/nip05.ts | 11 +++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/controllers/well-known/nostr.ts b/src/controllers/well-known/nostr.ts index 06698887..b6b7af09 100644 --- a/src/controllers/well-known/nostr.ts +++ b/src/controllers/well-known/nostr.ts @@ -10,9 +10,12 @@ const nameSchema = z.string().min(1).regex(/^\w+$/); * https://github.com/nostr-protocol/nips/blob/master/05.md */ const nostrController: AppController = async (c) => { + const store = c.get('store'); + const result = nameSchema.safeParse(c.req.query('name')); const name = result.success ? result.data : undefined; - const pointer = name ? await localNip05Lookup(c.get('store'), name) : undefined; + + const pointer = name ? await localNip05Lookup(store, name) : undefined; if (!name || !pointer) { return c.json({ names: {}, relays: {} }); diff --git a/src/utils/nip05.ts b/src/utils/nip05.ts index 840ef6df..a579da6c 100644 --- a/src/utils/nip05.ts +++ b/src/utils/nip05.ts @@ -45,16 +45,15 @@ const nip05Cache = new SimpleLRU( { max: 500, ttl: Time.hours(1) }, ); -async function localNip05Lookup(store: NStore, name: string): Promise { - const [label] = await store.query([{ - kinds: [1985], +async function localNip05Lookup(store: NStore, localpart: string): Promise { + const [grant] = await store.query([{ + kinds: [30360], + '#d': [`${localpart}@${Conf.url.host}`], authors: [Conf.pubkey], - '#L': ['nip05'], - '#l': [`${name}@${Conf.url.host}`], limit: 1, }]); - const pubkey = label?.tags.find(([name]) => name === 'p')?.[1]; + const pubkey = grant?.tags.find(([name]) => name === 'p')?.[1]; if (pubkey) { return { pubkey, relays: [Conf.relay] }; From 179cafcc23fe2169cc0dcc5f19bc707aa4db20cd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Jun 2024 14:44:45 -0500 Subject: [PATCH 3/8] adminAccountsController: display users with a NIP-05 grant, allow filtering "pending" users --- src/controllers/api/admin.ts | 48 +++++++++++++++++++++++++++--------- src/interfaces/DittoEvent.ts | 1 - 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index d7cd3658..af03db58 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -1,13 +1,14 @@ +import { NostrEvent } from '@nostrify/nostrify'; 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 { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts'; import { addTag } from '@/utils/tags.ts'; -import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; +import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; const adminAccountQuerySchema = z.object({ local: booleanParamSchema.optional(), @@ -36,25 +37,50 @@ const adminAccountsController: AppController = async (c) => { } = adminAccountQuerySchema.parse(c.req.query()); // Not supported. - if (pending || disabled || silenced || suspended || sensitized) { + if (disabled || silenced || suspended || sensitized) { return c.json([]); } const store = await Storages.db(); - const { since, until, limit } = paginationSchema.parse(c.req.query()); + const params = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - 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 store.query([{ kinds: [0], authors: pubkeys }], { signal }); + const pubkeys = new Set(); + const events: NostrEvent[] = []; - for (const event of events) { - const d = event.tags.find(([name]) => name === 'd')?.[1]; - (event as DittoEvent).d_author = authors.find((author) => author.pubkey === d); + if (pending) { + for (const event of await store.query([{ kinds: [3036], ...params }], { signal })) { + pubkeys.add(event.pubkey); + events.push(event); + } + } else { + for (const event of await store.query([{ kinds: [30360], authors: [Conf.pubkey], ...params }], { signal })) { + const pubkey = event.tags.find(([name]) => name === 'd')?.[1]; + if (pubkey) { + pubkeys.add(pubkey); + events.push(event); + } + } } + const authors = await store.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) + .then((events) => hydrateEvents({ store, events, signal })); + const accounts = await Promise.all( - events.map((event) => renderAdminAccount(event)), + [...pubkeys].map(async (pubkey) => { + const author = authors.find((event) => event.pubkey === pubkey); + const account = author ? await renderAdminAccount(author) : await renderAdminAccountFromPubkey(pubkey); + const request = events.find((event) => event.kind === 3036 && event.pubkey === pubkey); + const grant = events.find( + (event) => event.kind === 30360 && event.tags.find(([name]) => name === 'd')?.[1] === pubkey, + ); + + return { + ...account, + invite_request: request ? request.content : null, + approved: !!grant, + }; + }), ); return paginated(c, events, accounts); diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 6f3e1d22..fea8e1ec 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -21,7 +21,6 @@ export interface DittoEvent extends NostrEvent { author_domain?: string; author_stats?: AuthorStats; event_stats?: EventStats; - d_author?: DittoEvent; user?: DittoEvent; repost?: DittoEvent; quote?: DittoEvent; From 4f87287d4595e080ca5507270ec2e8bbe129acb0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Jun 2024 15:26:35 -0500 Subject: [PATCH 4/8] Add invite_request_username property to AdminAccount --- src/controllers/api/admin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index af03db58..8fc8cf3a 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -77,7 +77,8 @@ const adminAccountsController: AppController = async (c) => { return { ...account, - invite_request: request ? request.content : null, + invite_request: request?.content ?? null, + invite_request_username: request?.tags.find(([name]) => name === 'r')?.[1] ?? null, approved: !!grant, }; }), From a30e19b6b2c06bf598e713cb5510a3577899098e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Jun 2024 17:35:48 -0500 Subject: [PATCH 5/8] Fix nip05 endpoints --- src/controllers/api/admin.ts | 2 +- src/controllers/api/ditto.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 8fc8cf3a..8c55318a 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -49,7 +49,7 @@ const adminAccountsController: AppController = async (c) => { const events: NostrEvent[] = []; if (pending) { - for (const event of await store.query([{ kinds: [3036], ...params }], { signal })) { + for (const event of await store.query([{ kinds: [3036], '#p': [Conf.pubkey], ...params }], { signal })) { pubkeys.add(event.pubkey); events.push(event); } diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index cf08cd3b..e6f398db 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -79,5 +79,5 @@ export const inviteRequestController: AppController = async (c) => { ], }, c); - return new Response('', { status: 204 }); + return new Response(null, { status: 204 }); }; From a2d865d6ccea412c27fae5c543dc8aa8ea62ad9c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 8 Jun 2024 17:54:39 -0500 Subject: [PATCH 6/8] Generate an internal event for each report and invite request --- src/pipeline.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/pipeline.ts b/src/pipeline.ts index f59a1eb6..7cdaa60a 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -3,10 +3,12 @@ import Debug from '@soapbox/stickynotes/debug'; import { sql } from 'kysely'; import { LRUCache } from 'lru-cache'; +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 { RelayError } from '@/RelayError.ts'; +import { AdminSigner } from '@/signers/AdminSigner.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { eventAge, parseNip05, Time } from '@/utils.ts'; @@ -53,6 +55,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { } } +async function generateSetEvents(event: NostrEvent): Promise { + const tagsAdmin = event.tags.some(([name, value]) => ['p', 'P'].includes(name) && value === Conf.pubkey); + + if (event.kind === 1984 && tagsAdmin) { + const signer = new AdminSigner(); + + const rel = await signer.signEvent({ + kind: 30383, + content: '', + tags: [ + ['d', event.id], + ['p', event.pubkey], + ['k', '1984'], + ['n', 'open'], + ...[...getTagSet(event.tags, 'p')].map((pubkey) => ['P', pubkey]), + ...[...getTagSet(event.tags, 'e')].map((pubkey) => ['e', pubkey]), + ], + created_at: Math.floor(Date.now() / 1000), + }); + + await handleEvent(rel, AbortSignal.timeout(1000)); + } + + if (event.kind === 3036 && tagsAdmin) { + const signer = new AdminSigner(); + + const rel = await signer.signEvent({ + kind: 30383, + content: '', + tags: [ + ['d', event.id], + ['p', event.pubkey], + ['k', '3036'], + ['n', 'open'], + ], + created_at: Math.floor(Date.now() / 1000), + }); + + await handleEvent(rel, AbortSignal.timeout(1000)); + } +} + export { handleEvent }; From a14515bbe010eee99e601c7fa9c7a6749feb85fa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 8 Jun 2024 19:48:56 -0500 Subject: [PATCH 7/8] Rework reports with event sets --- src/controllers/api/reports.ts | 80 ++++++++++++++++++++++++++-------- src/storages/EventsDB.ts | 2 +- src/utils/api.ts | 17 ++++++-- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 9cb2627a..2092b8a6 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -1,12 +1,14 @@ -import { NSchema as n } from '@nostrify/nostrify'; +import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { createAdminEvent, createEvent, parseBody } from '@/utils/api.ts'; +import { createEvent, paginationSchema, parseBody, updateEventInfo } from '@/utils/api.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderAdminReport } from '@/views/mastodon/reports.ts'; import { renderReport } from '@/views/mastodon/reports.ts'; +import { getTagSet } from '@/utils/tags.ts'; +import { booleanParamSchema } from '@/schema.ts'; const reportSchema = z.object({ account_id: n.id(), @@ -52,18 +54,60 @@ const reportController: AppController = async (c) => { return c.json(await renderReport(event)); }; +const adminReportsSchema = z.object({ + resolved: booleanParamSchema.optional(), + account_id: n.id().optional(), + target_account_id: n.id().optional(), +}); + /** 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({ store, events: events, signal: c.req.raw.signal })) - .then((events) => - Promise.all( - events.map((event) => renderAdminReport(event, { viewerPubkey })), - ) - ); + const params = paginationSchema.parse(c.req.query()); + const { resolved, account_id, target_account_id } = adminReportsSchema.parse(c.req.query()); + + const filter: NostrFilter = { + kinds: [30383], + authors: [Conf.pubkey], + ...params, + }; + + if (typeof resolved === 'boolean') { + filter['#n'] = [resolved ? 'closed' : 'open']; + } + if (account_id) { + filter['#p'] = [account_id]; + } + if (target_account_id) { + filter['#P'] = [target_account_id]; + } + + const orig = await store.query([filter]); + const ids = new Set(); + + for (const event of orig) { + const d = event.tags.find(([name]) => name === 'd')?.[1]; + if (d) { + ids.add(d); + } + } + + const events = await store.query([{ kinds: [1984], ids: [...ids] }]) + .then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal })); + + const reports = await Promise.all( + events.map((event) => { + const internal = orig.find(({ tags }) => tags.some(([name, value]) => name === 'd' && value === event.id)); + const names = getTagSet(internal?.tags ?? [], 'n'); + + return renderAdminReport(event, { + viewerPubkey, + actionTaken: names.has('closed'), + }); + }), + ); return c.json(reports); }; @@ -82,12 +126,13 @@ const adminReportController: AppController = async (c) => { }], { signal }); if (!event) { - return c.json({ error: 'This action is not allowed' }, 403); + return c.json({ error: 'Not found' }, 404); } await hydrateEvents({ events: [event], store, signal }); - return c.json(await renderAdminReport(event, { viewerPubkey: pubkey })); + const report = await renderAdminReport(event, { viewerPubkey: pubkey }); + return c.json(report); }; /** https://docs.joinmastodon.org/methods/admin/reports/#resolve */ @@ -104,18 +149,15 @@ const adminReportResolveController: AppController = async (c) => { }], { signal }); if (!event) { - return c.json({ error: 'This action is not allowed' }, 403); + return c.json({ error: 'Not found' }, 404); } + await updateEventInfo(eventId, { open: false, closed: true }, c); + await hydrateEvents({ events: [event], store, signal }); - await createAdminEvent({ - kind: 5, - tags: [['e', event.id]], - content: 'Report closed.', - }, c); - - return c.json(await renderAdminReport(event, { viewerPubkey: pubkey, actionTaken: true })); + const report = await renderAdminReport(event, { viewerPubkey: pubkey, actionTaken: true }); + return c.json(report); }; export { adminReportController, adminReportResolveController, adminReportsController, reportController }; diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 8dcee6cc..6a949548 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -29,9 +29,9 @@ class EventsDB implements NStore { 'a': ({ count }) => count < 15, 'd': ({ event, count }) => count === 0 && NKinds.parameterizedReplaceable(event.kind), 'e': ({ event, count, value }) => ((event.kind === 10003) || count < 15) && isNostrId(value), + 'k': ({ count, value }) => count === 0 && Number.isInteger(Number(value)), 'L': ({ event, count }) => event.kind === 1985 || count === 0, 'l': ({ event, count }) => event.kind === 1985 || count === 0, - 'media': ({ count, value }) => (count < 4) && isURL(value), 'n': ({ count, value }) => count < 50 && value.length < 50, 'P': ({ count, value }) => count === 0 && isNostrId(value), 'p': ({ event, count, value }) => (count < 15 || event.kind === 3) && isNostrId(value), diff --git a/src/utils/api.ts b/src/utils/api.ts index a9390b5a..1fa397b5 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -107,12 +107,20 @@ async function updateAdminEvent( return createAdminEvent(fn(prev), c); } -async function updateUser(pubkey: string, n: Record, c: AppContext): Promise { +function updateUser(pubkey: string, n: Record, c: AppContext): Promise { + return updateNames(30382, pubkey, n, c); +} + +function updateEventInfo(id: string, n: Record, c: AppContext): Promise { + return updateNames(30383, id, n, c); +} + +async function updateNames(k: number, d: string, n: Record, c: AppContext): Promise { const signer = new AdminSigner(); const admin = await signer.getPublicKey(); return updateAdminEvent( - { kinds: [30382], authors: [admin], '#d': [pubkey], limit: 1 }, + { kinds: [k], authors: [admin], '#d': [d], limit: 1 }, (prev) => { const prevNames = prev?.tags.reduce((acc, [name, value]) => { if (name === 'n') acc[value] = true; @@ -124,10 +132,10 @@ async function updateUser(pubkey: string, n: Record, c: AppCont const other = prev?.tags.filter(([name]) => !['d', 'n'].includes(name)) ?? []; return { - kind: 30382, + kind: k, content: prev?.content ?? '', tags: [ - ['d', pubkey], + ['d', d], ...nTags, ...other, ], @@ -296,6 +304,7 @@ export { parseBody, updateAdminEvent, updateEvent, + updateEventInfo, updateListAdminEvent, updateListEvent, updateUser, From bd6424acf53ef73d14205ef4bdd1356818ffb3bf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 8 Jun 2024 22:16:34 -0500 Subject: [PATCH 8/8] Add preliminary nameRequestsController --- src/app.ts | 4 ++-- src/controllers/api/ditto.ts | 29 ++++++++++++++++++----- src/interfaces/DittoEvent.ts | 2 ++ src/pipeline.ts | 2 +- src/storages/hydrate.ts | 46 ++++++++++++++++++++++++++++-------- src/views/ditto.ts | 25 ++++++++++++++++++++ 6 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 src/views/ditto.ts diff --git a/src/app.ts b/src/app.ts index 198dc7f6..19641d45 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,7 +30,7 @@ import { adminAccountsController, adminActionController } from '@/controllers/ap 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, inviteRequestController } from '@/controllers/api/ditto.ts'; +import { adminRelaysController, adminSetRelaysController, nameRequestController } 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'; @@ -243,7 +243,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/ditto/nip05', requireSigner, inviteRequestController); +app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.post('/api/v1/ditto/zap', requireSigner, zapController); app.post('/api/v1/reports', requireSigner, reportController); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index e6f398db..4cf2cd45 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -3,9 +3,11 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { Storages } from '@/storages.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { Storages } from '@/storages.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; import { createEvent } from '@/utils/api.ts'; +import { renderNameRequest } from '@/views/ditto.ts'; const markerSchema = z.enum(['read', 'write']); @@ -60,15 +62,15 @@ function renderRelays(event: NostrEvent): RelayEntity[] { }, [] as RelayEntity[]); } -const inviteRequestSchema = z.object({ +const nameRequestSchema = z.object({ nip05: z.string().email(), reason: z.string().max(500).optional(), }); -export const inviteRequestController: AppController = async (c) => { - const { nip05, reason } = inviteRequestSchema.parse(await c.req.json()); +export const nameRequestController: AppController = async (c) => { + const { nip05, reason } = nameRequestSchema.parse(await c.req.json()); - await createEvent({ + const event = await createEvent({ kind: 3036, content: reason, tags: [ @@ -79,5 +81,20 @@ export const inviteRequestController: AppController = async (c) => { ], }, c); - return new Response(null, { status: 204 }); + await hydrateEvents({ events: [event], store: await Storages.db() }); + + const nameRequest = await renderNameRequest(event); + return c.json(nameRequest); +}; + +export const nameRequestsController: AppController = async (c) => { + const store = await Storages.db(); + const signer = c.get('signer')!; + const pubkey = await signer.getPublicKey(); + + const events = await store.query([{ kinds: [3036], authors: [pubkey], limit: 20 }]) + .then((events) => hydrateEvents({ events, store })); + + const nameRequests = await Promise.all(events.map(renderNameRequest)); + return c.json(nameRequests); }; diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index fea8e1ec..85e11d65 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -34,4 +34,6 @@ export interface DittoEvent extends NostrEvent { * https://github.com/nostr-protocol/nips/blob/master/56.md */ reported_notes?: DittoEvent[]; + /** Admin event relationship. */ + info?: DittoEvent; } diff --git a/src/pipeline.ts b/src/pipeline.ts index 7cdaa60a..20fed92d 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -212,7 +212,7 @@ async function generateSetEvents(event: NostrEvent): Promise { ['d', event.id], ['p', event.pubkey], ['k', '3036'], - ['n', 'open'], + ['n', 'pending'], ], created_at: Math.floor(Date.now() / 1000), }); diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index ded03d42..a3821aa3 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -44,6 +44,10 @@ async function hydrateEvents(opts: HydrateOpts): Promise { cache.push(event); } + for (const event of await gatherInfo({ events: cache, store, signal })) { + cache.push(event); + } + for (const event of await gatherReportedProfiles({ events: cache, store, signal })) { cache.push(event); } @@ -83,6 +87,7 @@ export function assembleEvents( for (const event of a) { event.author = b.find((e) => matchFilter({ kinds: [0], authors: [event.pubkey] }, e)); event.user = b.find((e) => matchFilter({ kinds: [30382], authors: [admin], '#d': [event.pubkey] }, e)); + event.info = b.find((e) => matchFilter({ kinds: [30383], authors: [admin], '#d': [event.id] }, e)); if (event.kind === 1) { const id = findQuoteTag(event.tags)?.[1] || findQuoteInContent(event.content); @@ -106,20 +111,21 @@ export function assembleEvents( } if (event.kind === 1984) { - const targetAccountId = event.tags.find(([name]) => name === 'p')?.[1]; - if (targetAccountId) { - event.reported_profile = b.find((e) => matchFilter({ kinds: [0], authors: [targetAccountId] }, e)); + const pubkey = event.tags.find(([name]) => name === 'p')?.[1]; + if (pubkey) { + event.reported_profile = b.find((e) => matchFilter({ kinds: [0], authors: [pubkey] }, e)); } - const reportedEvents: DittoEvent[] = []; - const status_ids = event.tags.filter(([name]) => name === 'e').map((tag) => tag[1]); - if (status_ids.length > 0) { - for (const id of status_ids) { - const reportedEvent = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); - if (reportedEvent) reportedEvents.push(reportedEvent); + const reportedEvents: DittoEvent[] = []; + const ids = event.tags.filter(([name]) => name === 'e').map(([_name, value]) => value); + + for (const id of ids) { + const reported = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + if (reported) { + reportedEvents.push(reported); } - event.reported_notes = reportedEvents; } + event.reported_notes = reportedEvents; } event.author_stats = stats.authors.find((stats) => stats.pubkey === event.pubkey); @@ -206,6 +212,26 @@ function gatherUsers({ events, store, signal }: HydrateOpts): Promise { + const ids = new Set(); + + for (const event of events) { + if (event.kind === 3036) { + ids.add(event.id); + } + } + + if (!ids.size) { + return Promise.resolve([]); + } + + return store.query( + [{ ids: [...ids], limit: ids.size }], + { signal }, + ); +} + /** Collect reported notes from the events. */ function gatherReportedNotes({ events, store, signal }: HydrateOpts): Promise { const ids = new Set(); diff --git a/src/views/ditto.ts b/src/views/ditto.ts new file mode 100644 index 00000000..708c5223 --- /dev/null +++ b/src/views/ditto.ts @@ -0,0 +1,25 @@ +import { DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; +import { getTagSet } from '@/utils/tags.ts'; + +export async function renderNameRequest(event: DittoEvent) { + const n = getTagSet(event.info?.tags ?? [], 'n'); + + let approvalStatus = 'pending'; + + if (n.has('approved')) { + approvalStatus = 'approved'; + } + if (n.has('rejected')) { + approvalStatus = 'rejected'; + } + + return { + id: event.id, + account: event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey), + name: event.tags.find(([name]) => name === 'r')?.[1] || '', + reason: event.content, + approval_status: approvalStatus, + created_at: new Date(event.created_at * 1000).toISOString(), + }; +}