Merge branch 'instance' into 'main'
Configurable Instance (partially), implement Pleroma's ConfigDB See merge request soapbox-pub/ditto!98
This commit is contained in:
commit
0914959ee6
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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) : '',
|
||||
);
|
||||
|
||||
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) : '',
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
await createAdminEvent({
|
||||
kind: 30078,
|
||||
content: JSON.stringify(data),
|
||||
tags: [['d', 'pub.ditto.frontendConfig']],
|
||||
content: await encryptAdmin(pubkey, JSON.stringify(configs)),
|
||||
tags: [['d', 'pub.ditto.pleroma.config']],
|
||||
}, c);
|
||||
}
|
||||
}
|
||||
|
||||
return c.json([]);
|
||||
return c.json({ configs: newConfigs, need_reboot: false });
|
||||
};
|
||||
|
||||
export { frontendConfigController, updateConfigController };
|
||||
export { configController, frontendConfigController, updateConfigController };
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue