mirror of
https://github.com/Cameri/nostream.git
synced 2025-04-08 11:58:03 +02:00
feat: implement nip-27
This commit is contained in:
parent
5da7d3a157
commit
1222e49a24
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
||||
|
@ -11,4 +11,5 @@ export enum EventKinds {
|
||||
export enum EventTags {
|
||||
Event = 'e',
|
||||
Pubkey = 'p',
|
||||
Multicast = 'm',
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user