mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
236 lines
6.9 KiB
TypeScript
236 lines
6.9 KiB
TypeScript
import { paginated } from '@ditto/mastoapi/pagination';
|
|
import { NostrFilter } from '@nostrify/nostrify';
|
|
import { logi } from '@soapbox/logi';
|
|
import { z } from 'zod';
|
|
|
|
import { type AppController } from '@/app.ts';
|
|
import { booleanParamSchema } from '@/schema.ts';
|
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
|
import { createAdminEvent, parseBody, updateEventInfo, updateUser } from '@/utils/api.ts';
|
|
import { renderNameRequest } from '@/views/ditto.ts';
|
|
import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts';
|
|
import { errorJson } from '@/utils/log.ts';
|
|
|
|
const adminAccountQuerySchema = z.object({
|
|
local: booleanParamSchema.optional(),
|
|
remote: booleanParamSchema.optional(),
|
|
active: booleanParamSchema.optional(),
|
|
pending: booleanParamSchema.optional(),
|
|
disabled: booleanParamSchema.optional(),
|
|
silenced: booleanParamSchema.optional(),
|
|
suspended: booleanParamSchema.optional(),
|
|
sensitized: booleanParamSchema.optional(),
|
|
username: z.string().optional(),
|
|
display_name: z.string().optional(),
|
|
by_domain: z.string().optional(),
|
|
email: z.string().optional(),
|
|
ip: z.string().optional(),
|
|
staff: booleanParamSchema.optional(),
|
|
});
|
|
|
|
const adminAccountsController: AppController = async (c) => {
|
|
const { conf, relay, signal, pagination } = c.var;
|
|
|
|
const {
|
|
local,
|
|
pending,
|
|
disabled,
|
|
silenced,
|
|
suspended,
|
|
sensitized,
|
|
staff,
|
|
} = adminAccountQuerySchema.parse(c.req.query());
|
|
|
|
const adminPubkey = await conf.signer.getPublicKey();
|
|
|
|
if (pending) {
|
|
if (disabled || silenced || suspended || sensitized) {
|
|
return c.json([]);
|
|
}
|
|
|
|
const orig = await relay.query(
|
|
[{ kinds: [30383], authors: [adminPubkey], '#k': ['3036'], '#n': ['pending'], ...pagination }],
|
|
{ signal },
|
|
);
|
|
|
|
const ids = new Set<string>(
|
|
orig
|
|
.map(({ tags }) => tags.find(([name]) => name === 'd')?.[1])
|
|
.filter((id): id is string => !!id),
|
|
);
|
|
|
|
const events = await relay.query([{ kinds: [3036], ids: [...ids] }])
|
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
|
|
|
const nameRequests = await Promise.all(events.map(renderNameRequest));
|
|
return paginated(c, orig, nameRequests);
|
|
}
|
|
|
|
if (disabled || silenced || suspended || sensitized) {
|
|
const n = [];
|
|
|
|
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');
|
|
}
|
|
|
|
const events = await relay.query(
|
|
[{ kinds: [30382], authors: [adminPubkey], '#n': n, ...pagination }],
|
|
{ signal },
|
|
);
|
|
|
|
const pubkeys = new Set<string>(
|
|
events
|
|
.map(({ tags }) => tags.find(([name]) => name === 'd')?.[1])
|
|
.filter((pubkey): pubkey is string => !!pubkey),
|
|
);
|
|
|
|
const authors = await relay.query([{ kinds: [0], authors: [...pubkeys] }])
|
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
|
|
|
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], ...pagination };
|
|
|
|
if (local) {
|
|
filter.search = `domain:${conf.url.host}`;
|
|
}
|
|
|
|
const events = await relay.query([filter], { signal })
|
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
|
|
|
const accounts = await Promise.all(events.map(renderAdminAccount));
|
|
return paginated(c, events, accounts);
|
|
};
|
|
|
|
const adminAccountActionSchema = z.object({
|
|
type: z.enum(['none', 'sensitive', 'disable', 'silence', 'suspend', 'revoke_name']),
|
|
});
|
|
|
|
const adminActionController: AppController = async (c) => {
|
|
const { conf, relay, requestId } = c.var;
|
|
|
|
const body = await parseBody(c.req.raw);
|
|
const result = adminAccountActionSchema.safeParse(body);
|
|
const authorId = c.req.param('id');
|
|
|
|
if (!result.success) {
|
|
return c.json({ error: 'This action is not allowed' }, 403);
|
|
}
|
|
|
|
const { data } = result;
|
|
|
|
const n: Record<string, boolean> = {};
|
|
|
|
if (data.type === 'sensitive') {
|
|
n.sensitized = true;
|
|
}
|
|
if (data.type === 'disable') {
|
|
n.disabled = true;
|
|
}
|
|
if (data.type === 'silence') {
|
|
n.silenced = true;
|
|
}
|
|
if (data.type === 'suspend') {
|
|
n.disabled = true;
|
|
n.suspended = true;
|
|
relay.remove!([{ authors: [authorId] }]).catch((e: unknown) => {
|
|
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, requestId, error: errorJson(e) });
|
|
});
|
|
}
|
|
if (data.type === 'revoke_name') {
|
|
n.revoke_name = true;
|
|
try {
|
|
await relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]);
|
|
} catch (e) {
|
|
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, requestId, error: errorJson(e) });
|
|
return c.json({ error: 'Unexpected runtime error' }, 500);
|
|
}
|
|
}
|
|
|
|
await updateUser(authorId, n, c);
|
|
|
|
return c.json({}, 200);
|
|
};
|
|
|
|
const adminApproveController: AppController = async (c) => {
|
|
const { conf } = c.var;
|
|
const eventId = c.req.param('id');
|
|
const { relay } = c.var;
|
|
|
|
const [event] = await relay.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 relay.query([
|
|
{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#d': [r.toLowerCase()], limit: 1 },
|
|
]);
|
|
|
|
if (existing) {
|
|
return c.json({ error: 'NIP-05 already granted to another user' }, 400);
|
|
}
|
|
|
|
await createAdminEvent({
|
|
kind: 30360,
|
|
tags: [
|
|
['d', r.toLowerCase()],
|
|
['r', 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({ ...c.var, events: [event] });
|
|
|
|
const nameRequest = await renderNameRequest(event);
|
|
return c.json(nameRequest);
|
|
};
|
|
|
|
const adminRejectController: AppController = async (c) => {
|
|
const eventId = c.req.param('id');
|
|
const { relay } = c.var;
|
|
|
|
const [event] = await relay.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({ ...c.var, events: [event] });
|
|
|
|
const nameRequest = await renderNameRequest(event);
|
|
return c.json(nameRequest);
|
|
};
|
|
export { adminAccountsController, adminActionController, adminApproveController, adminRejectController };
|