diff --git a/deno.json b/deno.json index c8eba2a..25ec2ed 100644 --- a/deno.json +++ b/deno.json @@ -21,7 +21,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.0", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.1", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/db/DittoDB.ts b/src/db/DittoDB.ts index 9c3b280..68fdc62 100644 --- a/src/db/DittoDB.ts +++ b/src/db/DittoDB.ts @@ -41,7 +41,7 @@ export class DittoDB { } /** Migrate the database to the latest version. */ - private static async migrate(kysely: Kysely) { + static async migrate(kysely: Kysely) { const migrator = new Migrator({ db: kysely, provider: new FileMigrationProvider({ diff --git a/src/pipeline.ts b/src/pipeline.ts index 6f487ed..6e4ebb1 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -45,7 +45,6 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - if (event.kind === 5) { - const ids = getTagSet(event.tags, 'e'); - const store = await Storages.db(); - - if (event.pubkey === Conf.pubkey) { - await store.remove([{ ids: [...ids] }], { signal }); - } else { - const events = await store.query( - [{ ids: [...ids], authors: [event.pubkey] }], - { signal }, - ); - - const deleteIds = events.map(({ id }) => id); - await store.remove([{ ids: deleteIds }], { signal }); - } - } -} - /** Track whenever a hashtag is used, for processing trending tags. */ async function trackHashtags(event: NostrEvent): Promise { const date = nostrDate(event.created_at); diff --git a/src/storages/EventsDB.test.ts b/src/storages/EventsDB.test.ts index 10f0d11..95c446c 100644 --- a/src/storages/EventsDB.test.ts +++ b/src/storages/EventsDB.test.ts @@ -1,22 +1,39 @@ +import { Database as Sqlite } from '@db/sqlite'; +import { DenoSqlite3Dialect } from '@soapbox/kysely-deno-sqlite'; import { assertEquals, assertRejects } from '@std/assert'; +import { Kysely } from 'kysely'; +import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; +import { DittoTables } from '@/db/DittoTables.ts'; +import { EventsDB } from '@/storages/EventsDB.ts'; import event0 from '~/fixtures/events/event-0.json' with { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' with { type: 'json' }; -import { EventsDB } from '@/storages/EventsDB.ts'; - -const kysely = await DittoDB.getInstance(); -const eventsDB = new EventsDB(kysely); +/** Create in-memory database for testing. */ +const createDB = async () => { + const kysely = new Kysely({ + dialect: new DenoSqlite3Dialect({ + database: new Sqlite(':memory:'), + }), + }); + const eventsDB = new EventsDB(kysely); + await DittoDB.migrate(kysely); + return { eventsDB, kysely }; +}; Deno.test('count filters', async () => { + const { eventsDB } = await createDB(); + assertEquals((await eventsDB.count([{ kinds: [1] }])).count, 0); await eventsDB.event(event1); assertEquals((await eventsDB.count([{ kinds: [1] }])).count, 1); }); Deno.test('insert and filter events', async () => { + const { eventsDB } = await createDB(); + await eventsDB.event(event1); assertEquals(await eventsDB.query([{ kinds: [1] }]), [event1]); @@ -30,6 +47,8 @@ Deno.test('insert and filter events', async () => { }); Deno.test('query events with domain search filter', async () => { + const { eventsDB, kysely } = await createDB(); + await eventsDB.event(event1); assertEquals(await eventsDB.query([{}]), [event1]); @@ -46,13 +65,84 @@ Deno.test('query events with domain search filter', async () => { }); Deno.test('delete events', async () => { - await eventsDB.event(event1); - assertEquals(await eventsDB.query([{ kinds: [1] }]), [event1]); - await eventsDB.remove([{ kinds: [1] }]); - assertEquals(await eventsDB.query([{ kinds: [1] }]), []); + const { eventsDB } = await createDB(); + + const [one, two] = [ + { id: '1', kind: 1, pubkey: 'abc', content: 'hello world', created_at: 1, sig: '', tags: [] }, + { id: '2', kind: 1, pubkey: 'abc', content: 'yolo fam', created_at: 2, sig: '', tags: [] }, + ]; + + await eventsDB.event(one); + await eventsDB.event(two); + + // Sanity check + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two, one]); + + await eventsDB.event({ + kind: 5, + pubkey: one.pubkey, + tags: [['e', one.id]], + created_at: 0, + content: '', + id: '', + sig: '', + }); + + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two]); +}); + +Deno.test("user cannot delete another user's event", async () => { + const { eventsDB } = await createDB(); + + const event = { id: '1', kind: 1, pubkey: 'abc', content: 'hello world', created_at: 1, sig: '', tags: [] }; + await eventsDB.event(event); + + // Sanity check + assertEquals(await eventsDB.query([{ kinds: [1] }]), [event]); + + await eventsDB.event({ + kind: 5, + pubkey: 'def', // different pubkey + tags: [['e', event.id]], + created_at: 0, + content: '', + id: '', + sig: '', + }); + + assertEquals(await eventsDB.query([{ kinds: [1] }]), [event]); +}); + +Deno.test('admin can delete any event', async () => { + const { eventsDB } = await createDB(); + + const [one, two] = [ + { id: '1', kind: 1, pubkey: 'abc', content: 'hello world', created_at: 1, sig: '', tags: [] }, + { id: '2', kind: 1, pubkey: 'abc', content: 'yolo fam', created_at: 2, sig: '', tags: [] }, + ]; + + await eventsDB.event(one); + await eventsDB.event(two); + + // Sanity check + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two, one]); + + await eventsDB.event({ + kind: 5, + pubkey: Conf.pubkey, // Admin pubkey + tags: [['e', one.id]], + created_at: 0, + content: '', + id: '', + sig: '', + }); + + assertEquals(await eventsDB.query([{ kinds: [1] }]), [two]); }); Deno.test('inserting replaceable events', async () => { + const { eventsDB } = await createDB(); + assertEquals((await eventsDB.count([{ kinds: [0], authors: [event0.pubkey] }])).count, 0); await eventsDB.event(event0); diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 5f1acbb..f2789d3 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -8,6 +8,7 @@ import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { normalizeFilters } from '@/filter.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; +import { getTagSet } from '@/tags.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; @@ -51,9 +52,18 @@ class EventsDB implements NStore { async event(event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise { event = purifyEvent(event); this.console.debug('EVENT', JSON.stringify(event)); + await this.deleteEventsAdmin(event); return this.store.event(event); } + /** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */ + async deleteEventsAdmin(event: NostrEvent): Promise { + if (event.kind === 5 && event.pubkey === Conf.pubkey) { + const ids = getTagSet(event.tags, 'e'); + await this.remove([{ ids: [...ids] }]); + } + } + /** Get events for filters from the database. */ async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { filters = await this.expandFilters(filters);