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,