import { Conf } from '@/config.ts'; import { type Context, type Event, EventTemplate, HTTPException, parseFormData, z } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { signAdminEvent, signEvent } from '@/sign.ts'; import { nostrNow } from '@/utils.ts'; import type { AppContext } from '@/app.ts'; /** EventTemplate with or without a timestamp. If no timestamp is given, it will be generated. */ interface EventStub extends Omit, 'created_at'> { created_at?: number; } /** Publish an event through the pipeline. */ async function createEvent(t: EventStub, c: AppContext): Promise> { const pubkey = c.get('pubkey'); if (!pubkey) { throw new HTTPException(401); } const event = await signEvent({ created_at: nostrNow(), ...t, }, c); return publishEvent(event, c); } /** Publish an admin event through the pipeline. */ async function createAdminEvent(t: EventStub, c: AppContext): Promise> { const event = await signAdminEvent({ created_at: nostrNow(), ...t, }); return publishEvent(event, c); } /** Push the event through the pipeline, rethrowing any RelayError. */ async function publishEvent(event: Event, c: AppContext): Promise> { try { await pipeline.handleEvent(event); } catch (e) { if (e instanceof pipeline.RelayError) { throw new HTTPException(422, { res: c.json({ error: e.message }, 422), }); } } return event; } /** Parse request body to JSON, depending on the content-type of the request. */ async function parseBody(req: Request): Promise { switch (req.headers.get('content-type')?.split(';')[0]) { case 'multipart/form-data': case 'application/x-www-form-urlencoded': return parseFormData(await req.formData()); case 'application/json': return req.json(); } } /** Schema to parse pagination query params. */ const paginationSchema = z.object({ since: z.coerce.number().optional().catch(undefined), until: z.lazy(() => z.coerce.number().catch(nostrNow())), limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)), }); /** Mastodon API pagination query params. */ type PaginationParams = z.infer; /** Build HTTP Link header for Mastodon API pagination. */ function buildLinkHeader(url: string, events: Event[]): string | undefined { if (!events.length) return; const firstEvent = events[0]; const lastEvent = events[events.length - 1]; const { pathname, search } = new URL(url); const next = new URL(pathname + search, Conf.localDomain); const prev = new URL(pathname + search, Conf.localDomain); next.searchParams.set('until', String(lastEvent.created_at)); prev.searchParams.set('since', String(firstEvent.created_at)); return `<${next}>; rel="next", <${prev}>; rel="prev"`; } /** JSON-LD context. */ type LDContext = (string | Record>)[]; /** Add a basic JSON-LD context to ActivityStreams object, if it doesn't already exist. */ function maybeAddContext(object: T): T & { '@context': LDContext } { return { '@context': ['https://www.w3.org/ns/activitystreams'], ...object, }; } /** Like hono's `c.json()` except returns JSON-LD. */ function activityJson(c: Context, object: T) { const response = c.json(maybeAddContext(object)); response.headers.set('content-type', 'application/activity+json; charset=UTF-8'); return response; } export { activityJson, buildLinkHeader, createAdminEvent, createEvent, type PaginationParams, paginationSchema, parseBody, };