From 9efdb8d209037ecf34ba180dec936fff8bc236b4 Mon Sep 17 00:00:00 2001 From: Ricardo Arturo Cabral Mejia Date: Tue, 16 Aug 2022 04:10:00 +0000 Subject: [PATCH] feat: implement nip-09 event deletion --- README.md | 2 +- src/@types/event.ts | 4 ++- src/@types/repositories.ts | 4 ++- src/adapters/web-server-adapter.ts | 2 +- src/constants/base.ts | 5 +++ src/factories/event-strategy-factory.ts | 7 ++-- .../event-strategies/delete-event-strategy.ts | 33 +++++++++++++++++++ ...trategy copy.ts => null-event-strategy.ts} | 0 src/repositories/event-repository.ts | 15 +++++++-- src/utils/event.ts | 5 +++ .../repositories/event-repository.spec.ts | 2 +- 11 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 src/handlers/event-strategies/delete-event-strategy.ts rename src/handlers/event-strategies/{null-event-strategy copy.ts => null-event-strategy.ts} (100%) diff --git a/README.md b/README.md index cbfb365..6c49ccd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ NIPs with a relay-specific implementation are listed here. - [ ] NIP-03: OpenTimestams Attestations for Events - [x] NIP-04: Encrypted Direct Message - [ ] NIP-05: Mapping Nostr keys to DNS identifiers -- [ ] NIP-09: Event deletion +- [x] NIP-09: Event deletion - [x] NIP-11: Relay information document - [x] NIP-12: Generic tag queries - [ ] NIP-13: Proof of Work diff --git a/src/@types/event.ts b/src/@types/event.ts index 365975d..0d12145 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -3,7 +3,9 @@ import { Pubkey, TagName } from './base' export type EventId = string -export interface Tag { +export type Tag = TagBase | [] + +export interface TagBase { 0: TagName [index: number]: string } diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index d2dd6fb..faceb2f 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -1,5 +1,6 @@ import { PassThrough } from 'stream' -import { DBEvent, Event } from './event' +import { Pubkey } from './base' +import { DBEvent, Event, EventId } from './event' import { SubscriptionFilter } from './subscription' export type ExposedPromiseKeys = 'then' | 'catch' | 'finally' @@ -12,4 +13,5 @@ export interface IEventRepository { create(event: Event): Promise upsert(event: Event): Promise findByFilters(filters: SubscriptionFilter[]): IQueryResult + deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise } diff --git a/src/adapters/web-server-adapter.ts b/src/adapters/web-server-adapter.ts index 0c6574c..39cf07c 100644 --- a/src/adapters/web-server-adapter.ts +++ b/src/adapters/web-server-adapter.ts @@ -32,7 +32,7 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter description, pubkey, contact, - supported_nips: [1, 2, 4, 11, 12, 15, 16], + supported_nips: [1, 2, 4, 9, 11, 12, 15, 16], software: packageJson.repository.url, version: packageJson.version, } diff --git a/src/constants/base.ts b/src/constants/base.ts index fbe90ec..92e2034 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -7,3 +7,8 @@ export enum EventKinds { DELETE = 5, REACTION = 7, } + +export enum EventTags { + Event = 'e', + Pubkey = 'p', +} diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index 979ceaa..841eb1d 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -1,13 +1,14 @@ import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy' import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy' -import { NullEventStrategy } from '../handlers/event-strategies/null-event-strategy copy' +import { NullEventStrategy } from '../handlers/event-strategies/null-event-strategy' import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy' import { Factory } from '../@types/base' import { Event } from '../@types/event' import { IEventStrategy } from '../@types/message-handlers' import { IEventRepository } from '../@types/repositories' -import { isEphemeralEvent, isNullEvent, isReplaceableEvent } from '../utils/event' +import { isDeleteEvent, isEphemeralEvent, isNullEvent, isReplaceableEvent } from '../utils/event' import { IWebSocketAdapter } from '../@types/adapters' +import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy' export const eventStrategyFactory = ( @@ -19,6 +20,8 @@ export const eventStrategyFactory = ( return new EphemeralEventStrategy(adapter) } else if (isNullEvent(event)) { return new NullEventStrategy() + } else if (isDeleteEvent(event)) { + return new DeleteEventStrategy(eventRepository) } return new DefaultEventStrategy(adapter, eventRepository) diff --git a/src/handlers/event-strategies/delete-event-strategy.ts b/src/handlers/event-strategies/delete-event-strategy.ts new file mode 100644 index 0000000..1535770 --- /dev/null +++ b/src/handlers/event-strategies/delete-event-strategy.ts @@ -0,0 +1,33 @@ +import { Event } from '../../@types/event' +import { IEventStrategy } from '../../@types/message-handlers' +import { IEventRepository } from '../../@types/repositories' +import { EventTags } from '../../constants/base' + + +export class DeleteEventStrategy implements IEventStrategy> { + public constructor( + private readonly eventRepository: IEventRepository, + ) { } + + public async execute(event: Event): Promise { + try { + const eTags = event.tags.filter((tag) => tag[0] === EventTags.Event) + + if (!eTags.length) { + return + } + + await this.eventRepository.deleteByPubkeyAndIds( + event.pubkey, + eTags.map((tag) => tag[1]) + ) + + return + } catch (error) { + console.error('Unable to handle event. Reason:', error) + + return false + } + } + +} diff --git a/src/handlers/event-strategies/null-event-strategy copy.ts b/src/handlers/event-strategies/null-event-strategy.ts similarity index 100% rename from src/handlers/event-strategies/null-event-strategy copy.ts rename to src/handlers/event-strategies/null-event-strategy.ts diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index 3f38564..8128bc7 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -1,7 +1,7 @@ import { Knex } from 'knex' -import { __, applySpec, equals, modulo, omit, pipe, prop, cond, always, groupBy, T, evolve, forEach, isEmpty, forEachObjIndexed, isNil, complement, toPairs, filter, nth, ifElse, invoker } from 'ramda' +import { __, applySpec, equals, modulo, omit, pipe, prop, cond, always, groupBy, T, evolve, forEach, isEmpty, forEachObjIndexed, isNil, complement, toPairs, filter, nth, ifElse, invoker, identity } from 'ramda' -import { DBEvent, Event } from '../@types/event' +import { DBEvent, Event, EventId } from '../@types/event' import { IEventRepository, IQueryResult } from '../@types/repositories' import { SubscriptionFilter } from '../@types/subscription' import { isGenericTagQuery } from '../utils/filter' @@ -156,4 +156,15 @@ export class EventRepository implements IEventRepository { .where('events.event_created_at', '<', row.event_created_at) .then(prop('rowCount') as () => number) } + + public async deleteByPubkeyAndIds(pubkey: string, ids: EventId[]): Promise { + const query = this.dbClient('events') + .where({ + event_pubkey: pubkey, + }) + .whereIn('event_id', ids) + .delete() + + return query.then(identity) + } } diff --git a/src/utils/event.ts b/src/utils/event.ts index 833ab9d..9544479 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -3,6 +3,7 @@ import { applySpec, pipe, prop } from 'ramda' import { CanonicalEvent, Event } from '../@types/event' import { SubscriptionFilter } from '../@types/subscription' +import { EventKinds } from '../constants/base' import { isGenericTagQuery } from './filter' import { fromBuffer } from './transform' @@ -95,3 +96,7 @@ export const isEphemeralEvent = (event: Event): boolean => { export const isNullEvent = (event: Event): boolean => { return event.kind === Number.MAX_SAFE_INTEGER } + +export const isDeleteEvent = (event: Event): boolean => { + return event.kind === EventKinds.DELETE +} diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index 0d4d94d..60c0514 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -11,7 +11,7 @@ const { expect } = chai import { EventRepository } from '../../../src/repositories/event-repository' -describe.only('EventRepository', () => { +describe('EventRepository', () => { let repository: IEventRepository let sandbox: sinon.SinonSandbox