diff --git a/src/app.ts b/src/app.ts index a719b78..82e083c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,7 +30,7 @@ import { emptyArrayController, emptyObjectController } from './controllers/api/f import { instanceController } from './controllers/api/instance.ts'; import { notificationsController } from './controllers/api/notifications.ts'; import { createTokenController, oauthAuthorizeController, oauthController } from './controllers/api/oauth.ts'; -import { frontendConfigController } from './controllers/api/pleroma.ts'; +import { 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'; @@ -55,7 +55,7 @@ import { nodeInfoController, nodeInfoSchemaController } from './controllers/well import { nostrController } from './controllers/well-known/nostr.ts'; import { webfingerController } from './controllers/well-known/webfinger.ts'; import { auth19, requirePubkey } from './middleware/auth19.ts'; -import { auth98 } from './middleware/auth98.ts'; +import { auth98, requireAdmin } from './middleware/auth98.ts'; interface AppEnv extends HonoEnv { Variables: { @@ -136,6 +136,8 @@ app.get('/api/v1/trends', trendingTagsController); app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/favourites', requirePubkey, favouritesController); +app.post('/api/v1/pleroma/admin/config', requireAdmin, updateConfigController); + // Not (yet) implemented. app.get('/api/v1/bookmarks', emptyArrayController); app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 992531e..396df5c 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -1,7 +1,46 @@ import { type AppController } from '@/app.ts'; +import * as eventsDB from '@/db/events.ts'; +import { z } from '@/deps.ts'; +import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts'; +import { createAdminEvent } from '@/utils/web.ts'; +import { Conf } from '@/config.ts'; + +const frontendConfigController: AppController = async (c) => { + const [event] = await eventsDB.getFilters( + [{ kinds: [30078], authors: [Conf.pubkey], limit: 1 }], + ); + + if (event) { + const data = JSON.parse(event.content); + return c.json(data); + } -const frontendConfigController: AppController = (c) => { return c.json({}); }; -export { frontendConfigController }; +/** 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); + + for (const { group, key, value } of configs) { + if (group === ':pleroma' && key === ':frontend_configurations') { + const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array(); + + const data = schema.parse(value).reduce>((result, [name, data]) => { + result[name.replace(/^:/, '')] = data; + return result; + }, {}); + + await createAdminEvent({ + kind: 30078, + content: JSON.stringify(data), + tags: [['d', 'pub.ditto.frontendConfig']], + }, c); + } + } + + return c.json([]); +}; + +export { frontendConfigController, updateConfigController }; diff --git a/src/db.ts b/src/db.ts index 78b4c1c..2deca9a 100644 --- a/src/db.ts +++ b/src/db.ts @@ -39,7 +39,7 @@ interface UserRow { pubkey: string; username: string; inserted_at: Date; - admin: boolean; + admin: 0 | 1; } interface RelayRow { diff --git a/src/db/users.ts b/src/db/users.ts index 632eca7..0e8d9a0 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -2,6 +2,13 @@ import { type Insertable } from '@/deps.ts'; import { db, type UserRow } from '../db.ts'; +interface User { + pubkey: string; + username: string; + inserted_at: Date; + admin: boolean; +} + /** Adds a user to the database. */ function insertUser(user: Insertable) { return db.insertInto('users').values(user).execute(); @@ -14,14 +21,21 @@ function insertUser(user: Insertable) { * await findUser({ username: 'alex' }); * ``` */ -function findUser(user: Partial>) { +async function findUser(user: Partial>): Promise { let query = db.selectFrom('users').selectAll(); for (const [key, value] of Object.entries(user)) { query = query.where(key as keyof UserRow, '=', value); } - return query.executeTakeFirst(); + const row = await query.executeTakeFirst(); + + if (row) { + return { + ...row, + admin: row.admin === 1, + }; + } } -export { findUser, insertUser }; +export { findUser, insertUser, type User }; diff --git a/src/pipeline.ts b/src/pipeline.ts index 14a364a..573dafb 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,3 +1,4 @@ +import { Conf } from '@/config.ts'; import * as eventsDB from '@/db/events.ts'; import { addRelays } from '@/db/relays.ts'; import { findUser } from '@/db/users.ts'; @@ -42,10 +43,13 @@ async function getEventData({ pubkey }: Event): Promise { return { user }; } +/** Check if the pubkey is the `DITTO_NSEC` pubkey. */ +const isAdminEvent = ({ pubkey }: Event): boolean => pubkey === Conf.pubkey; + /** Maybe store the event, if eligible. */ async function storeEvent(event: Event, data: EventData): Promise { if (isEphemeralKind(event.kind)) return; - if (data.user || await isLocallyFollowed(event.pubkey)) { + if (data.user || isAdminEvent(event) || await isLocallyFollowed(event.pubkey)) { await eventsDB.insertEvent(event).catch(console.warn); } else { return Promise.reject(new RelayError('blocked', 'only registered users can post')); diff --git a/src/schemas/pleroma-api.ts b/src/schemas/pleroma-api.ts new file mode 100644 index 0000000..839b442 --- /dev/null +++ b/src/schemas/pleroma-api.ts @@ -0,0 +1,46 @@ +import { z } from '@/deps.ts'; + +type ElixirValue = + | string + | number + | boolean + | null + | ElixirTuple + | ElixirValue[] + | { [key: string]: ElixirValue }; + +interface ElixirTuple { + tuple: [string, ElixirValue]; +} + +interface Config { + group: string; + key: string; + value: ElixirValue; +} + +const baseElixirValueSchema: z.ZodType = z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.lazy(() => elixirValueSchema.array()), + z.lazy(() => z.record(z.string(), elixirValueSchema)), +]); + +const elixirTupleSchema: z.ZodType = z.object({ + tuple: z.tuple([z.string(), z.lazy(() => elixirValueSchema)]), +}); + +const elixirValueSchema: z.ZodType = z.union([ + baseElixirValueSchema, + elixirTupleSchema, +]); + +const configSchema: z.ZodType = z.object({ + group: z.string(), + key: z.string(), + value: elixirValueSchema, +}); + +export { type Config, configSchema, type ElixirTuple, elixirTupleSchema, type ElixirValue }; diff --git a/src/transformers/nostr-to-mastoapi.ts b/src/transformers/nostr-to-mastoapi.ts index ca8c411..aa0b231 100644 --- a/src/transformers/nostr-to-mastoapi.ts +++ b/src/transformers/nostr-to-mastoapi.ts @@ -9,6 +9,7 @@ import { emojiTagSchema, filteredArray } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { isFollowing, type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts'; import { verifyNip05Cached } from '@/utils/nip05.ts'; +import { findUser } from '@/db/users.ts'; const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png'; const DEFAULT_BANNER = 'https://gleasonator.com/images/banner.png'; @@ -24,7 +25,8 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { const { name, nip05, picture, banner, about } = jsonMetaContentSchema.parse(event.content); const npub = nip19.npubEncode(pubkey); - const [parsed05, followersCount, followingCount, statusesCount] = await Promise.all([ + const [user, parsed05, followersCount, followingCount, statusesCount] = await Promise.all([ + findUser({ pubkey }), parseAndVerifyNip05(nip05, pubkey), eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]), getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length), @@ -65,6 +67,10 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { statuses_count: statusesCount, url: Conf.local(`/users/${pubkey}`), username: parsed05?.nickname || npub.substring(0, 8), + pleroma: { + is_admin: user?.admin || false, + is_moderator: user?.admin || false, + }, }; } diff --git a/src/types.ts b/src/types.ts index 11f933d..445a6d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ -import { UserRow } from '@/db.ts'; +import { User } from '@/db/users.ts'; interface EventData { - user: UserRow | undefined; + user: User | undefined; } export type { EventData };