diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index 98d05cd3..671dfaee 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -16,7 +16,6 @@ export interface IEventRepository { create(event: Event): Promise upsert(event: Event): Promise findByFilters(filters: SubscriptionFilter[]): IQueryResult - insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise } diff --git a/src/handlers/event-strategies/delete-event-strategy.ts b/src/handlers/event-strategies/delete-event-strategy.ts index c03479ff..c25a3b6b 100644 --- a/src/handlers/event-strategies/delete-event-strategy.ts +++ b/src/handlers/event-strategies/delete-event-strategy.ts @@ -32,16 +32,10 @@ export class DeleteEventStrategy implements IEventStrategy> ) if (eventIdsToDelete.length) { - const count = await this.eventRepository.deleteByPubkeyAndIds( + await this.eventRepository.deleteByPubkeyAndIds( event.pubkey, eventIdsToDelete ) - if (!count) { - await this.eventRepository.insertStubs( - event.pubkey, - eventIdsToDelete, - ) - } } const count = await this.eventRepository.create(event) diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index bc510860..fa4f7fa3 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -10,7 +10,6 @@ import { forEach, forEachObjIndexed, groupBy, - identity, ifElse, invoker, is, @@ -229,6 +228,7 @@ export class EventRepository implements IEventRepository { prop(EventExpirationTimeMetadataKey as any), always(null), ), + deleted_at: always(null), })(event) const query = this.masterDbClient('events') @@ -250,30 +250,6 @@ export class EventRepository implements IEventRepository { } as Promise } - public insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise { - debug('inserting stubs for %s: %o', pubkey, eventIdsToDelete) - const date = new Date() - return this.masterDbClient('events').insert( - eventIdsToDelete.map( - applySpec({ - event_id: pipe(identity, toBuffer), - event_pubkey: pipe(always(pubkey), toBuffer), - event_created_at: always(Math.floor(date.getTime() / 1000)), - event_kind: always(5), - event_tags: always('[]'), - event_content: always(''), - event_signature: pipe(always(''), toBuffer), - event_delegator: always(null), - event_deduplication: pipe(always([pubkey, 5]), toJSON), - expires_at: always(null), - deleted_at: always(date.toISOString()), - }) - ) - ) - .onConflict() - .ignore() as Promise - } - public deleteByPubkeyAndIds(pubkey: string, eventIdsToDelete: EventId[]): Promise { debug('deleting events from %s: %o', pubkey, eventIdsToDelete) diff --git a/test/integration/features/nip-09/nip-09.feature b/test/integration/features/nip-09/nip-09.feature index 663724ff..1bc53233 100644 --- a/test/integration/features/nip-09/nip-09.feature +++ b/test/integration/features/nip-09/nip-09.feature @@ -21,14 +21,10 @@ Feature: NIP-09 And Alice drafts a text_note event with content "Twitter > Nostr" When Alice sends a delete event for their last event And Alice sends their last draft event successfully - And Alice subscribes to author Alice - Then Alice receives 1 delete event from Alice and EOSE Scenario: Alice sends a delete before deleted set_metadata Given someone called Alice And someone called Bob And Alice drafts a set_metadata event When Alice sends a delete event for their last event - And Alice sends their last draft event unsuccessfully - And Alice subscribes to author Alice - Then Alice receives 1 delete event from Alice and EOSE + Then Alice sends their last draft event successfully diff --git a/test/integration/features/nip-33/nip-33.feature b/test/integration/features/nip-33/nip-33.feature index 855149db..dc669c61 100644 --- a/test/integration/features/nip-33/nip-33.feature +++ b/test/integration/features/nip-33/nip-33.feature @@ -30,3 +30,24 @@ Feature: NIP-33 Parameterized replaceable events And Alice sends a parameterized_replaceable_event_1 event with content "third" and tag d containing "friends" And Bob subscribes to author Alice Then Bob receives a parameterized_replaceable_event_1 event from Alice with content "third" and tag d containing "friends" + + Scenario: Alice deletes a parameterized replaceable event + Given someone called Alice + When Alice sends a parameterized_replaceable_event_1 event with content "exercise" and tag d containing "2023-resolutions" + And Alice sends a delete event for their last event + And Alice subscribes to author Alice + Then Alice receives 1 delete event from Alice and EOSE + + Scenario: Alice deletes and replaces a parameterized replaceable event + Given someone called Alice + And Alice sends a parameterized_replaceable_event_1 event with content "gym" and tag d containing "2024-resolutions" + And Alice sends a delete event for their last event + When Alice sends a parameterized_replaceable_event_1 event with content "exercise" and tag d containing "2024-resolutions" + And Alice subscribes to parameterized_replaceable_event_1 events from Alice + Then Alice receives a parameterized_replaceable_event_1 event from Alice with content "exercise" and tag d containing "2024-resolutions" + + Scenario: Alice deletes before sending parameterized replaceable event + Given someone called Alice + And Alice drafts a parameterized_replaceable_event_2 event with content "don't worry about it" and tag d containing "topsycrets" + When Alice sends a delete event for their last event + And Alice sends their last draft event successfully \ No newline at end of file diff --git a/test/integration/features/nip-33/nip-33.feature.ts b/test/integration/features/nip-33/nip-33.feature.ts index 9c421ebf..cba37160 100644 --- a/test/integration/features/nip-33/nip-33.feature.ts +++ b/test/integration/features/nip-33/nip-33.feature.ts @@ -1,10 +1,11 @@ -import { Then, When } from '@cucumber/cucumber' +import { Then, When, World } from '@cucumber/cucumber' import { expect } from 'chai' import WebSocket from 'ws' -import { createEvent, sendEvent, waitForEventCount, waitForNextEvent } from '../helpers' +import { createEvent, createSubscription, sendEvent, waitForEventCount, waitForNextEvent } from '../helpers' import { EventKinds, EventTags } from '../../../../src/constants/base' import { Event } from '../../../../src/@types/event' +import { isDraft } from '../shared' When(/^(\w+) sends a parameterized_replaceable_event_0 event with content "([^"]+)" and tag (\w) containing "([^"]+)"$/, async function( name: string, @@ -108,3 +109,40 @@ Then(/(\w+) receives (\d+) parameterized_replaceable_event_0 events? from (\w+) expect(events[0].pubkey).to.equal(this.parameters.identities[author].pubkey) expect(events[0].content).to.equal(content) }) + +When(/^(\w+) subscribes to parameterized_replaceable_event_1 events from (\w+)$/, async function (this: World>, name: string, author: string) { + const ws = this.parameters.clients[name] as WebSocket + const authorPubkey = this.parameters.identities[author].pubkey + const subscription = { + name: `test-${Math.random()}`, + filters: [ + { kinds: [EventKinds.PARAMETERIZED_REPLACEABLE_FIRST + 1], authors: [authorPubkey] }, + ], + } + this.parameters.subscriptions[name].push(subscription) + + await createSubscription(ws, subscription.name, subscription.filters) +}) + + +Then(/^(\w+) drafts a parameterized_replaceable_event_2 event with content "([^"]+?)" and tag (\w+) containing "([^"]+?)"$/, async function ( + name: string, + content: string, + tagName: string, + tagValue: string, +) { + const { pubkey, privkey } = this.parameters.identities[name] + + const event: Event = await createEvent({ + pubkey, + kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST + 2, + content, + tags: [ + [tagName, tagValue], + ], + }, privkey) + + event[isDraft] = true + + this.parameters.events[name].push(event) +}) \ No newline at end of file diff --git a/test/unit/handlers/event-strategies/delete-event-strategy.spec.ts b/test/unit/handlers/event-strategies/delete-event-strategy.spec.ts index 0d632a8c..b2c5fc63 100644 --- a/test/unit/handlers/event-strategies/delete-event-strategy.spec.ts +++ b/test/unit/handlers/event-strategies/delete-event-strategy.spec.ts @@ -32,7 +32,6 @@ describe('DeleteEventStrategy', () => { let webSocketEmitStub: Sinon.SinonStub let eventRepositoryCreateStub: Sinon.SinonStub let eventRepositoryDeleteByPubkeyAndIdsStub: Sinon.SinonStub - let eventRepositoryInsertStubsStub: Sinon.SinonStub let strategy: IEventStrategy> @@ -43,7 +42,6 @@ describe('DeleteEventStrategy', () => { eventRepositoryCreateStub = sandbox.stub(EventRepository.prototype, 'create') eventRepositoryDeleteByPubkeyAndIdsStub = sandbox.stub(EventRepository.prototype, 'deleteByPubkeyAndIds') - eventRepositoryInsertStubsStub = sandbox.stub(EventRepository.prototype, 'insertStubs') webSocketEmitStub = sandbox.stub() webSocket = { @@ -67,18 +65,6 @@ describe('DeleteEventStrategy', () => { expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event) }) - it('inserts stubs', async () => { - await strategy.execute(event) - - expect(eventRepositoryInsertStubsStub).to.have.been.calledOnceWithExactly( - event.pubkey, - [ - '0000000000000000000000000000000000000000000000000000000000000000', - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - ] - ) - }) - it('deletes events if it has e tags', async () => { await strategy.execute(event) @@ -135,7 +121,6 @@ describe('DeleteEventStrategy', () => { expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event) expect(eventRepositoryDeleteByPubkeyAndIdsStub).not.to.have.been.called - expect(eventRepositoryInsertStubsStub).to.not.have.been.called expect(webSocketEmitStub).not.to.have.been.called }) }) diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index c235c3d5..fd1b02df 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -439,24 +439,6 @@ describe('EventRepository', () => { }) }) - describe('insertStubs', () => { - let clock: sinon.SinonFakeTimers - - beforeEach(() => { - clock = sinon.useFakeTimers(1673835425) - }) - - afterEach(() => { - clock.restore() - }) - - it('insert stubs by pubkey & event ids', () => { - const query = repository.insertStubs('001122', ['aabbcc', 'ddeeff']).toString() - - expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at") values (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'aabbcc\', 5, X\'001122\', X\'\', \'[]\', NULL), (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'ddeeff\', 5, X\'001122\', X\'\', \'[]\', NULL) on conflict do nothing') - }) - }) - describe('deleteByPubkeyAndIds', () => { it('marks event as deleted by pubkey & event_id if not deleted', () => { const query = repository.deleteByPubkeyAndIds('001122', ['aabbcc', 'ddeeff']).toString() @@ -480,7 +462,7 @@ describe('EventRepository', () => { const query = repository.upsert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL where "events"."event_created_at" < 1564498626') + expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626') }) it('replaces event based on event_pubkey, event_kind and event_deduplication', () => { @@ -498,7 +480,7 @@ describe('EventRepository', () => { const query = repository.upsert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL where "events"."event_created_at" < 1564498626') + expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626') }) }) })