Convert users to Events

This commit is contained in:
Alex Gleason 2023-12-29 16:37:18 -06:00
parent 716a7019c2
commit 13bf936088
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
5 changed files with 107 additions and 16 deletions

View File

@ -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.<thing>` for `d` tags. [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.<thing>` 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. 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. JSON data for Pleroma frontends served on `/api/pleroma/frontend_configurations`. Each key contains arbitrary data used by a different frontend.

34
scripts/db.ts Normal file
View File

@ -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 <command>');
}
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);
}
}

View File

@ -1,7 +1,8 @@
import { Conf } from '@/config.ts';
import { db, type DittoDB } from '@/db.ts'; import { db, type DittoDB } from '@/db.ts';
import { Debug, type Event, type SelectQueryBuilder } from '@/deps.ts'; import { Debug, type Event, type SelectQueryBuilder } from '@/deps.ts';
import { type DittoFilter } from '@/filter.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 { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { type DittoEvent, EventStore, type GetEventsOpts, type StoreEventOpts } from '@/store.ts'; import { type DittoEvent, EventStore, type GetEventsOpts, type StoreEventOpts } from '@/store.ts';
import { isNostrId, isURL } from '@/utils.ts'; import { isNostrId, isURL } from '@/utils.ts';
@ -25,12 +26,18 @@ const tagConditions: Record<string, TagCondition> = {
'proxy': ({ count, value }) => count === 0 && isURL(value), 'proxy': ({ count, value }) => count === 0 && isURL(value),
'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value), 'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value),
't': ({ count, value }) => count < 5 && value.length < 50, '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. */ /** Insert an event (and its tags) into the database. */
function storeEvent(event: Event, opts: StoreEventOpts = {}): Promise<void> { function storeEvent(event: Event, opts: StoreEventOpts = {}): Promise<void> {
debug('EVENT', JSON.stringify(event)); 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) => { return db.transaction().execute(async (trx) => {
/** Insert the event into the database. */ /** Insert the event into the database. */
async function addEvent() { async function addEvent() {

View File

@ -1,6 +1,10 @@
import { Debug, type Insertable } from '@/deps.ts'; import { Conf } from '@/config.ts';
import { Debug, type Filter, type Insertable } from '@/deps.ts';
import { db, type UserRow } from '../db.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'); const debug = Debug('ditto:users');
@ -12,9 +16,25 @@ interface User {
} }
/** Adds a user to the database. */ /** Adds a user to the database. */
function insertUser(user: Insertable<UserRow>) { async function insertUser(user: Insertable<UserRow>) {
debug('insertUser', JSON.stringify(user)); 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<UserRow>) {
* ``` * ```
*/ */
async function findUser(user: Partial<Insertable<UserRow>>): Promise<User | undefined> { async function findUser(user: Partial<Insertable<UserRow>>): Promise<User | undefined> {
let query = db.selectFrom('users').selectAll(); const filter: Filter = { kinds: [30361], authors: [Conf.pubkey], limit: 1 };
for (const [key, value] of Object.entries(user)) { 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 { return {
...row, pubkey: event.tags.find(([name]) => name === 'd')?.[1]!,
admin: row.admin === 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',
}; };
} }
} }

View File

@ -18,6 +18,11 @@ function isParameterizedReplaceableKind(kind: number) {
return 30000 <= kind && kind < 40000; 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. */ /** Classification of the event kind. */
type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'; type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown';
@ -32,6 +37,7 @@ function classifyKind(kind: number): KindClassification {
export { export {
classifyKind, classifyKind,
isDittoInternalKind,
isEphemeralKind, isEphemeralKind,
isParameterizedReplaceableKind, isParameterizedReplaceableKind,
isRegularKind, isRegularKind,