From 41ae842d7a417eb4d7b476ac34195a0f81632954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Mon, 26 Dec 2022 01:08:12 -0500 Subject: [PATCH] feat: upsert kind 41 events --- src/repositories/event-repository.ts | 2 +- src/utils/event.ts | 1 + .../features/nip-28/nip-28.feature | 38 ++++++++++ test/integration/features/nip-28/nip-28.ts | 72 +++++++++++++++++++ .../repositories/event-repository.spec.ts | 4 +- 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 test/integration/features/nip-28/nip-28.feature create mode 100644 test/integration/features/nip-28/nip-28.ts diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index e29eee4..022e7a2 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -216,7 +216,7 @@ export class EventRepository implements IEventRepository { // NIP-33: Parameterized Replaceable Events .onConflict( this.dbClient.raw( - '(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)' + '(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)' ) ) .merge(omit(['event_pubkey', 'event_kind', 'event_deduplication'])(row)) diff --git a/src/utils/event.ts b/src/utils/event.ts index c1d3121..c504b3b 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -162,6 +162,7 @@ export const isEventSignatureValid = async (event: Event): Promise => { export const isReplaceableEvent = (event: Event): boolean => { return event.kind === EventKinds.SET_METADATA || event.kind === EventKinds.CONTACT_LIST + || event.kind === EventKinds.CHANNEL_METADATA || (event.kind >= EventKinds.REPLACEABLE_FIRST && event.kind <= EventKinds.REPLACEABLE_LAST) } diff --git a/test/integration/features/nip-28/nip-28.feature b/test/integration/features/nip-28/nip-28.feature new file mode 100644 index 0000000..2e6a0a0 --- /dev/null +++ b/test/integration/features/nip-28/nip-28.feature @@ -0,0 +1,38 @@ +Feature: NIP-28 + Scenario: Alice creates a channel + Given someone called Alice + When Alice sends a channel_creation event with content '{\"name\": \"Demo Channel\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice subscribes to last event from Alice + Then Alice receives a channel_creation event from Alice with content '{\"name\": \"Demo Channel\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + + Scenario: Alice sets metadata for a channel + Given someone called Alice + And Alice subscribes to author Alice + And Alice sends a channel_creation event with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice receives a channel_creation event from Alice with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + When Alice sends a channel_metadata event with content '{\"name\": \"New\", \"about\": \"A better test channel.\", \"picture\": \"https://placekitten.com/256/256\"}' + Then Alice receives a channel_metadata event from Alice with content '{\"name\": \"New\", \"about\": \"A better test channel.\", \"picture\": \"https://placekitten.com/256/256\"}' + + Scenario: Alice replaces metadata for a channel + Given someone called Alice + And Alice subscribes to author Alice + And Alice sends a channel_creation event with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice receives a channel_creation event from Alice with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice sends a channel_metadata event with content '{\"name\": \"New\", \"about\": \"A better test channel.\", \"picture\": \"https://placekitten.com/256/256\"}' + And Alice receives a channel_metadata event from Alice with content '{\"name\": \"New\", \"about\": \"A better test channel.\", \"picture\": \"https://placekitten.com/256/256\"}' + When Alice sends a channel_metadata event with content '{\"name\": \"Replaced\", \"about\": \"A different test channel.\", \"picture\": \"https://placekitten.com/400/400\"}' + Then Alice receives a channel_metadata event from Alice with content '{\"name\": \"Replaced\", \"about\": \"A different test channel.\", \"picture\": \"https://placekitten.com/400/400\"}' + + Scenario: Alice replaces metadata for a channel + Given someone called Alice + And Alice subscribes to author Alice + And Alice sends a channel_creation event with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice receives a channel_creation event from Alice with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice sends a channel_metadata event with content '{\"name\": \"New\", \"about\": \"A better test channel.\", \"picture\": \"https://placekitten.com/256/256\"}' + And Alice receives a channel_metadata event from Alice with content '{\"name\": \"New\", \"about\": \"A better test channel.\", \"picture\": \"https://placekitten.com/256/256\"}' + And Alice unsubscribes from author Alice + When Alice sends a channel_metadata event with content '{\"name\": \"Replaced\", \"about\": \"A different test channel.\", \"picture\": \"https://placekitten.com/400/400\"}' + And Alice subscribes to channel_creation events + And Alice receives a channel_creation event from Alice with content '{\"name\": \"Original\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}' + And Alice subscribes to channel_metadata events + Then Alice receives a channel_metadata event from Alice with content '{\"name\": \"Replaced\", \"about\": \"A different test channel.\", \"picture\": \"https://placekitten.com/400/400\"}' diff --git a/test/integration/features/nip-28/nip-28.ts b/test/integration/features/nip-28/nip-28.ts new file mode 100644 index 0000000..2274e21 --- /dev/null +++ b/test/integration/features/nip-28/nip-28.ts @@ -0,0 +1,72 @@ +import { Before, Then, When, World } from '@cucumber/cucumber' +import WebSocket from 'ws' + +import { createEvent, createSubscription, sendEvent, waitForNextEvent } from '../helpers' +import { Event } from '../../../../src/@types/event' +import { expect } from 'chai' + +Before(function () { + this.parameters.channels = [] +}) + +When(/^(\w+) sends a channel_creation event with content '([^']+)'$/, async function(name: string, content: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + const event: Event = await createEvent({ pubkey, kind: 40, content }, privkey) + this.parameters.channels.push(event.id) + await sendEvent(ws, event) + this.parameters.events[name].push(event) +}) + +When(/^(\w+) sends a channel_metadata event with content '([^']+)'$/, async function(name: string, content: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + const channel = this.parameters.channels[this.parameters.channels.length - 1] + const event: Event = await createEvent({ pubkey, kind: 41, content, tags: [['e', channel]] }, privkey) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) +}) + +Then(/(\w+) receives a channel_creation event from (\w+) with content '([^']+?)'/, async function(name: string, author: string, content: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const receivedEvent = await waitForNextEvent(ws, subscription.name) + + expect(receivedEvent.kind).to.equal(40) + expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey) + expect(receivedEvent.content).to.equal(content) +}) + + +Then(/(\w+) receives a channel_metadata event from (\w+) with content '([^']+?)'/, async function(name: string, author: string, content: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const receivedEvent = await waitForNextEvent(ws, subscription.name) + + const channel = this.parameters.channels[this.parameters.channels.length - 1] + + expect(receivedEvent.kind).to.equal(41) + expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey) + expect(receivedEvent.content).to.equal(content) + expect(receivedEvent.tags).to.deep.include(['e', channel]) +}) + +When(/^(\w+) subscribes to channel_creation events$/, async function(this: World>, name: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = { name: `test-${Math.random()}`, filters: [{ kinds: [40] }] } + this.parameters.subscriptions[name].push(subscription) + + await createSubscription(ws, subscription.name, subscription.filters) +}) + + +When(/^(\w+) subscribes to channel_metadata events$/, async function(this: World>, name: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = { name: `test-${Math.random()}`, filters: [{ kinds: [41] }] } + this.parameters.subscriptions[name].push(subscription) + + await createSubscription(ws, subscription.name, subscription.filters) +}) diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index 424a5dc..ac33f72 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -459,7 +459,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") 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\', \'[]\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 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 where "events"."event_created_at" < 1564498626') + 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") 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\', \'[]\') 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 where "events"."event_created_at" < 1564498626') }) it('replaces event based on event_pubkey, event_kind and event_deduplication', () => { @@ -476,7 +476,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") 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\', \'[]\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 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 where "events"."event_created_at" < 1564498626') + 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") 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\', \'[]\') 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 where "events"."event_created_at" < 1564498626') }) }) })