diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 8381607..7c8b0a0 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -1,7 +1,7 @@ import { type AppController } from '@/app.ts'; import { type Filter, findReplyTag, z } from '@/deps.ts'; import { getAuthor, getFilter, getFollows, publish } from '@/client.ts'; -import { parseMetaContent } from '@/schema.ts'; +import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { signEvent } from '@/sign.ts'; import { toAccount, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { buildLinkHeader, eventDateComparator, lookupAccount, nostrNow, paginationSchema, parseBody } from '@/utils.ts'; @@ -154,7 +154,7 @@ const updateCredentialsController: AppController = async (c) => { return c.json({ error: 'Could not find user.' }, 404); } - const meta = parseMetaContent(author); + const meta = jsonMetaContentSchema.parse(author.content); meta.name = result.data.display_name ?? meta.name; meta.about = result.data.note ?? meta.about; diff --git a/src/schema.ts b/src/schema.ts index 646367e..361a310 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,9 +1,5 @@ import { z } from '@/deps.ts'; -import type { Event } from './event.ts'; - -const optionalString = z.string().optional().catch(undefined); - /** Validates individual items in an array, dropping any that aren't valid. */ function filteredArray(schema: T) { return z.any().array().catch([]) @@ -24,31 +20,6 @@ const jsonSchema = z.string().transform((value, ctx) => { } }); -const metaContentSchema = z.object({ - name: optionalString, - about: optionalString, - picture: optionalString, - banner: optionalString, - nip05: optionalString, - lud16: optionalString, -}); - -/** Author metadata from Event<0>. */ -type MetaContent = z.infer; - -/** - * Get (and validate) data from a kind 0 event. - * https://github.com/nostr-protocol/nips/blob/master/01.md - */ -function parseMetaContent(event: Event<0>): MetaContent { - try { - const json = JSON.parse(event.content); - return metaContentSchema.passthrough().parse(json); - } catch (_e) { - return {}; - } -} - /** Alias for `safeParse`, but instead of returning a success object it returns the value (or undefined on fail). */ function parseValue(schema: z.ZodType, value: unknown): T | undefined { const result = schema.safeParse(value); @@ -83,15 +54,4 @@ const decode64Schema = z.string().transform((value, ctx) => { const hashtagSchema = z.string().regex(/^\w{1,30}$/); -export { - decode64Schema, - emojiTagSchema, - filteredArray, - hashtagSchema, - jsonSchema, - type MetaContent, - metaContentSchema, - parseMetaContent, - parseRelay, - relaySchema, -}; +export { decode64Schema, emojiTagSchema, filteredArray, hashtagSchema, jsonSchema, parseRelay, relaySchema }; diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index 1b318db..56bd276 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -1,5 +1,7 @@ import { verifySignature, z } from '@/deps.ts'; +import { jsonSchema } from '../schema.ts'; + /** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */ const hexIdSchema = z.string().regex(/^[0-9a-f]{64}$/); @@ -37,4 +39,28 @@ const clientMsgSchema = z.union([ z.tuple([z.literal('CLOSE'), z.string().min(1)]), ]); -export { clientMsgSchema, filterSchema, hexIdSchema, signedEventSchema }; +/** Kind 0 content schema. */ +const metaContentSchema = z.object({ + name: z.string().optional().catch(undefined), + about: z.string().optional().catch(undefined), + picture: z.string().optional().catch(undefined), + banner: z.string().optional().catch(undefined), + nip05: z.string().optional().catch(undefined), + lud16: z.string().optional().catch(undefined), +}).partial().passthrough(); + +/** Parses kind 0 content from a JSON string. */ +const jsonMetaContentSchema = jsonSchema.pipe(metaContentSchema).catch({}); + +/** Author metadata from Event<0>. */ +type MetaContent = z.infer; + +export { + clientMsgSchema, + filterSchema, + hexIdSchema, + jsonMetaContentSchema, + type MetaContent, + metaContentSchema, + signedEventSchema, +}; diff --git a/src/transformers/nostr-to-activitypub.ts b/src/transformers/nostr-to-activitypub.ts index ae7ea77..f868087 100644 --- a/src/transformers/nostr-to-activitypub.ts +++ b/src/transformers/nostr-to-activitypub.ts @@ -1,5 +1,5 @@ import { Conf } from '@/config.ts'; -import { parseMetaContent } from '@/schema.ts'; +import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { getPublicKeyPem } from '@/utils/rsa.ts'; import type { Event } from '@/event.ts'; @@ -7,7 +7,7 @@ import type { Actor } from '@/schemas/activitypub.ts'; /** Nostr metadata event to ActivityPub actor. */ async function toActor(event: Event<0>, username: string): Promise { - const content = parseMetaContent(event); + const content = jsonMetaContentSchema.parse(event.content); return { type: 'Person', diff --git a/src/transformers/nostr-to-mastoapi.ts b/src/transformers/nostr-to-mastoapi.ts index ea70df5..53ff85e 100644 --- a/src/transformers/nostr-to-mastoapi.ts +++ b/src/transformers/nostr-to-mastoapi.ts @@ -6,7 +6,8 @@ import { findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, z } from ' import { type Event } from '@/event.ts'; import { verifyNip05Cached } from '@/nip05.ts'; import { getMediaLinks, type MediaLink, parseNoteContent } from '@/note.ts'; -import { emojiTagSchema, filteredArray, type MetaContent, parseMetaContent } from '@/schema.ts'; +import { emojiTagSchema, filteredArray } from '@/schema.ts'; +import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts'; const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png'; @@ -20,7 +21,7 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { const { withSource = false } = opts; const { pubkey } = event; - const { name, nip05, picture, banner, about }: MetaContent = parseMetaContent(event); + const { name, nip05, picture, banner, about } = jsonMetaContentSchema.parse(event.content); const npub = nip19.npubEncode(pubkey); let parsed05: Nip05 | undefined;