From 15810c1935a48e35df8d33993a53a170a320bd41 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Jan 2024 16:53:46 -0600 Subject: [PATCH 1/4] Configure server from the kind 0 event of the admin user --- src/config.ts | 4 ---- src/controllers/api/instance.ts | 15 ++++++++++----- src/controllers/nostr/relay-info.ts | 15 ++++++++++----- src/schemas/nostr.ts | 10 ++++++++++ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/config.ts b/src/config.ts index 8c2626a..2f98c1e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -58,10 +58,6 @@ const Conf = { get postCharLimit() { return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); }, - /** Admin contact to expose through various endpoints. This information is public. */ - get adminEmail() { - return Deno.env.get('ADMIN_EMAIL') || 'webmaster@localhost'; - }, /** S3 media storage configuration. */ s3: { get endPoint() { diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index ebc3269..01f1a5b 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,17 +1,22 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; +import { eventsDB } from '@/storages.ts'; -const instanceController: AppController = (c) => { +const instanceController: AppController = async (c) => { const { host, protocol } = Conf.url; + const [event] = await eventsDB.filter([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }]); + const meta = jsonServerMetaSchema.parse(event?.content); + /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; return c.json({ uri: host, - title: 'Ditto', - description: 'Nostr and the Fediverse', - short_description: 'Nostr and the Fediverse', + title: meta.name ?? 'Ditto', + description: meta.about ?? 'Nostr and the Fediverse', + short_description: meta.tagline ?? 'Nostr and the Fediverse', registrations: Conf.registrations, max_toot_chars: Conf.postCharLimit, configuration: { @@ -49,7 +54,7 @@ const instanceController: AppController = (c) => { streaming_api: `${wsProtocol}//${host}`, }, version: '0.0.0 (compatible; Ditto 0.0.1)', - email: Conf.adminEmail, + email: meta.email ?? `postmaster@${host}`, nostr: { pubkey: Conf.pubkey, relay: `${wsProtocol}//${host}/relay`, diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index 9c16cdd..60c2f8b 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,13 +1,18 @@ import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; +import { eventsDB } from '@/storages.ts'; + +const relayInfoController: AppController = async (c) => { + const [event] = await eventsDB.filter([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }]); + const meta = jsonServerMetaSchema.parse(event?.content); -const relayInfoController: AppController = (c) => { return c.json({ - name: 'Ditto', - description: 'Nostr and the Fediverse.', + name: meta.name ?? 'Ditto', + description: meta.about ?? 'Nostr and the Fediverse.', pubkey: Conf.pubkey, - contact: `mailto:${Conf.adminEmail}`, - supported_nips: [1, 5, 9, 11, 16, 45, 46, 98], + contact: `mailto:${meta.email ?? `postmaster@${Conf.url.host}`}`, + supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98], software: 'Ditto', version: '0.0.0', limitation: { diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index 7fb888c..1e8c0af 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -85,6 +85,12 @@ const mediaDataSchema = z.object({ width: z.number().int().positive().optional().catch(undefined), }); +/** Kind 0 content schema for the Ditto server admin user. */ +const serverMetaSchema = metaContentSchema.extend({ + tagline: z.string().optional().catch(undefined), + email: z.string().optional().catch(undefined), +}); + /** Media data from `"media"` tags. */ type MediaData = z.infer; @@ -94,6 +100,9 @@ const jsonMetaContentSchema = jsonSchema.pipe(metaContentSchema).catch({}); /** Parses media data from a JSON string. */ const jsonMediaDataSchema = jsonSchema.pipe(mediaDataSchema).catch({}); +/** Parses server admin meta from a JSON string. */ +const jsonServerMetaSchema = jsonSchema.pipe(serverMetaSchema).catch({}); + /** NIP-11 Relay Information Document. */ const relayInfoDocSchema = z.object({ name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined), @@ -130,6 +139,7 @@ export { filterSchema, jsonMediaDataSchema, jsonMetaContentSchema, + jsonServerMetaSchema, type MediaData, mediaDataSchema, metaContentSchema, From 8c972dbabd0d93dbb053503b61853f57e69b8f12 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Jan 2024 17:34:07 -0600 Subject: [PATCH 2/4] Actually implement Pleroma's configs API correctly (wow!) --- src/controllers/api/pleroma.ts | 66 ++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 91c6cc7..e6709c9 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -1,49 +1,71 @@ import { type AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; +import { decryptAdmin, encryptAdmin } from '@/crypto.ts'; import { z } from '@/deps.ts'; import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts'; import { eventsDB } from '@/storages.ts'; import { createAdminEvent } from '@/utils/api.ts'; -import { Conf } from '@/config.ts'; +import { jsonSchema } from '@/schema.ts'; const frontendConfigController: AppController = async (c) => { const [event] = await eventsDB.filter([{ kinds: [30078], authors: [Conf.pubkey], - '#d': ['pub.ditto.frontendConfig'], + '#d': ['pub.ditto.pleroma.config'], limit: 1, }]); - if (event) { - const data = JSON.parse(event.content); - return c.json(data); - } + const configs = jsonSchema.pipe(z.array(configSchema)).parse( + event?.content ? await decryptAdmin(Conf.pubkey, event.content) : [], + ); - return c.json({}); + const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); + + if (frontendConfig) { + const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); + const data = schema.parse(frontendConfig.value).reduce>((result, [name, data]) => { + result[name.replace(/^:/, '')] = data; + return result; + }, {}); + return c.json(data); + } else { + return c.json({}); + } }; /** Pleroma admin config controller. */ const updateConfigController: AppController = async (c) => { - const json = await c.req.json(); - const { configs } = z.object({ configs: z.array(configSchema) }).parse(json); + const { pubkey } = Conf; - for (const { group, key, value } of configs) { - if (group === ':pleroma' && key === ':frontend_configurations') { - const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); + const [event] = await eventsDB.filter([{ + kinds: [30078], + authors: [pubkey], + '#d': ['pub.ditto.pleroma.config'], + limit: 1, + }]); - const data = schema.parse(value).reduce>((result, [name, data]) => { - result[name.replace(/^:/, '')] = data; - return result; - }, {}); + const configs = jsonSchema.pipe(z.array(configSchema)).parse( + event?.content ? await decryptAdmin(pubkey, event.content) : [], + ); - await createAdminEvent({ - kind: 30078, - content: JSON.stringify(data), - tags: [['d', 'pub.ditto.frontendConfig']], - }, c); + const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json()); + + for (const { group, key, value } of newConfigs) { + const index = configs.findIndex((c) => c.group === group && c.key === key); + if (index === -1) { + configs.push({ group, key, value }); + } else { + configs[index].value = value; } } - return c.json([]); + await createAdminEvent({ + kind: 30078, + content: await encryptAdmin(pubkey, JSON.stringify(configs)), + tags: [['d', 'pub.ditto.pleroma.config']], + }, c); + + return c.json({ configs: newConfigs, need_reboot: false }); }; export { frontendConfigController, updateConfigController }; From db3ee13baaa289d96cc77642212329e5f47db712 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Jan 2024 18:01:24 -0600 Subject: [PATCH 3/4] Implement Pleroma config GET controller --- src/app.ts | 3 ++- src/controllers/api/pleroma.ts | 27 ++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index bc1b372..f4e99dc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,7 +44,7 @@ import { instanceController } from './controllers/api/instance.ts'; import { mediaController } from './controllers/api/media.ts'; import { notificationsController } from './controllers/api/notifications.ts'; import { createTokenController, oauthAuthorizeController, oauthController } from './controllers/api/oauth.ts'; -import { frontendConfigController, updateConfigController } from './controllers/api/pleroma.ts'; +import { configController, frontendConfigController, updateConfigController } from './controllers/api/pleroma.ts'; import { preferencesController } from './controllers/api/preferences.ts'; import { relayController } from './controllers/nostr/relay.ts'; import { searchController } from './controllers/api/search.ts'; @@ -187,6 +187,7 @@ app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); app.get('/api/v1/blocks', requirePubkey, blocksController); app.get('/api/v1/admin/accounts', adminAccountsController); +app.get('/api/v1/pleroma/admin/config', requireRole('admin'), configController); app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); // Not (yet) implemented. diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index e6709c9..cc6addb 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -15,8 +15,8 @@ const frontendConfigController: AppController = async (c) => { limit: 1, }]); - const configs = jsonSchema.pipe(z.array(configSchema)).parse( - event?.content ? await decryptAdmin(Conf.pubkey, event.content) : [], + const configs = jsonSchema.pipe(z.array(configSchema)).catch([]).parse( + event?.content ? await decryptAdmin(Conf.pubkey, event.content) : '', ); const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations'); @@ -33,6 +33,23 @@ const frontendConfigController: AppController = async (c) => { } }; +const configController: AppController = async (c) => { + const { pubkey } = Conf; + + const [event] = await eventsDB.filter([{ + kinds: [30078], + authors: [pubkey], + '#d': ['pub.ditto.pleroma.config'], + limit: 1, + }]); + + const configs = jsonSchema.pipe(z.array(configSchema)).catch([]).parse( + event?.content ? await decryptAdmin(pubkey, event.content) : '', + ); + + return c.json({ configs, need_reboot: false }); +}; + /** Pleroma admin config controller. */ const updateConfigController: AppController = async (c) => { const { pubkey } = Conf; @@ -44,8 +61,8 @@ const updateConfigController: AppController = async (c) => { limit: 1, }]); - const configs = jsonSchema.pipe(z.array(configSchema)).parse( - event?.content ? await decryptAdmin(pubkey, event.content) : [], + const configs = jsonSchema.pipe(z.array(configSchema)).catch([]).parse( + event?.content ? await decryptAdmin(pubkey, event.content) : '', ); const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json()); @@ -68,4 +85,4 @@ const updateConfigController: AppController = async (c) => { return c.json({ configs: newConfigs, need_reboot: false }); }; -export { frontendConfigController, updateConfigController }; +export { configController, frontendConfigController, updateConfigController }; From bbaa5da0a0f337014cb9ae27fb5e79335bc5f32e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Jan 2024 18:21:35 -0600 Subject: [PATCH 4/4] docs: frontendConfig -> pleroma.config --- docs/events.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/events.md b/docs/events.md index f19845e..9f8e30a 100644 --- a/docs/events.md +++ b/docs/events.md @@ -37,10 +37,6 @@ Example: The sections below describe the `content` field. Some are encrypted and some are not, depending on whether the data should be public. Also, some events are user events, and some are admin events. -### `pub.ditto.blocks` +### `pub.ditto.pleroma.config` -An encrypted array of blocked pubkeys, JSON stringified in `content` and encrypted with `nip04.encrypt`. - -### `pub.ditto.frontendConfig` - -JSON data for Pleroma frontends served on `/api/pleroma/frontend_configurations`. Each key contains arbitrary data used by a different frontend. \ No newline at end of file +NIP-04 encrypted JSON array of Pleroma ConfigDB objects. Pleroma admin API endpoints set this config, and Ditto reads from it. \ No newline at end of file