From 5bd03bdcaa2c3ef821cf2e6132e766a432bb24fe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jan 2024 15:35:55 -0600 Subject: [PATCH 1/5] Support admin accounts endpoint (first pass) --- src/app.ts | 2 ++ src/controllers/api/admin.ts | 23 +++++++++++++++++++++ src/storages/types.ts | 2 ++ src/views/mastodon/accounts.ts | 4 ++-- src/views/mastodon/admin-accounts.ts | 30 ++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/admin.ts create mode 100644 src/views/mastodon/admin-accounts.ts diff --git a/src/app.ts b/src/app.ts index 0d38e34..bc1b372 100644 --- a/src/app.ts +++ b/src/app.ts @@ -35,6 +35,7 @@ import { updateCredentialsController, verifyCredentialsController, } from './controllers/api/accounts.ts'; +import { adminAccountsController } 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'; @@ -185,6 +186,7 @@ app.get('/api/v1/favourites', requirePubkey, favouritesController); app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); app.get('/api/v1/blocks', requirePubkey, blocksController); +app.get('/api/v1/admin/accounts', adminAccountsController); app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); // Not (yet) implemented. diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts new file mode 100644 index 0000000..46f7b29 --- /dev/null +++ b/src/controllers/api/admin.ts @@ -0,0 +1,23 @@ +import { type AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; +import { eventsDB } from '@/storages.ts'; +import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; + +const adminAccountsController: AppController = async (c) => { + const events = await eventsDB.getEvents([{ kinds: [30361], authors: [Conf.pubkey], limit: 20 }]); + const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); + const authors = await eventsDB.getEvents([{ kinds: [0], ids: pubkeys, limit: pubkeys.length }]); + + for (const event of events) { + const d = event.tags.find(([name]) => name === 'd')?.[1]; + event.d_author = authors.find((author) => author.pubkey === d); + } + + return c.json( + await Promise.all( + events.map((event) => renderAdminAccount(event)), + ), + ); +}; + +export { adminAccountsController }; diff --git a/src/storages/types.ts b/src/storages/types.ts index c89e9cc..dda2205 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -29,6 +29,8 @@ interface DittoEvent extends Event { author?: DittoEvent<0>; author_stats?: AuthorStats; event_stats?: EventStats; + d_author?: DittoEvent<0>; + user?: DittoEvent<30361>; } /** Storage interface for Nostr events. */ diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 620a1b5..0030f3b 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -12,7 +12,7 @@ interface ToAccountOpts { } async function renderAccount( - event: Omit, 'id' | 'sig'>, + event: Omit, 'id' | 'sig'>, opts: ToAccountOpts = {}, ) { const { withSource = false } = opts; @@ -39,7 +39,7 @@ async function renderAccount( avatar: picture, avatar_static: picture, bot: false, - created_at: nostrDate(event.created_at).toISOString(), + created_at: user ? user.inserted_at.toISOString() : nostrDate(event.created_at).toISOString(), discoverable: true, display_name: name, emojis: renderEmojis(event), diff --git a/src/views/mastodon/admin-accounts.ts b/src/views/mastodon/admin-accounts.ts new file mode 100644 index 0000000..0a73a99 --- /dev/null +++ b/src/views/mastodon/admin-accounts.ts @@ -0,0 +1,30 @@ +import { DittoEvent } from '@/storages/types.ts'; +import { nostrDate } from '@/utils.ts'; + +import { accountFromPubkey, renderAccount } from './accounts.ts'; + +async function renderAdminAccount(event: DittoEvent<30361>) { + const d = event.tags.find(([name]) => name === 'd')?.[1]!; + const account = event.d_author ? await renderAccount({ ...event.d_author, user: event }) : await accountFromPubkey(d); + + return { + id: account.id, + username: event.tags.find(([name]) => name === 'name')?.[1]!, + domain: account.acct.split('@')[1] || null, + created_at: nostrDate(event.created_at).toISOString(), + email: '', + ip: null, + ips: [], + locale: '', + invite_request: null, + role: event.tags.find(([name]) => name === 'role')?.[1] || 'user', + confirmed: true, + approved: true, + disabled: false, + silenced: false, + suspended: false, + account, + }; +} + +export { renderAdminAccount }; From 46b7185f7b5a1d5b4f73a7d28c903ba081dde81f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jan 2024 16:05:18 -0600 Subject: [PATCH 2/5] adminAccountsController: bail if querying by unsupported params --- src/controllers/api/admin.ts | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 46f7b29..4baaa42 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -1,10 +1,47 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { z } from '@/deps.ts'; +import { booleanParamSchema } from '@/schema.ts'; import { eventsDB } from '@/storages.ts'; import { renderAdminAccount } from '@/views/mastodon/admin-accounts.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(), + max_id: z.string().optional(), + since_id: z.string().optional(), + min_id: z.string().optional(), + limit: z.number().min(1).max(80).optional(), +}); + const adminAccountsController: AppController = async (c) => { - const events = await eventsDB.getEvents([{ kinds: [30361], authors: [Conf.pubkey], limit: 20 }]); + const { + pending, + disabled, + silenced, + suspended, + sensitized, + limit, + } = adminAccountQuerySchema.parse(c.req.query()); + + // Not supported. + if (pending || disabled || silenced || suspended || sensitized) { + return c.json([]); + } + + const events = await eventsDB.getEvents([{ kinds: [30361], authors: [Conf.pubkey], limit }]); const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); const authors = await eventsDB.getEvents([{ kinds: [0], ids: pubkeys, limit: pubkeys.length }]); From e1a95b231927d42b8caee1abe9e7125db0fd9766 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jan 2024 16:33:05 -0600 Subject: [PATCH 3/5] adminAccountsController: fix hydrating accounts --- src/controllers/api/admin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 4baaa42..d8845fd 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -43,7 +43,7 @@ const adminAccountsController: AppController = async (c) => { const events = await eventsDB.getEvents([{ kinds: [30361], authors: [Conf.pubkey], limit }]); const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); - const authors = await eventsDB.getEvents([{ kinds: [0], ids: pubkeys, limit: pubkeys.length }]); + const authors = await eventsDB.getEvents([{ kinds: [0], authors: pubkeys }]); for (const event of events) { const d = event.tags.find(([name]) => name === 'd')?.[1]; From 957b224453700deaa490671711a7633deb70f201 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jan 2024 17:11:32 -0600 Subject: [PATCH 4/5] adminAccountsController: support pagination, probably --- src/controllers/api/admin.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index d8845fd..b90c051 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -4,6 +4,7 @@ import { z } from '@/deps.ts'; import { booleanParamSchema } from '@/schema.ts'; import { eventsDB } from '@/storages.ts'; import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; +import { paginated, paginationSchema } from '@/utils/api.ts'; const adminAccountQuerySchema = z.object({ local: booleanParamSchema.optional(), @@ -20,10 +21,6 @@ const adminAccountQuerySchema = z.object({ email: z.string().optional(), ip: z.string().optional(), staff: booleanParamSchema.optional(), - max_id: z.string().optional(), - since_id: z.string().optional(), - min_id: z.string().optional(), - limit: z.number().min(1).max(80).optional(), }); const adminAccountsController: AppController = async (c) => { @@ -33,7 +30,6 @@ const adminAccountsController: AppController = async (c) => { silenced, suspended, sensitized, - limit, } = adminAccountQuerySchema.parse(c.req.query()); // Not supported. @@ -41,7 +37,9 @@ const adminAccountsController: AppController = async (c) => { return c.json([]); } - const events = await eventsDB.getEvents([{ kinds: [30361], authors: [Conf.pubkey], limit }]); + const { since, until, limit } = paginationSchema.parse(c.req.query()); + + const events = await eventsDB.getEvents([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }]); const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); const authors = await eventsDB.getEvents([{ kinds: [0], authors: pubkeys }]); @@ -50,11 +48,11 @@ const adminAccountsController: AppController = async (c) => { event.d_author = authors.find((author) => author.pubkey === d); } - return c.json( - await Promise.all( - events.map((event) => renderAdminAccount(event)), - ), + const accounts = await Promise.all( + events.map((event) => renderAdminAccount(event)), ); + + return paginated(c, events, accounts); }; export { adminAccountsController }; From 5d91c4b50752a84963c5ce19af8439d8f6aa9303 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jan 2024 18:04:24 -0600 Subject: [PATCH 5/5] EventsDB: normalizeFilters --- src/storages/events-db.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/storages/events-db.ts b/src/storages/events-db.ts index 143f4f6..0bff2bd 100644 --- a/src/storages/events-db.ts +++ b/src/storages/events-db.ts @@ -1,7 +1,7 @@ import { Conf } from '@/config.ts'; import { type DittoDB } from '@/db.ts'; import { Debug, type Event, Kysely, type SelectQueryBuilder } from '@/deps.ts'; -import { type DittoFilter } from '@/filter.ts'; +import { type DittoFilter, normalizeFilters } from '@/filter.ts'; import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { isNostrId, isURL } from '@/utils.ts'; @@ -264,12 +264,12 @@ class EventsDB implements EventStore { } /** Get events for filters from the database. */ - async getEvents( - filters: DittoFilter[], - opts: GetEventsOpts = {}, - ): Promise[]> { + async getEvents(filters: DittoFilter[], opts: GetEventsOpts = {}): Promise[]> { + filters = normalizeFilters(filters); // Improves performance of `{ kinds: [0], authors: ['...'] }` queries. + if (opts.signal?.aborted) return Promise.resolve([]); if (!filters.length) return Promise.resolve([]); + this.#debug('REQ', JSON.stringify(filters)); let query = this.getEventsQuery(filters);