From 154056f8d6486ffbfa3f730e6473fff6fa972a5e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Nov 2024 08:56:38 -0600 Subject: [PATCH] Add custom profile fields --- src/controllers/api/accounts.ts | 9 ++++++++- src/schemas/nostr.ts | 15 ++++++++++++++- src/views/mastodon/accounts.ts | 6 ++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 27cf7590..1ae6a4f6 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -15,6 +15,7 @@ import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts' import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; +import { metadataSchema } from '@/schemas/nostr.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { bech32ToPubkey } from '@/utils.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; @@ -269,6 +270,7 @@ const updateCredentialsSchema = z.object({ pleroma_settings_store: z.record(z.string(), z.unknown()).optional(), lud16: z.string().email().or(z.literal('')).optional(), website: z.string().url().or(z.literal('')).optional(), + fields_attributes: z.object({ name: z.string(), value: z.string() }).array().optional(), }); const updateCredentialsController: AppController = async (c) => { @@ -284,11 +286,12 @@ const updateCredentialsController: AppController = async (c) => { const event = await updateEvent( { kinds: [0], authors: [pubkey], limit: 1 }, async (prev) => { - const meta = n.json().pipe(n.metadata()).catch({}).parse(prev.content); + const meta = n.json().pipe(metadataSchema).catch({}).parse(prev.content); const { avatar: avatarFile, header: headerFile, display_name, + fields_attributes, note, nip05, lud16, @@ -316,6 +319,10 @@ const updateCredentialsController: AppController = async (c) => { if (lud16 === '') delete meta.lud16; if (website === '') delete meta.website; + if (fields_attributes) { + meta.fields = fields_attributes.map(({ name, value }) => [name, value]); + } + return { kind: 0, content: JSON.stringify(meta), diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index 4e8f917d..05cd0f31 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -9,6 +9,11 @@ const signedEventSchema = n.event() .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine(verifyEvent, 'Event signature is invalid'); +/** Kind 0 standardized fields extended with Ditto custom fields. */ +const metadataSchema = n.metadata().and(z.object({ + fields: z.tuple([z.string(), z.string()]).array().optional().catch(undefined), +})); + /** * Stored in the kind 0 content. * https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots @@ -63,4 +68,12 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url() /** NIP-30 custom emoji tag. */ type EmojiTag = z.infer; -export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema, signedEventSchema }; +export { + type EmojiTag, + emojiTagSchema, + metadataSchema, + relayInfoDocSchema, + screenshotsSchema, + serverMetaSchema, + signedEventSchema, +}; diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 6d0e0446..78358023 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -4,6 +4,7 @@ import { nip19, UnsignedEvent } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { MastodonAccount } from '@/entities/MastodonAccount.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { metadataSchema } from '@/schemas/nostr.ts'; import { getLnurl } from '@/utils/lnurl.ts'; import { parseAndVerifyNip05 } from '@/utils/nip05.ts'; import { parseNoteContent } from '@/utils/note.ts'; @@ -42,7 +43,8 @@ async function renderAccount( lud06, lud16, website, - } = n.json().pipe(n.metadata()).catch({}).parse(event.content); + fields, + } = n.json().pipe(metadataSchema).catch({}).parse(event.content); const npub = nip19.npubEncode(pubkey); const nprofile = nip19.nprofileEncode({ pubkey, relays: [Conf.relay] }); @@ -69,7 +71,7 @@ async function renderAccount( discoverable: true, display_name: name ?? '', emojis: renderEmojis(event), - fields: [], + fields: fields?.map(([name, value]) => ({ name, value, verified_at: null })) ?? [], follow_requests_count: 0, followers_count: event.author_stats?.followers_count ?? 0, following_count: event.author_stats?.following_count ?? 0,