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.
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.

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 { 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<string, TagCondition> = {
'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<void> {
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() {

View File

@ -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<UserRow>) {
async function insertUser(user: Insertable<UserRow>) {
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> {
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',
};
}
}

View File

@ -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,