From 13bf936088165a73aa510095f8f53f1d8ebb3678 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 29 Dec 2023 16:37:18 -0600 Subject: [PATCH] Convert users to Events --- docs/nip78.md | 20 ++++++++++++++---- scripts/db.ts | 34 ++++++++++++++++++++++++++++++ src/db/events.ts | 9 +++++++- src/db/users.ts | 54 ++++++++++++++++++++++++++++++++++++++---------- src/kinds.ts | 6 ++++++ 5 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 scripts/db.ts diff --git a/docs/nip78.md b/docs/nip78.md index caa6111..8f011e4 100644 --- a/docs/nip78.md +++ b/docs/nip78.md @@ -1,13 +1,25 @@ -# Ditto NIP-78 events +# Ditto custom events + +## Users + +Ditto user events describe a pubkey's relationship with the Ditto server. They are parameterized replaceable events of kind `30361` where the `d` tag is a pubkey. These events are published by Ditto's internal admin keypair. + +User events have the following tags: + +- `d` - pubkey of the user. +- `name` - NIP-05 username granted to the user, without the domain. +- `role` - one of `admin` or `user`. + +## NIP-78 [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md) defines events of kind `30078` with a globally unique `d` tag. These events are queried by the `d` tag, which allows Ditto to store custom data on relays. Ditto uses reverse DNS names like `pub.ditto.` for `d` tags. 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.blocks` -An encrypted array of blocked pubkeys, JSON stringified and encrypted with `nip07.encrypt`. +An encrypted array of blocked pubkeys, JSON stringified in `content` and encrypted with `nip04.encrypt`. -## `pub.ditto.frontendConfig` +### `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 diff --git a/scripts/db.ts b/scripts/db.ts new file mode 100644 index 0000000..f1b7989 --- /dev/null +++ b/scripts/db.ts @@ -0,0 +1,34 @@ +import { Conf } from '@/config.ts'; +import { db } from '@/db.ts'; +import { eventsDB } from '@/db/events.ts'; +import { signAdminEvent } from '@/sign.ts'; + +switch (Deno.args[0]) { + case 'users-to-events': + await usersToEvents(); + break; + default: + console.log('Usage: deno run -A scripts/db.ts '); +} + +async function usersToEvents() { + const { origin, host } = Conf.url; + + for (const row of await db.selectFrom('users').selectAll().execute()) { + const event = await signAdminEvent({ + kind: 30361, + tags: [ + ['d', row.pubkey], + ['name', row.username], + ['role', row.admin ? 'admin' : 'user'], + ['origin', origin], + // NIP-31: https://github.com/nostr-protocol/nips/blob/master/31.md + ['alt', `@${row.username}@${host}'s account was updated by the admins of ${host}`], + ], + content: '', + created_at: Math.floor(new Date(row.inserted_at).getTime() / 1000), + }); + + await eventsDB.storeEvent(event); + } +} diff --git a/src/db/events.ts b/src/db/events.ts index 96cf845..2d18af2 100644 --- a/src/db/events.ts +++ b/src/db/events.ts @@ -1,7 +1,8 @@ +import { Conf } from '@/config.ts'; import { db, type DittoDB } from '@/db.ts'; import { Debug, type Event, type SelectQueryBuilder } from '@/deps.ts'; import { type DittoFilter } from '@/filter.ts'; -import { isParameterizedReplaceableKind } from '@/kinds.ts'; +import { isDittoInternalKind, isParameterizedReplaceableKind } from '@/kinds.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { type DittoEvent, EventStore, type GetEventsOpts, type StoreEventOpts } from '@/store.ts'; import { isNostrId, isURL } from '@/utils.ts'; @@ -25,12 +26,18 @@ const tagConditions: Record = { 'proxy': ({ count, value }) => count === 0 && isURL(value), 'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value), 't': ({ count, value }) => count < 5 && value.length < 50, + 'name': ({ event, count }) => event.kind === 30361 && count === 0, + 'role': ({ event, count }) => event.kind === 30361 && count === 0, }; /** Insert an event (and its tags) into the database. */ function storeEvent(event: Event, opts: StoreEventOpts = {}): Promise { debug('EVENT', JSON.stringify(event)); + if (isDittoInternalKind(event.kind) && event.pubkey !== Conf.pubkey) { + throw new Error('Internal events can only be stored by the server keypair'); + } + return db.transaction().execute(async (trx) => { /** Insert the event into the database. */ async function addEvent() { diff --git a/src/db/users.ts b/src/db/users.ts index 7d56e1c..8f0a78a 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -1,6 +1,10 @@ -import { Debug, type Insertable } from '@/deps.ts'; - -import { db, type UserRow } from '../db.ts'; +import { Conf } from '@/config.ts'; +import { Debug, type Filter, type Insertable } from '@/deps.ts'; +import { type UserRow } from '@/db.ts'; +import { eventsDB } from '@/db/events.ts'; +import * as pipeline from '@/pipeline.ts'; +import { signAdminEvent } from '@/sign.ts'; +import { nostrNow } from '@/utils.ts'; const debug = Debug('ditto:users'); @@ -12,9 +16,25 @@ interface User { } /** Adds a user to the database. */ -function insertUser(user: Insertable) { +async function insertUser(user: Insertable) { debug('insertUser', JSON.stringify(user)); - return db.insertInto('users').values(user).execute(); + const { origin, host } = Conf.url; + + const event = await signAdminEvent({ + kind: 30361, + tags: [ + ['d', user.pubkey], + ['name', user.username], + ['role', user.admin ? 'admin' : 'user'], + ['origin', origin], + // NIP-31: https://github.com/nostr-protocol/nips/blob/master/31.md + ['alt', `@${user.username}@${host}'s account was updated by the admins of ${host}`], + ], + content: '', + created_at: nostrNow(), + }); + + return pipeline.handleEvent(event); } /** @@ -25,18 +45,30 @@ function insertUser(user: Insertable) { * ``` */ async function findUser(user: Partial>): Promise { - let query = db.selectFrom('users').selectAll(); + const filter: Filter = { kinds: [30361], authors: [Conf.pubkey], limit: 1 }; for (const [key, value] of Object.entries(user)) { - query = query.where(key as keyof UserRow, '=', value); + switch (key) { + case 'pubkey': + filter['#d'] = [String(value)]; + break; + case 'username': + filter['#name'] = [String(value)]; + break; + case 'admin': + filter['#role'] = [value ? 'admin' : 'user']; + break; + } } - const row = await query.executeTakeFirst(); + const [event] = await eventsDB.getEvents([filter]); - if (row) { + if (event) { return { - ...row, - admin: row.admin === 1, + pubkey: event.tags.find(([name]) => name === 'd')?.[1]!, + username: event.tags.find(([name]) => name === 'name')?.[1]!, + inserted_at: new Date(event.created_at * 1000), + admin: event.tags.find(([name]) => name === 'role')?.[1] === 'admin', }; } } diff --git a/src/kinds.ts b/src/kinds.ts index 45c9729..7953837 100644 --- a/src/kinds.ts +++ b/src/kinds.ts @@ -18,6 +18,11 @@ function isParameterizedReplaceableKind(kind: number) { return 30000 <= kind && kind < 40000; } +/** These events are only valid if published by the server keypair. */ +function isDittoInternalKind(kind: number) { + return kind === 30361; +} + /** Classification of the event kind. */ type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'; @@ -32,6 +37,7 @@ function classifyKind(kind: number): KindClassification { export { classifyKind, + isDittoInternalKind, isEphemeralKind, isParameterizedReplaceableKind, isRegularKind,