diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index 44b0b9b4..a6904f1e 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -55,8 +55,6 @@ import { adminSetRelaysController, deleteZapSplitsController, getZapSplitsController, - nameRequestController, - nameRequestsController, statusZapSplitsController, updateInstanceController, updateZapSplitsController, @@ -149,6 +147,7 @@ import { rateLimitMiddleware } from '@/middleware/rateLimitMiddleware.ts'; import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts'; import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts'; import { logiMiddleware } from '@/middleware/logiMiddleware.ts'; +import dittoNamesRoute from '@/routes/dittoNamesRoute.ts'; import { DittoRelayStore } from '@/storages/DittoRelayStore.ts'; export interface AppEnv extends DittoEnv { @@ -446,8 +445,7 @@ app.put('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminSe app.put('/api/v1/admin/ditto/instance', userMiddleware({ role: 'admin' }), updateInstanceController); -app.post('/api/v1/ditto/names', userMiddleware(), nameRequestController); -app.get('/api/v1/ditto/names', userMiddleware(), nameRequestsController); +app.route('/api/v1/ditto/names', dittoNamesRoute); app.get('/api/v1/ditto/captcha', rateLimitMiddleware(3, Time.minutes(1)), captchaController); app.post( diff --git a/packages/ditto/controllers/api/ditto.ts b/packages/ditto/controllers/api/ditto.ts index 7493085a..fb87c1b7 100644 --- a/packages/ditto/controllers/api/ditto.ts +++ b/packages/ditto/controllers/api/ditto.ts @@ -1,19 +1,17 @@ -import { paginated } from '@ditto/mastoapi/pagination'; -import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { AppController } from '@/app.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getAuthor } from '@/queries.ts'; import { addTag } from '@/utils/tags.ts'; -import { createEvent, parseBody, updateAdminEvent } from '@/utils/api.ts'; +import { parseBody, updateAdminEvent } from '@/utils/api.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; import { deleteTag } from '@/utils/tags.ts'; import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts'; import { screenshotsSchema } from '@/schemas/nostr.ts'; -import { booleanParamSchema, percentageSchema } from '@/schema.ts'; +import { percentageSchema } from '@/schema.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; -import { renderNameRequest } from '@/views/ditto.ts'; import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; import { updateListAdminEvent } from '@/utils/api.ts'; @@ -81,102 +79,6 @@ function renderRelays(event: NostrEvent): RelayEntity[] { }, [] as RelayEntity[]); } -const nameRequestSchema = z.object({ - name: z.string().email(), - reason: z.string().max(500).optional(), -}); - -export const nameRequestController: AppController = async (c) => { - const { conf, relay, user } = c.var; - - const pubkey = await user!.signer.getPublicKey(); - const result = nameRequestSchema.safeParse(await c.req.json()); - - if (!result.success) { - return c.json({ error: 'Invalid username', schema: result.error }, 400); - } - - const { name, reason } = result.data; - - const [existing] = await relay.query([{ kinds: [3036], authors: [pubkey], '#r': [name.toLowerCase()], limit: 1 }]); - if (existing) { - return c.json({ error: 'Name request already exists' }, 400); - } - - const r: string[][] = [['r', name]]; - - if (name !== name.toLowerCase()) { - r.push(['r', name.toLowerCase()]); - } - - const event = await createEvent({ - kind: 3036, - content: reason, - tags: [ - ...r, - ['L', 'nip05.domain'], - ['l', name.split('@')[1], 'nip05.domain'], - ['p', await conf.signer.getPublicKey()], - ], - }, c); - - await hydrateEvents({ ...c.var, events: [event] }); - - const nameRequest = await renderNameRequest(event); - return c.json(nameRequest); -}; - -const nameRequestsSchema = z.object({ - approved: booleanParamSchema.optional(), - rejected: booleanParamSchema.optional(), -}); - -export const nameRequestsController: AppController = async (c) => { - const { conf, relay, user } = c.var; - const pubkey = await user!.signer.getPublicKey(); - - const params = c.get('pagination'); - const { approved, rejected } = nameRequestsSchema.parse(c.req.query()); - - const filter: NostrFilter = { - kinds: [30383], - authors: [await conf.signer.getPublicKey()], - '#k': ['3036'], - '#p': [pubkey], - ...params, - }; - - if (approved) { - filter['#n'] = ['approved']; - } - if (rejected) { - filter['#n'] = ['rejected']; - } - - const orig = await relay.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); - } - } - - if (!ids.size) { - return c.json([]); - } - - const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }]) - .then((events) => hydrateEvents({ ...c.var, events })); - - const nameRequests = await Promise.all( - events.map((event) => renderNameRequest(event)), - ); - - return paginated(c, orig, nameRequests); -}; - const zapSplitSchema = z.record( n.id(), z.object({ diff --git a/packages/ditto/routes/dittoNamesRoute.ts b/packages/ditto/routes/dittoNamesRoute.ts new file mode 100644 index 00000000..2baa3adf --- /dev/null +++ b/packages/ditto/routes/dittoNamesRoute.ts @@ -0,0 +1,108 @@ +import { paginationMiddleware, userMiddleware } from '@ditto/mastoapi/middleware'; +import { DittoRoute } from '@ditto/mastoapi/router'; +import { z } from 'zod'; + +import { createEvent } from '@/utils/api.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; +import { renderNameRequest } from '@/views/ditto.ts'; +import { booleanParamSchema } from '@/schema.ts'; +import { NostrFilter } from '@nostrify/nostrify'; + +const nameRequestSchema = z.object({ + name: z.string().email(), + reason: z.string().max(500).optional(), +}); + +const route = new DittoRoute(); + +route.post('/', userMiddleware(), async (c) => { + const { conf, relay, user } = c.var; + + const pubkey = await user!.signer.getPublicKey(); + const result = nameRequestSchema.safeParse(await c.req.json()); + + if (!result.success) { + return c.json({ error: 'Invalid username', schema: result.error }, 400); + } + + const { name, reason } = result.data; + + const [existing] = await relay.query([{ kinds: [3036], authors: [pubkey], '#r': [name.toLowerCase()], limit: 1 }]); + if (existing) { + return c.json({ error: 'Name request already exists' }, 400); + } + + const r: string[][] = [['r', name]]; + + if (name !== name.toLowerCase()) { + r.push(['r', name.toLowerCase()]); + } + + const event = await createEvent({ + kind: 3036, + content: reason, + tags: [ + ...r, + ['L', 'nip05.domain'], + ['l', name.split('@')[1], 'nip05.domain'], + ['p', await conf.signer.getPublicKey()], + ], + }, c); + + await hydrateEvents({ ...c.var, events: [event] }); + + const nameRequest = await renderNameRequest(event); + return c.json(nameRequest); +}); + +const nameRequestsSchema = z.object({ + approved: booleanParamSchema.optional(), + rejected: booleanParamSchema.optional(), +}); + +route.get('/', paginationMiddleware(), userMiddleware(), async (c) => { + const { conf, relay, user, pagination } = c.var; + const pubkey = await user!.signer.getPublicKey(); + + const { approved, rejected } = nameRequestsSchema.parse(c.req.query()); + + const filter: NostrFilter = { + kinds: [30383], + authors: [await conf.signer.getPublicKey()], + '#k': ['3036'], + '#p': [pubkey], + ...pagination, + }; + + if (approved) { + filter['#n'] = ['approved']; + } + if (rejected) { + filter['#n'] = ['rejected']; + } + + const orig = await relay.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); + } + } + + if (!ids.size) { + return c.json([]); + } + + const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }]) + .then((events) => hydrateEvents({ ...c.var, events })); + + const nameRequests = await Promise.all( + events.map((event) => renderNameRequest(event)), + ); + + return c.var.paginate(orig, nameRequests); +}); + +export default route;