Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
<a href='https://coveralls.io/github/Cameri/nostream?branch=main'>
<img alt='Coverage Status' src='https://coveralls.io/repos/github/Cameri/nostream/badge.svg?branch=main' />
</a>
<a href='https://sonarcloud.io/project/overview?id=Cameri_nostream'>
<img alt='Sonarcloud quality gate' src='https://sonarcloud.io/api/project_badges/measure?project=Cameri_nostream&metric=alert_status' />
<a href='https://sonarcloud.io/project/overview?id=Cameri_nostr-ts-relay'>
<img alt='Sonarcloud quality gate' src='https://sonarcloud.io/api/project_badges/measure?project=Cameri_nostr&metric=alert_status' />
</a>
<a href='https://github.com/Cameri/nostream/actions'>
<img alt='Build status' src='https://github.com/Cameri/nostream/actions/workflows/checks.yml/badge.svg?branch=main&event=push' />
Expand Down Expand Up @@ -57,6 +57,7 @@ NIPs with a relay-specific implementation are listed here.
- [x] NIP-20: Command Results
- [x] NIP-22: Event `created_at` Limits
- [x] NIP-26: Delegated Event Signing
- [x] NIP-28: Public Chat
- [x] NIP-33: Parameterized Replaceable Events

## Requirements
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
exports.up = async function (knex) {
return knex.schema
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
.raw(`DELETE FROM events a
WHERE a.event_kind = 41
AND a.event_created_at < (
SELECT max(event_created_at)
FROM events b
WHERE a.event_pubkey = b.event_pubkey
AND b.event_kind = 41
AND a.event_tags = b.event_tags
);`)
.raw(`UPDATE events a
SET event_deduplication = ('["' || encode(a.event_pubkey, 'hex') || '",' || a.event_kind || ']')::jsonb
WHERE a.event_kind = 41
AND a.event_created_at = (
SELECT max(event_created_at)
FROM events b
WHERE a.event_pubkey = b.event_pubkey
AND b.event_kind = 41
AND a.event_tags = b.event_tags
);`)
.raw(
`CREATE UNIQUE INDEX replaceable_events_idx
ON events ( 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);`,
)
}

exports.down = function (knex) {
return knex.schema
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
.raw(
`CREATE UNIQUE INDEX replaceable_events_idx
ON events ( 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);`,
)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
20,
22,
26,
28,
33
],
"main": "src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/repositories/event-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/utils/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const isEventSignatureValid = async (event: Event): Promise<boolean> => {
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)
}

Expand Down
38 changes: 38 additions & 0 deletions test/integration/features/nip-28/nip-28.feature
Original file line number Diff line number Diff line change
@@ -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\"}'
72 changes: 72 additions & 0 deletions test/integration/features/nip-28/nip-28.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>>, 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<Record<string, any>>, 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)
})
4 changes: 2 additions & 2 deletions test/unit/repositories/event-repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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')
})
})
})