From 8a7cae98419b23e665debee161936d1126c7c8a5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 11:03:46 -0500 Subject: [PATCH 1/9] Refactor reports more, add reopen endpoint --- src/app.ts | 7 +++ src/controllers/api/ditto.ts | 97 +++++++++++++++++++++++++++++++--- src/controllers/api/reports.ts | 46 +++++++++++----- src/storages/hydrate.ts | 8 ++- src/views/mastodon/reports.ts | 32 +++++------ 5 files changed, 153 insertions(+), 37 deletions(-) diff --git a/src/app.ts b/src/app.ts index 19641d4..e58bd53 100644 --- a/src/app.ts +++ b/src/app.ts @@ -53,6 +53,7 @@ import { deleteReactionController, reactionController, reactionsController } fro import { relayController } from '@/controllers/nostr/relay.ts'; import { adminReportController, + adminReportReopenController, adminReportResolveController, adminReportsController, reportController, @@ -255,6 +256,12 @@ app.post( requireRole('admin'), adminReportResolveController, ); +app.post( + '/api/v1/admin/reports/:id{[0-9a-f]{64}}/reopen', + requireSigner, + requireRole('admin'), + adminReportReopenController, +); app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminActionController); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 4cf2cd4..d1f9b0d 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -1,12 +1,13 @@ -import { NostrEvent } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { booleanParamSchema } from '@/schema.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 { createEvent, paginated, paginationSchema } from '@/utils/api.ts'; import { renderNameRequest } from '@/views/ditto.ts'; const markerSchema = z.enum(['read', 'write']); @@ -87,14 +88,98 @@ export const nameRequestController: AppController = async (c) => { return c.json(nameRequest); }; +const nameRequestsSchema = z.object({ + approved: booleanParamSchema.optional(), + rejected: booleanParamSchema.optional(), +}); + 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 params = paginationSchema.parse(c.req.query()); + const { approved, rejected } = nameRequestsSchema.parse(c.req.query()); - const nameRequests = await Promise.all(events.map(renderNameRequest)); - return c.json(nameRequests); + const filter: NostrFilter = { + kinds: [30383], + authors: [Conf.pubkey], + '#k': ['3036'], + '#p': [pubkey], + ...params, + }; + + if (approved) { + filter['#n'] = ['approved']; + } + if (rejected) { + filter['#n'] = ['rejected']; + } + + 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: [3036], ids: [...ids] }]) + .then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal })); + + const nameRequests = await Promise.all( + events.map((event) => renderNameRequest(event)), + ); + + return paginated(c, orig, nameRequests); +}; + +const adminNameRequestsSchema = z.object({ + account_id: n.id().optional(), + approved: booleanParamSchema.optional(), + rejected: booleanParamSchema.optional(), +}); + +export const adminNameRequestsController: AppController = async (c) => { + const store = await Storages.db(); + const params = paginationSchema.parse(c.req.query()); + const { account_id, approved, rejected } = adminNameRequestsSchema.parse(c.req.query()); + + const filter: NostrFilter = { + kinds: [30383], + authors: [Conf.pubkey], + '#k': ['3036'], + ...params, + }; + + if (account_id) { + filter['#p'] = [account_id]; + } + if (approved) { + filter['#n'] = ['approved']; + } + if (rejected) { + filter['#n'] = ['rejected']; + } + + 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: [3036], ids: [...ids] }]) + .then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal })); + + const nameRequests = await Promise.all( + events.map((event) => renderNameRequest(event)), + ); + + return paginated(c, orig, nameRequests); }; diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index 2092b8a..9a2750f 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -7,7 +7,6 @@ import { createEvent, paginationSchema, parseBody, updateEventInfo } from '@/uti 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({ @@ -71,6 +70,7 @@ const adminReportsController: AppController = async (c) => { const filter: NostrFilter = { kinds: [30383], authors: [Conf.pubkey], + '#k': ['1984'], ...params, }; @@ -98,15 +98,7 @@ const adminReportsController: AppController = async (c) => { .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'), - }); - }), + events.map((event) => renderAdminReport(event, { viewerPubkey })), ); return c.json(reports); @@ -153,11 +145,39 @@ const adminReportResolveController: AppController = async (c) => { } await updateEventInfo(eventId, { open: false, closed: true }, c); - await hydrateEvents({ events: [event], store, signal }); - const report = await renderAdminReport(event, { viewerPubkey: pubkey, actionTaken: true }); + const report = await renderAdminReport(event, { viewerPubkey: pubkey }); return c.json(report); }; -export { adminReportController, adminReportResolveController, adminReportsController, reportController }; +const adminReportReopenController: AppController = async (c) => { + const eventId = c.req.param('id'); + const { signal } = c.req.raw; + const store = c.get('store'); + const pubkey = await c.get('signer')?.getPublicKey(); + + const [event] = await store.query([{ + kinds: [1984], + ids: [eventId], + limit: 1, + }], { signal }); + + if (!event) { + return c.json({ error: 'Not found' }, 404); + } + + await updateEventInfo(eventId, { open: true, closed: false }, c); + await hydrateEvents({ events: [event], store, signal }); + + const report = await renderAdminReport(event, { viewerPubkey: pubkey }); + return c.json(report); +}; + +export { + adminReportController, + adminReportReopenController, + adminReportResolveController, + adminReportsController, + reportController, +}; diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index a3821aa..9ec9e8c 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -206,6 +206,10 @@ function gatherAuthors({ events, store, signal }: HydrateOpts): Promise { const pubkeys = new Set(events.map((event) => event.pubkey)); + if (!pubkeys.size) { + return Promise.resolve([]); + } + return store.query( [{ kinds: [30382], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }], { signal }, @@ -217,7 +221,7 @@ function gatherInfo({ events, store, signal }: HydrateOpts): Promise(); for (const event of events) { - if (event.kind === 3036) { + if (event.kind === 1984 || event.kind === 3036) { ids.add(event.id); } } @@ -227,7 +231,7 @@ function gatherInfo({ events, store, signal }: HydrateOpts): Promise name === 'p')?.[2]; + const category = event.tags.find(([name]) => name === 'p')?.[2]; const statuses = []; - if (reportEvent.reported_notes) { - for (const status of reportEvent.reported_notes) { + if (event.reported_notes) { + for (const status of event.reported_notes) { statuses.push(await renderStatus(status, { viewerPubkey })); } } - const reportedPubkey = reportEvent.tags.find(([name]) => name === 'p')?.[1]; + const reportedPubkey = event.tags.find(([name]) => name === 'p')?.[1]; if (!reportedPubkey) { return; } + const names = getTagSet(event.info?.tags ?? [], 'n'); + return { - id: reportEvent.id, - action_taken: actionTaken, + id: event.id, + action_taken: names.has('closed'), action_taken_at: null, category, - comment: reportEvent.content, + comment: event.content, forwarded: false, - created_at: nostrDate(reportEvent.created_at).toISOString(), - account: reportEvent.author - ? await renderAdminAccount(reportEvent.author) - : await renderAdminAccountFromPubkey(reportEvent.pubkey), - target_account: reportEvent.reported_profile - ? await renderAdminAccount(reportEvent.reported_profile) + created_at: nostrDate(event.created_at).toISOString(), + account: event.author ? await renderAdminAccount(event.author) : await renderAdminAccountFromPubkey(event.pubkey), + target_account: event.reported_profile + ? await renderAdminAccount(event.reported_profile) : await renderAdminAccountFromPubkey(reportedPubkey), assigned_account: null, action_taken_by_account: null, From 58a01f90de6709ce47f67693e357192aba28a363 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 11:07:37 -0500 Subject: [PATCH 2/9] Paginate reports --- 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 9a2750f..da107ed 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, paginationSchema, parseBody, updateEventInfo } from '@/utils/api.ts'; +import { createEvent, paginated, 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'; @@ -101,7 +101,7 @@ const adminReportsController: AppController = async (c) => { events.map((event) => renderAdminReport(event, { viewerPubkey })), ); - return c.json(reports); + return paginated(c, orig, reports); }; /** https://docs.joinmastodon.org/methods/admin/reports/#get-one */ From 8802cbd77935a9b6874f3ba564ea8ac7c413aecd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 11:24:01 -0500 Subject: [PATCH 3/9] suggest -> suggested --- src/controllers/api/pleroma.ts | 4 ++-- src/controllers/api/suggestions.ts | 4 ++-- src/views/mastodon/accounts.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index f428ce9..31d8545 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -157,7 +157,7 @@ const pleromaAdminSuggestController: AppController = async (c) => { for (const nickname of nicknames) { const pubkey = await lookupPubkey(nickname); if (!pubkey) continue; - await updateUser(pubkey, { suggest: true }, c); + await updateUser(pubkey, { suggested: true }, c); } return new Response(null, { status: 204 }); @@ -169,7 +169,7 @@ const pleromaAdminUnsuggestController: AppController = async (c) => { for (const nickname of nicknames) { const pubkey = await lookupPubkey(nickname); if (!pubkey) continue; - await updateUser(pubkey, { suggest: false }, c); + await updateUser(pubkey, { suggested: false }, c); } return new Response(null, { status: 204 }); diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index c31ffc0..7e461c4 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -31,7 +31,7 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s const pubkey = await signer?.getPublicKey(); const filters: NostrFilter[] = [ - { kinds: [30382], authors: [Conf.pubkey], '#n': ['suggest'], limit }, + { kinds: [30382], authors: [Conf.pubkey], '#n': ['suggested'], limit }, { kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [Conf.pubkey], limit: 1 }, ]; @@ -43,7 +43,7 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s const events = await store.query(filters, { signal }); const [userEvents, followsEvent, mutesEvent, trendingEvent] = [ - events.filter((event) => matchFilter({ kinds: [30382], authors: [Conf.pubkey], '#n': ['suggest'] }, event)), + events.filter((event) => matchFilter({ kinds: [30382], authors: [Conf.pubkey], '#n': ['suggested'] }, event)), pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined, pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined, events.find((event) => diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 9f2f052..99f69f0 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -79,7 +79,7 @@ async function renderAccount( pleroma: { is_admin: names.has('admin'), is_moderator: names.has('admin') || names.has('moderator'), - is_suggested: names.has('suggest'), + is_suggested: names.has('suggested'), is_local: parsed05?.domain === Conf.url.host, settings_store: undefined as unknown, tags: [...getTagSet(event.user?.tags ?? [], 't')], From 594f37ea3348682edebe8c48a2f9edb7b96a5e57 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 11:26:57 -0500 Subject: [PATCH 4/9] Use past-tense for some n-tag values --- src/controllers/api/admin.ts | 6 +++--- src/pipeline.ts | 4 ++-- src/storages/AdminStore.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 9da15bc..a9ec619 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -107,13 +107,13 @@ const adminActionController: AppController = async (c) => { n.sensitive = true; } if (data.type === 'disable') { - n.disable = true; + n.disabled = true; } if (data.type === 'silence') { - n.silence = true; + n.silenced = true; } if (data.type === 'suspend') { - n.suspend = true; + n.suspended = true; } await updateUser(authorId, n, c); diff --git a/src/pipeline.ts b/src/pipeline.ts index 20fed92..9f99520 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -45,10 +45,10 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise Date: Sun, 9 Jun 2024 11:57:10 -0500 Subject: [PATCH 5/9] Add admin name approve/reject endpoints --- src/app.ts | 16 +++++++++- src/controllers/api/ditto.ts | 57 +++++++++++++++++++++++++++++++++++- src/storages/EventsDB.ts | 2 ++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index e58bd53..7bf29ca 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,7 +30,15 @@ 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, nameRequestController } from '@/controllers/api/ditto.ts'; +import { + adminNameApproveController, + adminNameRejectController, + adminNameRequestsController, + adminRelaysController, + adminSetRelaysController, + nameRequestController, + nameRequestsController, +} 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'; @@ -245,6 +253,12 @@ app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysControlle app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); app.post('/api/v1/ditto/names', requireSigner, nameRequestController); +app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); + +app.get('/api/v1/admin/ditto/names', requireRole('admin'), adminNameRequestsController); +app.post('/api/v1/admin/ditto/names/:id{[0-9a-f]{64}}/approve', requireRole('admin'), adminNameApproveController); +app.post('/api/v1/admin/ditto/names/:id{[0-9a-f]{64}}/reject', requireRole('admin'), adminNameRejectController); + 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 d1f9b0d..f8ac5f2 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -7,7 +7,7 @@ import { booleanParamSchema } from '@/schema.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { createEvent, paginated, paginationSchema } from '@/utils/api.ts'; +import { createAdminEvent, createEvent, paginated, paginationSchema, updateEventInfo } from '@/utils/api.ts'; import { renderNameRequest } from '@/views/ditto.ts'; const markerSchema = z.enum(['read', 'write']); @@ -183,3 +183,58 @@ export const adminNameRequestsController: AppController = async (c) => { return paginated(c, orig, nameRequests); }; + +export const adminNameApproveController: AppController = async (c) => { + const eventId = c.req.param('id'); + const store = await Storages.db(); + + const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]); + if (!event) { + return c.json({ error: 'Event not found' }, 404); + } + + const r = event.tags.find(([name]) => name === 'r')?.[1]; + if (!r) { + return c.json({ error: 'NIP-05 not found' }, 404); + } + if (!z.string().email().safeParse(r).success) { + return c.json({ error: 'Invalid NIP-05' }, 400); + } + + const [existing] = await store.query([{ kinds: [30360], authors: [Conf.pubkey], '#d': [r], limit: 1 }]); + if (existing) { + return c.json({ error: 'NIP-05 already granted to another user' }, 400); + } + + await createAdminEvent({ + kind: 30360, + tags: [ + ['d', r], + ['L', 'nip05.domain'], + ['l', r.split('@')[1], 'nip05.domain'], + ['p', event.pubkey], + ], + }, c); + + await updateEventInfo(eventId, { pending: false, approved: true, rejected: false }, c); + await hydrateEvents({ events: [event], store }); + + const nameRequest = await renderNameRequest(event); + return c.json(nameRequest); +}; + +export const adminNameRejectController: AppController = async (c) => { + const eventId = c.req.param('id'); + const store = await Storages.db(); + + const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]); + if (!event) { + return c.json({ error: 'Event not found' }, 404); + } + + await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c); + await hydrateEvents({ events: [event], store }); + + const nameRequest = await renderNameRequest(event); + return c.json(nameRequest); +}; diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 6a94954..c26ebf1 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -223,6 +223,8 @@ class EventsDB implements NStore { return event.content; case 30009: return EventsDB.buildTagsSearchContent(event.tags.filter(([t]) => t !== 'alt')); + case 30360: + return event.tags.find(([name]) => name === 'd')?.[1] || ''; default: return ''; } From 5379863d36678ae348015c2fb4304120a34cc380 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 12:13:50 -0500 Subject: [PATCH 6/9] Tag the nip05 request in the grant event --- src/controllers/api/ditto.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index f8ac5f2..5c8ea9c 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -64,20 +64,20 @@ function renderRelays(event: NostrEvent): RelayEntity[] { } const nameRequestSchema = z.object({ - nip05: z.string().email(), + name: z.string().email(), reason: z.string().max(500).optional(), }); export const nameRequestController: AppController = async (c) => { - const { nip05, reason } = nameRequestSchema.parse(await c.req.json()); + const { name, reason } = nameRequestSchema.parse(await c.req.json()); const event = await createEvent({ kind: 3036, content: reason, tags: [ - ['r', nip05], + ['r', name], ['L', 'nip05.domain'], - ['l', nip05.split('@')[1], 'nip05.domain'], + ['l', name.split('@')[1], 'nip05.domain'], ['p', Conf.pubkey], ], }, c); @@ -213,6 +213,7 @@ export const adminNameApproveController: AppController = async (c) => { ['L', 'nip05.domain'], ['l', r.split('@')[1], 'nip05.domain'], ['p', event.pubkey], + ['e', event.id], ], }, c); From 07a380fb75f24cc95ea77a834b65e36c0fc132b2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 13:43:40 -0500 Subject: [PATCH 7/9] Rework adminAccountsController to display pending accounts from nip05 requests --- src/controllers/api/admin.ts | 112 ++++++++++++++++----------- src/views/ditto.ts | 25 +++--- src/views/mastodon/admin-accounts.ts | 22 ++++-- 3 files changed, 95 insertions(+), 64 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index a9ec619..df5bf96 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -1,13 +1,14 @@ -import { NostrEvent } from '@nostrify/nostrify'; +import { NostrFilter } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; -import { paginated, paginationSchema, parseBody, updateUser } from '@/utils/api.ts'; -import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; +import { paginated, paginationSchema, parseBody, updateUser } from '@/utils/api.ts'; +import { renderNameRequest } from '@/views/ditto.ts'; +import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; const adminAccountQuerySchema = z.object({ local: booleanParamSchema.optional(), @@ -27,62 +28,83 @@ const adminAccountQuerySchema = z.object({ }); const adminAccountsController: AppController = async (c) => { + const store = await Storages.db(); + const params = paginationSchema.parse(c.req.query()); + const { signal } = c.req.raw; const { + local, pending, disabled, silenced, suspended, sensitized, + staff, } = adminAccountQuerySchema.parse(c.req.query()); - // Not supported. - if (disabled || silenced || suspended || sensitized) { - return c.json([]); - } - - const store = await Storages.db(); - const params = paginationSchema.parse(c.req.query()); - const { signal } = c.req.raw; - - const pubkeys = new Set(); - const events: NostrEvent[] = []; - if (pending) { - for (const event of await store.query([{ kinds: [3036], '#p': [Conf.pubkey], ...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); - } + if (disabled || silenced || suspended || sensitized) { + return c.json([]); } + + const orig = await store.query( + [{ kinds: [30383], authors: [Conf.pubkey], '#k': ['3036'], ...params }], + { signal }, + ); + + const ids = new Set( + orig + .map(({ tags }) => tags.find(([name]) => name === 'd')?.[1]) + .filter((id): id is string => !!id), + ); + + const events = await store.query([{ kinds: [3036], ids: [...ids] }]) + .then((events) => hydrateEvents({ store, events, signal })); + + const nameRequests = await Promise.all(events.map(renderNameRequest)); + return paginated(c, orig, nameRequests); } - const authors = await store.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) - .then((events) => hydrateEvents({ store, events, signal })); + if (disabled || silenced || suspended || sensitized) { + const n = []; - const accounts = await Promise.all( - [...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, - ); + if (disabled) { + n.push('disabled'); + } + if (silenced) { + n.push('silenced'); + } + if (suspended) { + n.push('suspended'); + } + if (sensitized) { + n.push('sensitized'); + } + if (staff) { + n.push('admin'); + n.push('moderator'); + } - return { - ...account, - invite_request: request?.content ?? null, - invite_request_username: request?.tags.find(([name]) => name === 'r')?.[1] ?? null, - approved: !!grant, - }; - }), - ); + const events = await store.query([{ kinds: [30382], authors: [Conf.pubkey], '#n': n, ...params }], { signal }); + const pubkeys = new Set(events.map(({ pubkey }) => pubkey)); + const authors = await store.query([{ kinds: [0], authors: [...pubkeys] }]) + .then((events) => hydrateEvents({ store, events, signal })); + const accounts = await Promise.all( + [...pubkeys].map((pubkey) => { + const author = authors.find((e) => e.pubkey === pubkey); + return author ? renderAdminAccount(author) : renderAdminAccountFromPubkey(pubkey); + }), + ); + + return paginated(c, events, accounts); + } + + const filter: NostrFilter = { kinds: [0], ...params }; + if (local) { + filter.search = `domain:${Conf.url.host}`; + } + const events = await store.query([filter], { signal }); + const accounts = await Promise.all(events.map(renderAdminAccount)); return paginated(c, events, accounts); }; @@ -104,7 +126,7 @@ const adminActionController: AppController = async (c) => { const n: Record = {}; if (data.type === 'sensitive') { - n.sensitive = true; + n.sensitized = true; } if (data.type === 'disable') { n.disabled = true; diff --git a/src/views/ditto.ts b/src/views/ditto.ts index 708c522..ebc07b7 100644 --- a/src/views/ditto.ts +++ b/src/views/ditto.ts @@ -1,25 +1,22 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts'; -import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { getTagSet } from '@/utils/tags.ts'; +import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; +/** Renders an Admin::Account entity from a name request event. */ export async function renderNameRequest(event: DittoEvent) { const n = getTagSet(event.info?.tags ?? [], 'n'); + const [username, domain] = event.tags.find(([name]) => name === 'r')?.[1]?.split('@') ?? []; - let approvalStatus = 'pending'; - - if (n.has('approved')) { - approvalStatus = 'approved'; - } - if (n.has('rejected')) { - approvalStatus = 'rejected'; - } + const adminAccount = event.author + ? await renderAdminAccount(event.author) + : await renderAdminAccountFromPubkey(event.pubkey); return { + ...adminAccount, 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(), + approved: n.has('approved'), + username, + domain, + invite_request: event.content, }; } diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts index 4dc8569..34b6860 100644 --- a/src/views/mastodon/admin-accounts.ts +++ b/src/views/mastodon/admin-accounts.ts @@ -1,9 +1,20 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; -import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { getTagSet } from '@/utils/tags.ts'; /** Expects a kind 0 fully hydrated */ async function renderAdminAccount(event: DittoEvent) { const account = await renderAccount(event); + const names = getTagSet(event.user?.tags ?? [], 'n'); + + let role = 'user'; + + if (names.has('admin')) { + role = 'admin'; + } + if (names.has('moderator')) { + role = 'moderator'; + } return { id: account.id, @@ -15,12 +26,13 @@ async function renderAdminAccount(event: DittoEvent) { ips: [], locale: '', invite_request: null, - role: event.tags.find(([name]) => name === 'role')?.[1], + role, confirmed: true, approved: true, - disabled: false, - silenced: false, - suspended: false, + disabled: names.has('disabled'), + silenced: names.has('silenced'), + suspended: names.has('suspended'), + sensitized: names.has('sensitized'), account, }; } From 2245263011a70b8f7a0ce1e5560834d1607cc6e9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 14:50:37 -0500 Subject: [PATCH 8/9] Add ditto:name_grant notification --- src/app.ts | 23 +++--- src/controllers/api/admin.ts | 61 ++++++++++++++- src/controllers/api/ditto.ts | 108 +-------------------------- src/controllers/api/notifications.ts | 80 +++++++++++++++++--- src/views/mastodon/notifications.ts | 19 +++++ 5 files changed, 164 insertions(+), 127 deletions(-) diff --git a/src/app.ts b/src/app.ts index 7bf29ca..074359d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,14 +26,16 @@ import { updateCredentialsController, verifyCredentialsController, } from '@/controllers/api/accounts.ts'; -import { adminAccountsController, adminActionController } from '@/controllers/api/admin.ts'; +import { + adminAccountsController, + adminActionController, + adminApproveController, + adminRejectController, +} 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 { - adminNameApproveController, - adminNameRejectController, - adminNameRequestsController, adminRelaysController, adminSetRelaysController, nameRequestController, @@ -244,7 +246,6 @@ app.get('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', reactions app.put('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', requireSigner, reactionController); app.delete('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', requireSigner, deleteReactionController); -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); app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAdminDeleteStatusController); @@ -255,10 +256,6 @@ app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysContro app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); -app.get('/api/v1/admin/ditto/names', requireRole('admin'), adminNameRequestsController); -app.post('/api/v1/admin/ditto/names/:id{[0-9a-f]{64}}/approve', requireRole('admin'), adminNameApproveController); -app.post('/api/v1/admin/ditto/names/:id{[0-9a-f]{64}}/reject', requireRole('admin'), adminNameRejectController); - app.post('/api/v1/ditto/zap', requireSigner, zapController); app.post('/api/v1/reports', requireSigner, reportController); @@ -277,7 +274,15 @@ app.post( adminReportReopenController, ); +app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController); app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminActionController); +app.post( + '/api/v1/admin/accounts/:id{[0-9a-f]{64}}/approve', + requireSigner, + requireRole('admin'), + adminApproveController, +); +app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/reject', requireSigner, requireRole('admin'), adminRejectController); app.put('/api/v1/pleroma/admin/users/tag', requireRole('admin'), pleromaAdminTagController); app.delete('/api/v1/pleroma/admin/users/tag', requireRole('admin'), pleromaAdminUntagController); diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index df5bf96..90afd52 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -6,7 +6,7 @@ import { Conf } from '@/config.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { paginated, paginationSchema, parseBody, updateUser } from '@/utils/api.ts'; +import { createAdminEvent, paginated, paginationSchema, parseBody, updateEventInfo, updateUser } from '@/utils/api.ts'; import { renderNameRequest } from '@/views/ditto.ts'; import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; @@ -47,7 +47,7 @@ const adminAccountsController: AppController = async (c) => { } const orig = await store.query( - [{ kinds: [30383], authors: [Conf.pubkey], '#k': ['3036'], ...params }], + [{ kinds: [30383], authors: [Conf.pubkey], '#k': ['3036'], '#n': ['pending'], ...params }], { signal }, ); @@ -143,4 +143,59 @@ const adminActionController: AppController = async (c) => { return c.json({}, 200); }; -export { adminAccountsController, adminActionController }; +const adminApproveController: AppController = async (c) => { + const eventId = c.req.param('id'); + const store = await Storages.db(); + + const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]); + if (!event) { + return c.json({ error: 'Event not found' }, 404); + } + + const r = event.tags.find(([name]) => name === 'r')?.[1]; + if (!r) { + return c.json({ error: 'NIP-05 not found' }, 404); + } + if (!z.string().email().safeParse(r).success) { + return c.json({ error: 'Invalid NIP-05' }, 400); + } + + const [existing] = await store.query([{ kinds: [30360], authors: [Conf.pubkey], '#d': [r], limit: 1 }]); + if (existing) { + return c.json({ error: 'NIP-05 already granted to another user' }, 400); + } + + await createAdminEvent({ + kind: 30360, + tags: [ + ['d', r], + ['L', 'nip05.domain'], + ['l', r.split('@')[1], 'nip05.domain'], + ['p', event.pubkey], + ['e', event.id], + ], + }, c); + + await updateEventInfo(eventId, { pending: false, approved: true, rejected: false }, c); + await hydrateEvents({ events: [event], store }); + + const nameRequest = await renderNameRequest(event); + return c.json(nameRequest); +}; + +const adminRejectController: AppController = async (c) => { + const eventId = c.req.param('id'); + const store = await Storages.db(); + + const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]); + if (!event) { + return c.json({ error: 'Event not found' }, 404); + } + + await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c); + await hydrateEvents({ events: [event], store }); + + const nameRequest = await renderNameRequest(event); + return c.json(nameRequest); +}; +export { adminAccountsController, adminActionController, adminApproveController, adminRejectController }; diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 5c8ea9c..5723a9e 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { z } from 'zod'; import { AppController } from '@/app.ts'; @@ -7,7 +7,7 @@ import { booleanParamSchema } from '@/schema.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { createAdminEvent, createEvent, paginated, paginationSchema, updateEventInfo } from '@/utils/api.ts'; +import { createEvent, paginated, paginationSchema } from '@/utils/api.ts'; import { renderNameRequest } from '@/views/ditto.ts'; const markerSchema = z.enum(['read', 'write']); @@ -135,107 +135,3 @@ export const nameRequestsController: AppController = async (c) => { return paginated(c, orig, nameRequests); }; - -const adminNameRequestsSchema = z.object({ - account_id: n.id().optional(), - approved: booleanParamSchema.optional(), - rejected: booleanParamSchema.optional(), -}); - -export const adminNameRequestsController: AppController = async (c) => { - const store = await Storages.db(); - const params = paginationSchema.parse(c.req.query()); - const { account_id, approved, rejected } = adminNameRequestsSchema.parse(c.req.query()); - - const filter: NostrFilter = { - kinds: [30383], - authors: [Conf.pubkey], - '#k': ['3036'], - ...params, - }; - - if (account_id) { - filter['#p'] = [account_id]; - } - if (approved) { - filter['#n'] = ['approved']; - } - if (rejected) { - filter['#n'] = ['rejected']; - } - - 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: [3036], ids: [...ids] }]) - .then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal })); - - const nameRequests = await Promise.all( - events.map((event) => renderNameRequest(event)), - ); - - return paginated(c, orig, nameRequests); -}; - -export const adminNameApproveController: AppController = async (c) => { - const eventId = c.req.param('id'); - const store = await Storages.db(); - - const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]); - if (!event) { - return c.json({ error: 'Event not found' }, 404); - } - - const r = event.tags.find(([name]) => name === 'r')?.[1]; - if (!r) { - return c.json({ error: 'NIP-05 not found' }, 404); - } - if (!z.string().email().safeParse(r).success) { - return c.json({ error: 'Invalid NIP-05' }, 400); - } - - const [existing] = await store.query([{ kinds: [30360], authors: [Conf.pubkey], '#d': [r], limit: 1 }]); - if (existing) { - return c.json({ error: 'NIP-05 already granted to another user' }, 400); - } - - await createAdminEvent({ - kind: 30360, - tags: [ - ['d', r], - ['L', 'nip05.domain'], - ['l', r.split('@')[1], 'nip05.domain'], - ['p', event.pubkey], - ['e', event.id], - ], - }, c); - - await updateEventInfo(eventId, { pending: false, approved: true, rejected: false }, c); - await hydrateEvents({ events: [event], store }); - - const nameRequest = await renderNameRequest(event); - return c.json(nameRequest); -}; - -export const adminNameRejectController: AppController = async (c) => { - const eventId = c.req.param('id'); - const store = await Storages.db(); - - const [event] = await store.query([{ kinds: [3036], ids: [eventId] }]); - if (!event) { - return c.json({ error: 'Event not found' }, 404); - } - - await updateEventInfo(eventId, { pending: false, approved: false, rejected: true }, c); - await hydrateEvents({ events: [event], store }); - - const nameRequest = await renderNameRequest(event); - return c.json(nameRequest); -}; diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index ba15bd0..d92ccf4 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -1,24 +1,87 @@ -import { NostrFilter } from '@nostrify/nostrify'; +import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { z } from 'zod'; import { AppContext, AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { paginated, paginationSchema } from '@/utils/api.ts'; +import { paginated, PaginationParams, paginationSchema } from '@/utils/api.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; +/** Set of known notification types across backends. */ +const notificationTypes = new Set([ + 'mention', + 'status', + 'reblog', + 'follow', + 'follow_request', + 'favourite', + 'poll', + 'update', + 'admin.sign_up', + 'admin.report', + 'severed_relationships', + 'pleroma:emoji_reaction', + 'ditto:name_grant', +]); + +const notificationsSchema = z.object({ + account_id: n.id().optional(), +}); + const notificationsController: AppController = async (c) => { const pubkey = await c.get('signer')?.getPublicKey()!; - const { since, until } = paginationSchema.parse(c.req.query()); + const params = paginationSchema.parse(c.req.query()); - return renderNotifications(c, [{ kinds: [1, 6, 7], '#p': [pubkey], since, until }]); + const types = notificationTypes + .intersection(new Set(c.req.queries('types[]') ?? notificationTypes)) + .difference(new Set(c.req.queries('exclude_types[]'))); + + const { account_id } = notificationsSchema.parse(c.req.query()); + + const kinds = new Set(); + + if (types.has('mention')) { + kinds.add(1); + } + if (types.has('reblog')) { + kinds.add(6); + } + if (types.has('favourite') || types.has('pleroma:emoji_reaction')) { + kinds.add(7); + } + + const filter: NostrFilter = { + kinds: [...kinds], + '#p': [pubkey], + ...params, + }; + + const filters: NostrFilter[] = [filter]; + + if (account_id) { + filter.authors = [account_id]; + } + + if (types.has('ditto:name_grant') && !account_id) { + filters.push({ kinds: [30360], authors: [Conf.pubkey], '#p': [pubkey], ...params }); + } + + return renderNotifications(filters, types, params, c); }; -async function renderNotifications(c: AppContext, filters: NostrFilter[]) { +async function renderNotifications( + filters: NostrFilter[], + types: Set, + params: PaginationParams, + c: AppContext, +) { const store = c.get('store'); const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; + const opts = { signal, limit: params.limit }; const events = await store - .query(filters, { signal }) + .query(filters, opts) .then((events) => events.filter((event) => event.pubkey !== pubkey)) .then((events) => hydrateEvents({ events, store, signal })); @@ -26,9 +89,8 @@ async function renderNotifications(c: AppContext, filters: NostrFilter[]) { return c.json([]); } - const notifications = (await Promise - .all(events.map((event) => renderNotification(event, { viewerPubkey: pubkey })))) - .filter(Boolean); + const notifications = (await Promise.all(events.map((event) => renderNotification(event, { viewerPubkey: pubkey })))) + .filter((notification) => notification && types.has(notification.type)); if (!notifications.length) { return c.json([]); diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts index 5b618d7..e11d45a 100644 --- a/src/views/mastodon/notifications.ts +++ b/src/views/mastodon/notifications.ts @@ -26,6 +26,10 @@ function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) { if (event.kind === 7) { return renderReaction(event, opts); } + + if (event.kind === 30360) { + return renderNameGrant(event); + } } async function renderMention(event: DittoEvent, opts: RenderNotificationOpts) { @@ -87,6 +91,21 @@ async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) { }; } +async function renderNameGrant(event: DittoEvent) { + const d = event.tags.find(([name]) => name === 'd')?.[1]; + const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey); + + if (!d) return; + + return { + id: notificationId(event), + type: 'ditto:name_grant', + name: d, + created_at: nostrDate(event.created_at).toISOString(), + account, + }; +} + /** This helps notifications be sorted in the correct order. */ function notificationId({ id, created_at }: NostrEvent): string { return `${created_at}-${id}`; From 42fac52e9ef80f423852ce92163514eae6046b2b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 15:31:14 -0500 Subject: [PATCH 9/9] Support streaming notifications --- src/controllers/api/streaming.ts | 66 ++++++++++++++++------------- src/views/mastodon/notifications.ts | 12 +++--- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 427c350..552ea3b 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -1,4 +1,4 @@ -import { NostrFilter } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { z } from 'zod'; @@ -11,6 +11,7 @@ import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; +import { renderNotification } from '@/views/mastodon/notifications.ts'; const debug = Debug('ditto:streaming'); @@ -52,6 +53,11 @@ const streamingController: AppController = async (c) => { const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 }); + const store = await Storages.db(); + const pubsub = await Storages.pubsub(); + + const policy = pubkey ? new MuteListPolicy(pubkey, await Storages.admin()) : undefined; + function send(name: string, payload: object) { if (socket.readyState === WebSocket.OPEN) { debug('send', name, JSON.stringify(payload)); @@ -63,52 +69,54 @@ const streamingController: AppController = async (c) => { } } - socket.onopen = async () => { - if (!stream) return; - - const filter = await topicToFilter(stream, c.req.query(), pubkey); - if (!filter) return; - + async function sub(type: string, filters: NostrFilter[], render: (event: NostrEvent) => Promise) { try { - const db = await Storages.db(); - const pubsub = await Storages.pubsub(); - - for await (const msg of pubsub.req([filter], { signal: controller.signal })) { + for await (const msg of pubsub.req(filters, { signal: controller.signal })) { if (msg[0] === 'EVENT') { const event = msg[2]; - if (pubkey) { - const policy = new MuteListPolicy(pubkey, await Storages.admin()); + if (policy) { const [, , ok] = await policy.call(event); if (!ok) { continue; } } - await hydrateEvents({ - events: [event], - store: db, - signal: AbortSignal.timeout(1000), - }); + await hydrateEvents({ events: [event], store, signal: AbortSignal.timeout(1000) }); - if (event.kind === 1) { - const status = await renderStatus(event, { viewerPubkey: pubkey }); - if (status) { - send('update', status); - } - } + const result = await render(event); - if (event.kind === 6) { - const status = await renderReblog(event, { viewerPubkey: pubkey }); - if (status) { - send('update', status); - } + if (result) { + send(type, result); } } } } catch (e) { debug('streaming error:', e); } + } + + socket.onopen = async () => { + if (!stream) return; + const topicFilter = await topicToFilter(stream, c.req.query(), pubkey); + + if (topicFilter) { + sub('update', [topicFilter], async (event) => { + if (event.kind === 1) { + return await renderStatus(event, { viewerPubkey: pubkey }); + } + if (event.kind === 6) { + return await renderReblog(event, { viewerPubkey: pubkey }); + } + }); + } + + if (['user', 'user:notification'].includes(stream) && pubkey) { + sub('notification', [{ '#p': [pubkey] }], async (event) => { + return await renderNotification(event, { viewerPubkey: pubkey }); + }); + return; + } }; socket.onclose = () => { diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts index e11d45a..8f2a8a6 100644 --- a/src/views/mastodon/notifications.ts +++ b/src/views/mastodon/notifications.ts @@ -1,8 +1,10 @@ +import { NostrEvent } from '@nostrify/nostrify'; + +import { Conf } from '@/config.ts'; 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; @@ -27,7 +29,7 @@ function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) { return renderReaction(event, opts); } - if (event.kind === 30360) { + if (event.kind === 30360 && event.pubkey === Conf.pubkey) { return renderNameGrant(event); } } @@ -49,7 +51,7 @@ 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); + const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey); return { id: notificationId(event), @@ -64,7 +66,7 @@ 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); + const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey); return { id: notificationId(event), @@ -79,7 +81,7 @@ 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); + const account = event.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey); return { id: notificationId(event),