Convert users to Events
This commit is contained in:
parent
716a7019c2
commit
13bf936088
|
@ -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.
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue