diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts index 549f08d..6c9ed02 100644 --- a/src/controllers/activitypub/actor.ts +++ b/src/controllers/activitypub/actor.ts @@ -1,7 +1,7 @@ import { findUser } from '@/db/users.ts'; import { getAuthor } from '@/queries.ts'; import { toActor } from '@/transformers/nostr-to-activitypub.ts'; -import { activityJson } from '@/utils.ts'; +import { activityJson } from '@/utils/web.ts'; import type { AppContext, AppController } from '@/app.ts'; diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 84b3ce2..febf881 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -7,15 +7,8 @@ import { booleanParamSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { signEvent } from '@/sign.ts'; import { toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; -import { - buildLinkHeader, - eventDateComparator, - isFollowing, - lookupAccount, - nostrNow, - paginationSchema, - parseBody, -} from '@/utils.ts'; +import { eventDateComparator, isFollowing, lookupAccount, nostrNow } from '@/utils.ts'; +import { buildLinkHeader, paginationSchema, parseBody } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; const createAccountController: AppController = (c) => { diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index db0597f..830f433 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -1,6 +1,7 @@ import { lodash, nip19, uuid62, z } from '@/deps.ts'; import { AppController } from '@/app.ts'; -import { nostrNow, parseBody } from '@/utils.ts'; +import { nostrNow } from '@/utils.ts'; +import { parseBody } from '@/utils/web.ts'; const passwordGrantSchema = z.object({ grant_type: z.literal('password'), diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index aae7f6c..afb049a 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -4,7 +4,8 @@ import * as pipeline from '@/pipeline.ts'; import { getAncestors, getDescendants, getEvent } from '@/queries.ts'; import { signEvent } from '@/sign.ts'; import { toStatus } from '@/transformers/nostr-to-mastoapi.ts'; -import { nostrNow, parseBody } from '@/utils.ts'; +import { nostrNow } from '@/utils.ts'; +import { parseBody } from '@/utils/web.ts'; const createStatusSchema = z.object({ in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(), diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 6a442c1..6470e43 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -2,7 +2,7 @@ import { z } from '@/deps.ts'; import { getFeed, getPublicFeed } from '@/queries.ts'; import { booleanParamSchema } from '@/schema.ts'; import { toStatus } from '@/transformers/nostr-to-mastoapi.ts'; -import { buildLinkHeader, paginationSchema } from '@/utils.ts'; +import { buildLinkHeader, paginationSchema } from '@/utils/web.ts'; import type { AppController } from '@/app.ts'; diff --git a/src/queries.ts b/src/queries.ts index dc3f7f5..6b73a8b 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -1,7 +1,7 @@ import * as eventsDB from '@/db/events.ts'; import { type Event, type Filter, findReplyTag } from '@/deps.ts'; import * as mixer from '@/mixer.ts'; -import { type PaginationParams } from '@/utils.ts'; +import { type PaginationParams } from '@/utils/web.ts'; interface GetEventOpts { /** Timeout in milliseconds. */ diff --git a/src/utils.ts b/src/utils.ts index 4c2b82f..653c824 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,4 @@ -import { Conf } from '@/config.ts'; -import { type Context, type Event, nip19, parseFormData, z } from '@/deps.ts'; +import { type Event, nip19, z } from '@/deps.ts'; import { lookupNip05Cached } from '@/nip05.ts'; import { getAuthor } from '@/queries.ts'; @@ -66,40 +65,6 @@ async function lookupAccount(value: string): Promise | undefined> { } } -/** 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(); - } -} - -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)), -}); - -type PaginationParams = z.infer; - -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"`; -} - /** Return the event's age in milliseconds. */ function eventAge(event: Event): number { return new Date().getTime() - nostrDate(event.created_at).getTime(); @@ -123,24 +88,6 @@ async function sha256(message: string): Promise { return hashHex; } -/** 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; -} - /** Schema to parse a relay URL. */ const relaySchema = z.string().max(255).startsWith('wss://').url(); @@ -155,9 +102,7 @@ function isFollowing(source: Event<3>, targetPubkey: string): boolean { } export { - activityJson, bech32ToPubkey, - buildLinkHeader, eventAge, eventDateComparator, findTag, @@ -167,9 +112,6 @@ export { type Nip05, nostrDate, nostrNow, - type PaginationParams, - paginationSchema, - parseBody, parseNip05, relaySchema, sha256, diff --git a/src/utils/web.ts b/src/utils/web.ts index baccf85..5df153e 100644 --- a/src/utils/web.ts +++ b/src/utils/web.ts @@ -1,4 +1,5 @@ -import { EventTemplate, HTTPException } from '@/deps.ts'; +import { Conf } from '@/config.ts'; +import { type Context, type Event, EventTemplate, HTTPException, parseFormData, z } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { signEvent } from '@/sign.ts'; import { nostrNow } from '@/utils.ts'; @@ -31,4 +32,59 @@ async function createEvent(t: Omit, 'created_ return event; } -export { createEvent }; +/** 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, createEvent, type PaginationParams, paginationSchema, parseBody };