diff --git a/src/pipeline.ts b/src/pipeline.ts index 6e4ebb1..15d495e 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -122,17 +122,8 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise { @@ -140,16 +143,51 @@ Deno.test('admin can delete any event', async () => { assertEquals(await eventsDB.query([{ kinds: [1] }]), [two]); }); +Deno.test('throws a RelayError when inserting an event deleted by the admin', async () => { + const { eventsDB } = await createDB(); + + const event = genEvent(); + await eventsDB.event(event); + + const deletion = genEvent({ kind: 5, tags: [['e', event.id]] }, Conf.seckey); + await eventsDB.event(deletion); + + await assertRejects( + () => eventsDB.event(event), + RelayError, + 'event deleted by admin', + ); +}); + +Deno.test('throws a RelayError when inserting an event deleted by a user', async () => { + const { eventsDB } = await createDB(); + + const sk = generateSecretKey(); + + const event = genEvent({}, sk); + await eventsDB.event(event); + + const deletion = genEvent({ kind: 5, tags: [['e', event.id]] }, sk); + await eventsDB.event(deletion); + + await assertRejects( + () => eventsDB.event(event), + RelayError, + 'event deleted by user', + ); +}); + Deno.test('inserting replaceable events', async () => { const { eventsDB } = await createDB(); - assertEquals((await eventsDB.count([{ kinds: [0], authors: [event0.pubkey] }])).count, 0); + const event = event0; + await eventsDB.event(event); - await eventsDB.event(event0); - await assertRejects(() => eventsDB.event(event0)); - assertEquals((await eventsDB.count([{ kinds: [0], authors: [event0.pubkey] }])).count, 1); + const olderEvent = { ...event, id: '123', created_at: event.created_at - 1 }; + await eventsDB.event(olderEvent); + assertEquals(await eventsDB.query([{ kinds: [0], authors: [event.pubkey] }]), [event]); - const changeEvent = { ...event0, id: '123', created_at: event0.created_at + 1 }; - await eventsDB.event(changeEvent); - assertEquals(await eventsDB.query([{ kinds: [0] }]), [changeEvent]); + const newerEvent = { ...event, id: '123', created_at: event.created_at + 1 }; + await eventsDB.event(newerEvent); + assertEquals(await eventsDB.query([{ kinds: [0] }]), [newerEvent]); }); diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index f2789d3..aac8e52 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -11,6 +11,7 @@ import { purifyEvent } from '@/storages/hydrate.ts'; import { getTagSet } from '@/tags.ts'; import { isNostrId, isURL } from '@/utils.ts'; import { abortError } from '@/utils/abort.ts'; +import { RelayError } from '@/RelayError.ts'; /** Function to decide whether or not to index a tag. */ type TagCondition = ({ event, count, value }: { @@ -52,12 +53,36 @@ class EventsDB implements NStore { async event(event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise { event = purifyEvent(event); this.console.debug('EVENT', JSON.stringify(event)); + + if (await this.isDeletedAdmin(event)) { + throw new RelayError('blocked', 'event deleted by admin'); + } + await this.deleteEventsAdmin(event); - return this.store.event(event); + + try { + await this.store.event(event); + } catch (e) { + if (e.message === 'Cannot add a deleted event') { + throw new RelayError('blocked', 'event deleted by user'); + } else if (e.message === 'Cannot replace an event with an older event') { + return; + } else { + this.console.debug('ERROR', e.message); + } + } + } + + /** Check if an event has been deleted by the admin. */ + private async isDeletedAdmin(event: NostrEvent): Promise { + const [deletion] = await this.query([ + { kinds: [5], authors: [Conf.pubkey], '#e': [event.id], limit: 1 }, + ]); + return !!deletion; } /** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */ - async deleteEventsAdmin(event: NostrEvent): Promise { + private async deleteEventsAdmin(event: NostrEvent): Promise { if (event.kind === 5 && event.pubkey === Conf.pubkey) { const ids = getTagSet(event.tags, 'e'); await this.remove([{ ids: [...ids] }]); diff --git a/src/test.ts b/src/test.ts index 4586282..ea9c8fa 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,7 +1,23 @@ import { NostrEvent } from '@nostrify/nostrify'; +import { finalizeEvent, generateSecretKey } from 'nostr-tools'; + +import { purifyEvent } from '@/storages/hydrate.ts'; /** Import an event fixture by name in tests. */ export async function eventFixture(name: string): Promise { const result = await import(`~/fixtures/events/${name}.json`, { with: { type: 'json' } }); return structuredClone(result.default); } + +/** Generate an event for use in tests. */ +export function genEvent(t: Partial = {}, sk: Uint8Array = generateSecretKey()): NostrEvent { + const event = finalizeEvent({ + kind: 255, + created_at: 0, + content: '', + tags: [], + ...t, + }, sk); + + return purifyEvent(event); +}