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.
|
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`.
|
NIP-04 encrypted JSON array of Pleroma ConfigDB objects. Pleroma admin API endpoints set this config, and Ditto reads from it.
|
||||||
|
|
||||||
### `pub.ditto.frontendConfig`
|
|
||||||
|
|
||||||
JSON data for Pleroma frontends served on `/api/pleroma/frontend_configurations`. Each key contains arbitrary data used by a different frontend.
|
|
|
@ -44,7 +44,7 @@ import { instanceController } from './controllers/api/instance.ts';
|
||||||
import { mediaController } from './controllers/api/media.ts';
|
import { mediaController } from './controllers/api/media.ts';
|
||||||
import { notificationsController } from './controllers/api/notifications.ts';
|
import { notificationsController } from './controllers/api/notifications.ts';
|
||||||
import { createTokenController, oauthAuthorizeController, oauthController } from './controllers/api/oauth.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 { preferencesController } from './controllers/api/preferences.ts';
|
||||||
import { relayController } from './controllers/nostr/relay.ts';
|
import { relayController } from './controllers/nostr/relay.ts';
|
||||||
import { searchController } from './controllers/api/search.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/blocks', requirePubkey, blocksController);
|
||||||
|
|
||||||
app.get('/api/v1/admin/accounts', adminAccountsController);
|
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);
|
app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController);
|
||||||
|
|
||||||
// Not (yet) implemented.
|
// Not (yet) implemented.
|
||||||
|
|
|
@ -58,10 +58,6 @@ const Conf = {
|
||||||
get postCharLimit() {
|
get postCharLimit() {
|
||||||
return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000);
|
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 media storage configuration. */
|
||||||
s3: {
|
s3: {
|
||||||
get endPoint() {
|
get endPoint() {
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { Conf } from '@/config.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 { 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`. */
|
/** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */
|
||||||
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
uri: host,
|
uri: host,
|
||||||
title: 'Ditto',
|
title: meta.name ?? 'Ditto',
|
||||||
description: 'Nostr and the Fediverse',
|
description: meta.about ?? 'Nostr and the Fediverse',
|
||||||
short_description: 'Nostr and the Fediverse',
|
short_description: meta.tagline ?? 'Nostr and the Fediverse',
|
||||||
registrations: Conf.registrations,
|
registrations: Conf.registrations,
|
||||||
max_toot_chars: Conf.postCharLimit,
|
max_toot_chars: Conf.postCharLimit,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@ -49,7 +54,7 @@ const instanceController: AppController = (c) => {
|
||||||
streaming_api: `${wsProtocol}//${host}`,
|
streaming_api: `${wsProtocol}//${host}`,
|
||||||
},
|
},
|
||||||
version: '0.0.0 (compatible; Ditto 0.0.1)',
|
version: '0.0.0 (compatible; Ditto 0.0.1)',
|
||||||
email: Conf.adminEmail,
|
email: meta.email ?? `postmaster@${host}`,
|
||||||
nostr: {
|
nostr: {
|
||||||
pubkey: Conf.pubkey,
|
pubkey: Conf.pubkey,
|
||||||
relay: `${wsProtocol}//${host}/relay`,
|
relay: `${wsProtocol}//${host}/relay`,
|
||||||
|
|
|
@ -1,49 +1,88 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { decryptAdmin, encryptAdmin } from '@/crypto.ts';
|
||||||
import { z } from '@/deps.ts';
|
import { z } from '@/deps.ts';
|
||||||
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
|
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
|
||||||
import { eventsDB } from '@/storages.ts';
|
import { eventsDB } from '@/storages.ts';
|
||||||
import { createAdminEvent } from '@/utils/api.ts';
|
import { createAdminEvent } from '@/utils/api.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { jsonSchema } from '@/schema.ts';
|
||||||
|
|
||||||
const frontendConfigController: AppController = async (c) => {
|
const frontendConfigController: AppController = async (c) => {
|
||||||
const [event] = await eventsDB.filter([{
|
const [event] = await eventsDB.filter([{
|
||||||
kinds: [30078],
|
kinds: [30078],
|
||||||
authors: [Conf.pubkey],
|
authors: [Conf.pubkey],
|
||||||
'#d': ['pub.ditto.frontendConfig'],
|
'#d': ['pub.ditto.pleroma.config'],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
if (event) {
|
const configs = jsonSchema.pipe(z.array(configSchema)).catch([]).parse(
|
||||||
const data = JSON.parse(event.content);
|
event?.content ? await decryptAdmin(Conf.pubkey, event.content) : '',
|
||||||
return c.json(data);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
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({});
|
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. */
|
/** Pleroma admin config controller. */
|
||||||
const updateConfigController: AppController = async (c) => {
|
const updateConfigController: AppController = async (c) => {
|
||||||
const json = await c.req.json();
|
const { pubkey } = Conf;
|
||||||
const { configs } = z.object({ configs: z.array(configSchema) }).parse(json);
|
|
||||||
|
|
||||||
for (const { group, key, value } of configs) {
|
const [event] = await eventsDB.filter([{
|
||||||
if (group === ':pleroma' && key === ':frontend_configurations') {
|
kinds: [30078],
|
||||||
const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array();
|
authors: [pubkey],
|
||||||
|
'#d': ['pub.ditto.pleroma.config'],
|
||||||
|
limit: 1,
|
||||||
|
}]);
|
||||||
|
|
||||||
const data = schema.parse(value).reduce<Record<string, unknown>>((result, [name, data]) => {
|
const configs = jsonSchema.pipe(z.array(configSchema)).catch([]).parse(
|
||||||
result[name.replace(/^:/, '')] = data;
|
event?.content ? await decryptAdmin(pubkey, event.content) : '',
|
||||||
return result;
|
);
|
||||||
}, {});
|
|
||||||
|
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({
|
await createAdminEvent({
|
||||||
kind: 30078,
|
kind: 30078,
|
||||||
content: JSON.stringify(data),
|
content: await encryptAdmin(pubkey, JSON.stringify(configs)),
|
||||||
tags: [['d', 'pub.ditto.frontendConfig']],
|
tags: [['d', 'pub.ditto.pleroma.config']],
|
||||||
}, c);
|
}, 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 { AppController } from '@/app.ts';
|
||||||
import { Conf } from '@/config.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({
|
return c.json({
|
||||||
name: 'Ditto',
|
name: meta.name ?? 'Ditto',
|
||||||
description: 'Nostr and the Fediverse.',
|
description: meta.about ?? 'Nostr and the Fediverse.',
|
||||||
pubkey: Conf.pubkey,
|
pubkey: Conf.pubkey,
|
||||||
contact: `mailto:${Conf.adminEmail}`,
|
contact: `mailto:${meta.email ?? `postmaster@${Conf.url.host}`}`,
|
||||||
supported_nips: [1, 5, 9, 11, 16, 45, 46, 98],
|
supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98],
|
||||||
software: 'Ditto',
|
software: 'Ditto',
|
||||||
version: '0.0.0',
|
version: '0.0.0',
|
||||||
limitation: {
|
limitation: {
|
||||||
|
|
|
@ -85,6 +85,12 @@ const mediaDataSchema = z.object({
|
||||||
width: z.number().int().positive().optional().catch(undefined),
|
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. */
|
/** Media data from `"media"` tags. */
|
||||||
type MediaData = z.infer<typeof mediaDataSchema>;
|
type MediaData = z.infer<typeof mediaDataSchema>;
|
||||||
|
|
||||||
|
@ -94,6 +100,9 @@ const jsonMetaContentSchema = jsonSchema.pipe(metaContentSchema).catch({});
|
||||||
/** Parses media data from a JSON string. */
|
/** Parses media data from a JSON string. */
|
||||||
const jsonMediaDataSchema = jsonSchema.pipe(mediaDataSchema).catch({});
|
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. */
|
/** NIP-11 Relay Information Document. */
|
||||||
const relayInfoDocSchema = z.object({
|
const relayInfoDocSchema = z.object({
|
||||||
name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined),
|
name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined),
|
||||||
|
@ -130,6 +139,7 @@ export {
|
||||||
filterSchema,
|
filterSchema,
|
||||||
jsonMediaDataSchema,
|
jsonMediaDataSchema,
|
||||||
jsonMetaContentSchema,
|
jsonMetaContentSchema,
|
||||||
|
jsonServerMetaSchema,
|
||||||
type MediaData,
|
type MediaData,
|
||||||
mediaDataSchema,
|
mediaDataSchema,
|
||||||
metaContentSchema,
|
metaContentSchema,
|
||||||
|
|
Loading…
Reference in New Issue