Merge branch 'instance' into 'main'

Configurable Instance (partially), implement Pleroma's ConfigDB

See merge request soapbox-pub/ditto!98
This commit is contained in:
Alex Gleason 2024-01-09 04:56:39 +00:00
commit 0914959ee6
7 changed files with 96 additions and 44 deletions

View File

@ -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.
NIP-04 encrypted JSON array of Pleroma ConfigDB objects. Pleroma admin API endpoints set this config, and Ditto reads from it.

View File

@ -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.

View File

@ -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() {

View File

@ -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`,

View File

@ -1,49 +1,88 @@
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)).catch([]).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<Record<string, unknown>>((result, [name, data]) => {
result[name.replace(/^:/, '')] = data;
return result;
}, {});
return c.json(data);
} else {
return c.json({});
}
};
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 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<Record<string, unknown>>((result, [name, data]) => {
result[name.replace(/^:/, '')] = data;
return result;
}, {});
const configs = jsonSchema.pipe(z.array(configSchema)).catch([]).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 };
export { configController, frontendConfigController, updateConfigController };

View File

@ -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: {

View File

@ -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<typeof mediaDataSchema>;
@ -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,