feat: implement nip-27

This commit is contained in:
Ricardo Arturo Cabral Mejia 2022-08-17 04:57:02 +00:00
parent 5da7d3a157
commit 1222e49a24
No known key found for this signature in database
GPG Key ID: 5931EBF43A650245
11 changed files with 103 additions and 20 deletions

View File

@ -13,7 +13,6 @@ NIPs with a relay-specific implementation are listed here.
- [x] NIP-02: Contact list and petnames
- [ ] NIP-03: OpenTimestams Attestations for Events
- [x] NIP-04: Encrypted Direct Message
- [ ] NIP-05: Mapping Nostr keys to DNS identifiers
- [x] NIP-09: Event deletion
- [x] NIP-11: Relay information document
- [x] NIP-12: Generic tag queries
@ -21,6 +20,7 @@ NIPs with a relay-specific implementation are listed here.
- [x] NIP-15: End of Stored Events Notice
- [x] NIP-16: Event Treatment
- [ ] NIP-25: Reactions
- [x] NIP-27: Multicasting (Experimental)
## Requirements

View File

@ -1,6 +1,13 @@
export type EventId = string
export type Pubkey = string
export type TagName = string
export type Signature = string
export type Tag = TagBase & string[]
export interface TagBase {
0: TagName
[index: number]: string
}
type Enumerate<
N extends number,
@ -14,4 +21,4 @@ export type Range<F extends number, T extends number> = Exclude<
Enumerate<F>
>
export type Factory<TOutput = any, TInput = any> = (input: TInput) => TOutput
export type Factory<TOutput = any, TInput = any> = (input: TInput) => TOutput

View File

