From 07a380fb75f24cc95ea77a834b65e36c0fc132b2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Jun 2024 13:43:40 -0500 Subject: [PATCH] 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, }; }