Refactor reports more, add reopen endpoint

This commit is contained in:
Alex Gleason 2024-06-09 11:03:46 -05:00
parent 177f25ad96
commit 8a7cae9841
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
5 changed files with 153 additions and 37 deletions

View File

@ -53,6 +53,7 @@ import { deleteReactionController, reactionController, reactionsController } fro
import { relayController } from '@/controllers/nostr/relay.ts'; import { relayController } from '@/controllers/nostr/relay.ts';
import { import {
adminReportController, adminReportController,
adminReportReopenController,
adminReportResolveController, adminReportResolveController,
adminReportsController, adminReportsController,
reportController, reportController,
@ -255,6 +256,12 @@ app.post(
requireRole('admin'), requireRole('admin'),
adminReportResolveController, 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); app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminActionController);

View File

@ -1,12 +1,13 @@
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
import { z } from 'zod'; import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { booleanParamSchema } from '@/schema.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { hydrateEvents } from '@/storages/hydrate.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'; import { renderNameRequest } from '@/views/ditto.ts';
const markerSchema = z.enum(['read', 'write']); const markerSchema = z.enum(['read', 'write']);
@ -87,14 +88,98 @@ export const nameRequestController: AppController = async (c) => {
return c.json(nameRequest); return c.json(nameRequest);
}; };
const nameRequestsSchema = z.object({
approved: booleanParamSchema.optional(),
rejected: booleanParamSchema.optional(),
});
export const nameRequestsController: AppController = async (c) => { export const nameRequestsController: AppController = async (c) => {
const store = await Storages.db(); const store = await Storages.db();
const signer = c.get('signer')!; const signer = c.get('signer')!;
const pubkey = await signer.getPublicKey(); const pubkey = await signer.getPublicKey();
const events = await store.query([{ kinds: [3036], authors: [pubkey], limit: 20 }]) const params = paginationSchema.parse(c.req.query());
.then((events) => hydrateEvents({ events, store })); const { approved, rejected } = nameRequestsSchema.parse(c.req.query());
const nameRequests = await Promise.all(events.map(renderNameRequest)); const filter: NostrFilter = {
return c.json(nameRequests); 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<string>();
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<string>();
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);
}; };

View File

@ -7,7 +7,6 @@ import { createEvent, paginationSchema, parseBody, updateEventInfo } from '@/uti
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { renderAdminReport } from '@/views/mastodon/reports.ts'; import { renderAdminReport } from '@/views/mastodon/reports.ts';
import { renderReport } from '@/views/mastodon/reports.ts'; import { renderReport } from '@/views/mastodon/reports.ts';
import { getTagSet } from '@/utils/tags.ts';
import { booleanParamSchema } from '@/schema.ts'; import { booleanParamSchema } from '@/schema.ts';
const reportSchema = z.object({ const reportSchema = z.object({
@ -71,6 +70,7 @@ const adminReportsController: AppController = async (c) => {
const filter: NostrFilter = { const filter: NostrFilter = {
kinds: [30383], kinds: [30383],
authors: [Conf.pubkey], authors: [Conf.pubkey],
'#k': ['1984'],
...params, ...params,
}; };
@ -98,15 +98,7 @@ const adminReportsController: AppController = async (c) => {
.then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal })); .then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal }));
const reports = await Promise.all( const reports = await Promise.all(
events.map((event) => { events.map((event) => renderAdminReport(event, { viewerPubkey })),
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); return c.json(reports);
@ -153,11 +145,39 @@ const adminReportResolveController: AppController = async (c) => {
} }
await updateEventInfo(eventId, { open: false, closed: true }, c); await updateEventInfo(eventId, { open: false, closed: true }, c);
await hydrateEvents({ events: [event], store, signal }); 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); 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,
};

View File

@ -206,6 +206,10 @@ function gatherAuthors({ events, store, signal }: HydrateOpts): Promise<DittoEve
function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> { function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
const pubkeys = new Set(events.map((event) => event.pubkey)); const pubkeys = new Set(events.map((event) => event.pubkey));
if (!pubkeys.size) {
return Promise.resolve([]);
}
return store.query( return store.query(
[{ kinds: [30382], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }], [{ kinds: [30382], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }],
{ signal }, { signal },
@ -217,7 +221,7 @@ function gatherInfo({ events, store, signal }: HydrateOpts): Promise<DittoEvent[
const ids = new Set<string>(); const ids = new Set<string>();
for (const event of events) { for (const event of events) {
if (event.kind === 3036) { if (event.kind === 1984 || event.kind === 3036) {
ids.add(event.id); ids.add(event.id);
} }
} }
@ -227,7 +231,7 @@ function gatherInfo({ events, store, signal }: HydrateOpts): Promise<DittoEvent[
} }
return store.query( return store.query(
[{ ids: [...ids], limit: ids.size }], [{ kinds: [30383], authors: [Conf.pubkey], '#d': [...ids], limit: ids.size }],
{ signal }, { signal },
); );
} }

View File

@ -3,6 +3,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { nostrDate } from '@/utils.ts'; import { nostrDate } from '@/utils.ts';
import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts'; import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import { getTagSet } from '@/utils/tags.ts';
/** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */ /** Expects a `reportEvent` of kind 1984 and a `profile` of kind 0 of the person being reported */
async function renderReport(event: DittoEvent) { async function renderReport(event: DittoEvent) {
@ -30,43 +31,42 @@ async function renderReport(event: DittoEvent) {
interface RenderAdminReportOpts { interface RenderAdminReportOpts {
viewerPubkey?: string; viewerPubkey?: string;
actionTaken?: boolean;
} }
/** Admin-level information about a filed report. /** Admin-level information about a filed report.
* Expects an event of kind 1984 fully hydrated. * Expects an event of kind 1984 fully hydrated.
* https://docs.joinmastodon.org/entities/Admin_Report */ * https://docs.joinmastodon.org/entities/Admin_Report */
async function renderAdminReport(reportEvent: DittoEvent, opts: RenderAdminReportOpts) { async function renderAdminReport(event: DittoEvent, opts: RenderAdminReportOpts) {
const { viewerPubkey, actionTaken = false } = opts; 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 // 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 category = event.tags.find(([name]) => name === 'p')?.[2];
const statuses = []; const statuses = [];
if (reportEvent.reported_notes) { if (event.reported_notes) {
for (const status of reportEvent.reported_notes) { for (const status of event.reported_notes) {
statuses.push(await renderStatus(status, { viewerPubkey })); 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) { if (!reportedPubkey) {
return; return;
} }
const names = getTagSet(event.info?.tags ?? [], 'n');
return { return {
id: reportEvent.id, id: event.id,
action_taken: actionTaken, action_taken: names.has('closed'),
action_taken_at: null, action_taken_at: null,
category, category,
comment: reportEvent.content, comment: event.content,
forwarded: false, forwarded: false,
created_at: nostrDate(reportEvent.created_at).toISOString(), created_at: nostrDate(event.created_at).toISOString(),
account: reportEvent.author account: event.author ? await renderAdminAccount(event.author) : await renderAdminAccountFromPubkey(event.pubkey),
? await renderAdminAccount(reportEvent.author) target_account: event.reported_profile
: await renderAdminAccountFromPubkey(reportEvent.pubkey), ? await renderAdminAccount(event.reported_profile)
target_account: reportEvent.reported_profile
? await renderAdminAccount(reportEvent.reported_profile)
: await renderAdminAccountFromPubkey(reportedPubkey), : await renderAdminAccountFromPubkey(reportedPubkey),
assigned_account: null, assigned_account: null,
action_taken_by_account: null, action_taken_by_account: null,