@ -1,14 +1,6 @@
import { EventKinds } from '../constants/base'
import { Pubkey, TagName } from './base'
import { EventId, Pubkey, Tag } from './base'
export type EventId = string
export type Tag = TagBase & string[]
export interface TagBase {
0: TagName
[index: number]: string
}
export interface Event {
id: EventId

View File

@ -1,6 +1,6 @@
import { PassThrough } from 'stream'
import { Pubkey } from './base'
import { DBEvent, Event, EventId } from './event'
import { EventId, Pubkey } from './base'
import { DBEvent, Event } from './event'
import { SubscriptionFilter } from './subscription'
export type ExposedPromiseKeys = 'then' | 'catch' | 'finally'

View File

@ -1,6 +1,5 @@
import { EventKinds } from '../constants/base'
import { Pubkey } from './base'
import { EventId } from './event'
import { EventId, Pubkey } from './base'
export type SubscriptionId = string

View File

@ -11,4 +11,5 @@ export enum EventKinds {
export enum EventTags {
Event = 'e',
Pubkey = 'p',
Multicast = 'm',
}

View File

@ -6,9 +6,10 @@ import { SubscribeMessage } from '../@types/messages'
import { IWebSocketAdapter } from '../@types/adapters'
import { IEventRepository } from '../@types/repositories'
import { SubscriptionId, SubscriptionFilter } from '../@types/subscription'
import { toNostrEvent } from '../utils/event'
import { streamEach, streamEnd, streamMap } from '../utils/stream'
import { isEventMatchingFilter, toNostrEvent } from '../utils/event'
import { streamEach, streamEnd, streamFilter, streamMap } from '../utils/stream'
import { Event } from '../@types/event'
import { anyPass, map } from 'ramda'
export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
@ -39,6 +40,7 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
await pipeline(
findEvents,
streamMap(toNostrEvent),
streamFilter(anyPass(map(isEventMatchingFilter)(filters))),
streamEach(sendEvent),
streamEnd(sendEOSE), // NIP-15: End of Stored Events Notice
{

View File

@ -1,7 +1,8 @@
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, identity } from 'ramda'
import { EventId } from '../@types/base'
import { DBEvent, Event, EventId } from '../@types/event'
import { DBEvent, Event } from '../@types/event'
import { IEventRepository, IQueryResult } from '../@types/repositories'
import { SubscriptionFilter } from '../@types/subscription'
import { isGenericTagQuery } from '../utils/filter'
@ -83,7 +84,6 @@ export class EventRepository implements IEventRepository {
const andWhereRaw = invoker(1, 'andWhereRaw')
const orWhereRaw = invoker(2, 'orWhereRaw')
pipe(
toPairs,
filter(pipe(nth(0), isGenericTagQuery)) as any,

View File

@ -3,7 +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 { EventKinds, EventTags } from '../constants/base'
import { isGenericTagQuery } from './filter'
import { fromBuffer } from './transform'
@ -29,6 +29,8 @@ export const toNostrEvent = applySpec({
export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => {
const startsWith = (input: string) => (prefix) => input.startsWith(prefix)
// NIP-01: Basic protocol flow description
if (Array.isArray(filter.ids) && (
!filter.ids.some(startsWith(event.id))
)) {
@ -54,6 +56,18 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve
return false
}
// NIP-27: Multicast
const targetMulticastGroups: string[] = event.tags.reduce(
(acc, tag) => (tag[0] === EventTags.Multicast)
? [...acc, tag[1]]
: acc,
[] as string[]
)
if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) {
return false
}
// NIP-01: Support #e and #p tags
// NIP-12: Support generic tag queries

View File

@ -15,6 +15,17 @@ export const streamEach = (writeFn: (chunk: any) => void) => new PassThrough({
},
})
export const streamFilter = (predicate: (chunk: any) => boolean) => new Transform({
objectMode: true,
transform(chunk, _encoding, callback) {
if (predicate(chunk)) {
return callback(null, chunk)
}
callback()
},
})
export const streamEnd = (finalFn: () => void) => new PassThrough({
objectMode: true,
final(callback) {

View File

@ -76,6 +76,7 @@ describe('NIP-01', () => {
it('returns true if ids with prefix matches event', () => {
const event: Event = {
id: '7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96',
tags: [],
} as any
const prefix = '7377fa81fc6c'
@ -86,6 +87,7 @@ describe('NIP-01', () => {
it('returns false if ids with prefix does not matches event', () => {
const event: Event = {
id: '7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96',
tags: [],
} as any
const prefix = '001122'
@ -112,6 +114,7 @@ describe('NIP-01', () => {
it('returns true if authors with prefix matches event', () => {
const event: Event = {
pubkey: '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793',
tags: [],
} as any
const prefix = '22e804d'
@ -122,6 +125,7 @@ describe('NIP-01', () => {
it('returns false if authors with prefix does not matches event', () => {
const event: Event = {
pubkey: '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793',
tags: [],
} as any
const prefix = '001122'
@ -330,3 +334,56 @@ describe('isNullEvent', () => {
expect(isNullEvent({ kind: Number.MAX_SAFE_INTEGER - 1 } as any)).to.be.false
})
})
describe('NIP-27', () => {
describe('isEventMatchingFilter', () => {
describe('#m filter', () => {
let event: Event
beforeEach(() => {
event = {
tags: [
[
'm',
'group',
],
],
} as any
})
it('returns true given non-multicast event and there is no #m filter', () => {
event.tags = []
expect(isEventMatchingFilter({})(event)).to.be.true
})
it('returns true given multicast event and contained in #m filter', () => {
expect(isEventMatchingFilter({ '#m': ['group'] })(event)).to.be.true
})
it('returns true given multicast event and contained second in #m filter', () => {
expect(isEventMatchingFilter({ '#m': ['some group', 'group'] })(event)).to.be.true
})
it('returns false given multicast event and not contained in #m filter', () => {
expect(isEventMatchingFilter({ '#m': ['other group'] })(event)).to.be.false
})
it('returns false if given multicast event and there is no #m filter', () => {
expect(isEventMatchingFilter({})(event)).to.be.false
})
it('returns false if given multicast event and #m filter is empty', () => {
expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false
})
it('returns false given non-multicast event and filter contains some group', () => {
event.tags = []
expect(isEventMatchingFilter({ '#m': ['group'] })(event)).to.be.false
})
it('returns false given non-multicast event and filter is empty', () => {
event.tags = []
expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false
})
})
})
})