mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 21:31:48 +01:00
feat: support nip-26 delegated event signing
This commit is contained in:
parent
b02c76c6d0
commit
262e00ad53
39
.eslintrc.js
39
.eslintrc.js
@ -1,21 +1,36 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ["@typescript-eslint/eslint-plugin"],
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "dist", "tslint.json", "node_modules"],
|
||||
ignorePatterns: ['dist', 'tslint.json', 'node_modules'],
|
||||
rules: {
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
"no-console": "off",
|
||||
semi: ["error", "never"],
|
||||
quotes: ["error", "single", { avoidEscape: true }]
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'no-console': 'off',
|
||||
semi: ['error', 'never'],
|
||||
quotes: ['error', 'single', { avoidEscape: true }],
|
||||
'sort-imports': ['error', {
|
||||
ignoreCase: true,
|
||||
allowSeparatedGroups: true,
|
||||
}],
|
||||
curly: [2, 'multi-line'],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 120,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreRegExpLiterals: true,
|
||||
},
|
||||
],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
extension: ['ts'],
|
||||
require: ['ts-node/register', 'source-map-support/register'],
|
||||
require: ['ts-node/register'],
|
||||
reporter: 'mochawesome',
|
||||
slow: 75,
|
||||
sorted: true,
|
||||
|
@ -20,7 +20,8 @@ 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)
|
||||
- [x] NIP-26: Delegated Event Signing
|
||||
- [ ] NIP-27: Restricted Events (Experimental)
|
||||
|
||||
## Requirements
|
||||
|
||||
|
12
migrations/20220825_204900_add_delegator_to_events_table.js
Normal file
12
migrations/20220825_204900_add_delegator_to_events_table.js
Normal file
@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
// NIP-26: Delegated Event Signing
|
||||
return knex.schema.alterTable('events', function (table) {
|
||||
table.binary('event_delegator').nullable().index()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable('events', function (table) {
|
||||
table.dropColumn('event_delegator')
|
||||
})
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from 'node:stream'
|
||||
import { WebSocket } from 'ws'
|
||||
|
||||
import { Event } from './event'
|
||||
import { OutgoingMessage } from './messages'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EventKinds } from '../constants/base'
|
||||
import { EventDelegatorMetadataKey, EventKinds } from '../constants/base'
|
||||
import { EventId, Pubkey, Tag } from './base'
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ export interface Event {
|
||||
tags: Tag[]
|
||||
sig: string
|
||||
content: string
|
||||
[EventDelegatorMetadataKey]?: Pubkey
|
||||
}
|
||||
|
||||
export interface DBEvent {
|
||||
@ -21,6 +22,7 @@ export interface DBEvent {
|
||||
event_content: string
|
||||
event_tags: Tag[]
|
||||
event_signature: Buffer
|
||||
event_delegator?: Buffer | null
|
||||
first_seen: Date
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Range } from './base'
|
||||
import { SubscriptionFilter, SubscriptionId } from './subscription'
|
||||
import { Event } from './event'
|
||||
import { SubscriptionId, SubscriptionFilter } from './subscription'
|
||||
import { Range } from './base'
|
||||
|
||||
export enum MessageType {
|
||||
REQ = 'REQ',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PassThrough } from 'stream'
|
||||
import { EventId, Pubkey } from './base'
|
||||
|
||||
import { DBEvent, Event } from './event'
|
||||
import { EventId, Pubkey } from './base'
|
||||
import { SubscriptionFilter } from './subscription'
|
||||
|
||||
export type ExposedPromiseKeys = 'then' | 'catch' | 'finally'
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { EventKinds } from '../constants/base'
|
||||
import { EventId, Pubkey } from './base'
|
||||
import { EventKinds } from '../constants/base'
|
||||
|
||||
export type SubscriptionId = string
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { IncomingMessage, Server, ServerResponse } from 'http'
|
||||
import { Duplex, EventEmitter } from 'stream'
|
||||
|
||||
import { IncomingMessage, Server, ServerResponse } from 'http'
|
||||
import packageJson from '../../package.json'
|
||||
import { Settings } from '../settings'
|
||||
|
||||
import { IWebServerAdapter } from '../@types/adapters'
|
||||
import { Settings } from '../utils/settings'
|
||||
|
||||
export class WebServerAdapter extends EventEmitter implements IWebServerAdapter {
|
||||
|
||||
@ -12,8 +12,8 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter
|
||||
) {
|
||||
super()
|
||||
this.webServer.on('request', this.onWebServerRequest.bind(this))
|
||||
this.webServer.on('clientError', this.onWebServerSocketError.bind(this))
|
||||
this.webServer.on('close', this.onClose.bind(this))
|
||||
.on('clientError', this.onWebServerSocketError.bind(this))
|
||||
.on('close', this.onClose.bind(this))
|
||||
}
|
||||
|
||||
public listen(port: number): void {
|
||||
@ -32,7 +32,7 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter
|
||||
description,
|
||||
pubkey,
|
||||
contact,
|
||||
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16],
|
||||
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 26],
|
||||
software: packageJson.repository.url,
|
||||
version: packageJson.version,
|
||||
}
|
||||
|
@ -1,23 +1,27 @@
|
||||
import { EventEmitter } from 'stream'
|
||||
import { IncomingMessage as IncomingHttpMessage } from 'http'
|
||||
import { WebSocket } from 'ws'
|
||||
|
||||
import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters'
|
||||
import { Factory } from '../@types/base'
|
||||
import { Event } from '../@types/event'
|
||||
import { IMessageHandler, IAbortable } from '../@types/message-handlers'
|
||||
import { IAbortable, IMessageHandler } from '../@types/message-handlers'
|
||||
import { IncomingMessage, OutgoingMessage } from '../@types/messages'
|
||||
import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters'
|
||||
import { SubscriptionFilter, SubscriptionId } from '../@types/subscription'
|
||||
import { createOutgoingEventMessage } from '../messages'
|
||||
import { messageSchema } from '../schemas/message-schema'
|
||||
import { isEventMatchingFilter } from '../utils/event'
|
||||
import { attemptValidation } from '../utils/validation'
|
||||
import { createOutgoingEventMessage } from '../utils/messages'
|
||||
import { Event } from '../@types/event'
|
||||
import { Factory } from '../@types/base'
|
||||
import { isEventMatchingFilter } from '../utils/event'
|
||||
import { messageSchema } from '../schemas/message-schema'
|
||||
|
||||
export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter {
|
||||
private id: string
|
||||
private clientAddress: string
|
||||
private alive: boolean
|
||||
private subscriptions: Map<SubscriptionId, Set<SubscriptionFilter>>
|
||||
|
||||
public constructor(
|
||||
private readonly client: WebSocket,
|
||||
private readonly request: IncomingHttpMessage,
|
||||
private readonly webSocketServer: IWebSocketServerAdapter,
|
||||
private readonly createMessageHandler: Factory<IMessageHandler, [IncomingMessage, IWebSocketAdapter]>,
|
||||
) {
|
||||
@ -25,11 +29,19 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
this.alive = true
|
||||
this.subscriptions = new Map()
|
||||
|
||||
this.id = Buffer.from(request.headers['sec-websocket-key'], 'base64').toString('hex')
|
||||
this.clientAddress = request.headers['x-forwarded-for'] as string
|
||||
|
||||
console.log('id', this.id, 'clientAddress', this.clientAddress)
|
||||
console.log('listener count:', this.client.listenerCount('close'))
|
||||
|
||||
this.client
|
||||
.on('message', this.onClientMessage.bind(this))
|
||||
.on('close', this.onClientClose.bind(this))
|
||||
.on('pong', this.onClientPong.bind(this))
|
||||
|
||||
console.log('+listener count:', this.client.listenerCount('close'))
|
||||
|
||||
this
|
||||
.on('heartbeat', this.onHeartbeat.bind(this))
|
||||
.on('subscribe', this.onSubscribed.bind(this))
|
||||
@ -60,6 +72,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
}
|
||||
|
||||
public sendMessage(message: OutgoingMessage): void {
|
||||
console.log('sending message', message)
|
||||
this.client.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
@ -86,12 +99,11 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
try {
|
||||
const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf-8')))
|
||||
|
||||
console.debug('message received:', message[0])
|
||||
|
||||
const messageHandler = this.createMessageHandler([message, this]) as IMessageHandler & IAbortable
|
||||
if (typeof messageHandler.abort === 'function') {
|
||||
abort = messageHandler.abort.bind(messageHandler)
|
||||
this.client.prependOnceListener('close', abort)
|
||||
console.log('+listener count:', this.client.listenerCount('close'))
|
||||
}
|
||||
|
||||
await messageHandler?.handleMessage(message)
|
||||
@ -107,6 +119,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
if (abort) {
|
||||
this.client.removeListener('close', abort)
|
||||
}
|
||||
console.log('-listener count:', this.client.listenerCount('close'))
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,5 +134,6 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
|
||||
this.removeAllListeners()
|
||||
this.client.removeAllListeners()
|
||||
console.log('-listener count:', this.client.listenerCount('close'))
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Server } from 'http'
|
||||
import { IncomingMessage, Server } from 'http'
|
||||
import WebSocket, { OPEN, WebSocketServer } from 'ws'
|
||||
|
||||
import { Event } from '../@types/event'
|
||||
import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters'
|
||||
import { WebServerAdapter } from './web-server-adapter'
|
||||
import { Event } from '../@types/event'
|
||||
import { Factory } from '../@types/base'
|
||||
import { propEq } from 'ramda'
|
||||
import { WebServerAdapter } from './web-server-adapter'
|
||||
|
||||
|
||||
const WSS_CLIENT_HEALTH_PROBE_INTERVAL = 30000
|
||||
@ -18,7 +18,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
|
||||
public constructor(
|
||||
webServer: Server,
|
||||
private readonly webSocketServer: WebSocketServer,
|
||||
private readonly createWebSocketAdapter: Factory<IWebSocketAdapter, [WebSocket, IWebSocketServerAdapter]>
|
||||
private readonly createWebSocketAdapter: Factory<
|
||||
IWebSocketAdapter,
|
||||
[WebSocket, IncomingMessage, IWebSocketServerAdapter]
|
||||
>
|
||||
) {
|
||||
super(webServer)
|
||||
|
||||
@ -49,10 +52,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
|
||||
})
|
||||
}
|
||||
|
||||
private onWebSocketServerConnection(client: WebSocket) {
|
||||
private onWebSocketServerConnection(client: WebSocket, req: IncomingMessage) {
|
||||
console.debug(`new client - ${this.getConnectedClients()} connected / ${this.webSocketServer.clients.size} total`)
|
||||
|
||||
this.webSocketsAdapters.set(client, this.createWebSocketAdapter([client, this]))
|
||||
this.webSocketsAdapters.set(client, this.createWebSocketAdapter([client, req, this]))
|
||||
}
|
||||
|
||||
private onWebSocketServerHeartbeat() {
|
||||
|
@ -11,5 +11,9 @@ export enum EventKinds {
|
||||
export enum EventTags {
|
||||
Event = 'e',
|
||||
Pubkey = 'p',
|
||||
Multicast = 'm',
|
||||
// Multicast = 'm',
|
||||
Delegation = 'delegation',
|
||||
}
|
||||
|
||||
export const EventDelegatorMetadataKey = Symbol('Delegator')
|
||||
|
||||
|
@ -1,28 +1,29 @@
|
||||
import { isDeleteEvent, isEphemeralEvent, isNullEvent, isReplaceableEvent } from '../utils/event'
|
||||
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
|
||||
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
|
||||
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
|
||||
import { Event } from '../@types/event'
|
||||
import { Factory } from '../@types/base'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { IEventStrategy } from '../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { NullEventStrategy } from '../handlers/event-strategies/null-event-strategy'
|
||||
import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy'
|
||||
import { Factory } from '../@types/base'
|
||||
import { Event } from '../@types/event'
|
||||
import { IEventStrategy } from '../@types/message-handlers'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { isDeleteEvent, isEphemeralEvent, isNullEvent, isReplaceableEvent } from '../utils/event'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
|
||||
|
||||
|
||||
export const eventStrategyFactory = (
|
||||
eventRepository: IEventRepository,
|
||||
): Factory<IEventStrategy<Event, Promise<boolean>>, [Event, IWebSocketAdapter]> => ([event, adapter]: [Event, IWebSocketAdapter]) => {
|
||||
if (isReplaceableEvent(event)) {
|
||||
return new ReplaceableEventStrategy(adapter, eventRepository)
|
||||
} else if (isEphemeralEvent(event)) {
|
||||
return new EphemeralEventStrategy(adapter)
|
||||
} else if (isNullEvent(event)) {
|
||||
return new NullEventStrategy()
|
||||
} else if (isDeleteEvent(event)) {
|
||||
return new DeleteEventStrategy(eventRepository)
|
||||
}
|
||||
): Factory<IEventStrategy<Event, Promise<boolean>>, [Event, IWebSocketAdapter]> =>
|
||||
([event, adapter]: [Event, IWebSocketAdapter]) => {
|
||||
if (isReplaceableEvent(event)) {
|
||||
return new ReplaceableEventStrategy(adapter, eventRepository)
|
||||
} else if (isEphemeralEvent(event)) {
|
||||
return new EphemeralEventStrategy(adapter)
|
||||
} else if (isNullEvent(event)) {
|
||||
return new NullEventStrategy()
|
||||
} else if (isDeleteEvent(event)) {
|
||||
return new DeleteEventStrategy(eventRepository)
|
||||
}
|
||||
|
||||
return new DefaultEventStrategy(adapter, eventRepository)
|
||||
}
|
||||
return new DefaultEventStrategy(adapter, eventRepository)
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { EventMessageHandler } from '../handlers/event-message-handler'
|
||||
import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler'
|
||||
import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler'
|
||||
import { IncomingMessage, MessageType } from '../@types/messages'
|
||||
import { EventMessageHandler } from '../handlers/event-message-handler'
|
||||
import { eventStrategyFactory } from './event-strategy-factory'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { eventStrategyFactory } from './event-strategy-factory'
|
||||
import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler'
|
||||
import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler'
|
||||
|
||||
|
||||
export const messageHandlerFactory = (
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { IncomingMessage } from 'http'
|
||||
import { WebSocket } from 'ws'
|
||||
import { IWebSocketServerAdapter } from '../@types/adapters'
|
||||
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { WebSocketAdapter } from '../adapters/web-socket-adapter'
|
||||
import { IWebSocketServerAdapter } from '../@types/adapters'
|
||||
import { messageHandlerFactory } from './message-handler-factory'
|
||||
import { WebSocketAdapter } from '../adapters/web-socket-adapter'
|
||||
|
||||
|
||||
export const webSocketAdapterFactory = (
|
||||
eventRepository: IEventRepository,
|
||||
) => ([client, webSocketServerAdapter]: [WebSocket, IWebSocketServerAdapter,]) =>
|
||||
) => ([client, request, webSocketServerAdapter]: [WebSocket, IncomingMessage, IWebSocketServerAdapter]) =>
|
||||
new WebSocketAdapter(
|
||||
client,
|
||||
request,
|
||||
webSocketServerAdapter,
|
||||
messageHandlerFactory(eventRepository)
|
||||
)
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { IMessageHandler, IEventStrategy } from '../@types/message-handlers'
|
||||
import { IncomingEventMessage } from '../@types/messages'
|
||||
import { EventDelegatorMetadataKey, EventTags } from '../constants/base'
|
||||
import { IEventStrategy, IMessageHandler } from '../@types/message-handlers'
|
||||
import { isDelegatedEvent, isDelegatedEventValid, isEventIdValid, isEventSignatureValid } from '../utils/event'
|
||||
import { Event } from '../@types/event'
|
||||
import { Factory } from '../@types/base'
|
||||
import { isEventIdValid, isEventSignatureValid } from '../utils/event'
|
||||
import { IncomingEventMessage } from '../@types/messages'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
|
||||
export class EventMessageHandler implements IMessageHandler {
|
||||
@ -19,6 +20,16 @@ export class EventMessageHandler implements IMessageHandler {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDelegatedEvent(event)) {
|
||||
if (await isDelegatedEventValid(event)) {
|
||||
const [, delegator] = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
|
||||
event[EventDelegatorMetadataKey] = delegator
|
||||
} else {
|
||||
console.warn(`Delegated event ${event.id} from ${event.pubkey} is not valid`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const strategy = this.strategyFactory([event, this.webSocket])
|
||||
|
||||
if (typeof strategy?.execute !== 'function') {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
import { Event } from '../../@types/event'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IEventRepository } from '../../@types/repositories'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
|
||||
|
||||
export class DefaultEventStrategy implements IEventStrategy<Event, Promise<boolean>> {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Event } from '../../@types/event'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IEventRepository } from '../../@types/repositories'
|
||||
import { EventTags } from '../../constants/base'
|
||||
import { IEventRepository } from '../../@types/repositories'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
|
||||
|
||||
export class DeleteEventStrategy implements IEventStrategy<Event, Promise<boolean>> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
import { Event } from '../../@types/event'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
|
||||
|
||||
export class EphemeralEventStrategy implements IEventStrategy<Event, Promise<boolean>> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Event } from '../../@types/event'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IEventRepository } from '../../@types/repositories'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { anyPass, map } from 'ramda'
|
||||
import { pipeline } from 'stream/promises'
|
||||
|
||||
import { createOutgoingEventMessage, createEndOfStoredEventsNoticeMessage } from '../messages'
|
||||
import { createEndOfStoredEventsNoticeMessage, createOutgoingEventMessage } from '../utils/messages'
|
||||
import { IAbortable, IMessageHandler } from '../@types/message-handlers'
|
||||
import { SubscribeMessage } from '../@types/messages'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { SubscriptionId, SubscriptionFilter } from '../@types/subscription'
|
||||
import { isEventMatchingFilter, toNostrEvent } from '../utils/event'
|
||||
import { streamEach, streamEnd, streamFilter, streamMap } from '../utils/stream'
|
||||
import { SubscriptionFilter, SubscriptionId } from '../@types/subscription'
|
||||
import { Event } from '../@types/event'
|
||||
import { anyPass, map } from 'ramda'
|
||||
import { IEventRepository } from '../@types/repositories'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { SubscribeMessage } from '../@types/messages'
|
||||
|
||||
|
||||
export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
|
||||
|
12
src/index.ts
12
src/index.ts
@ -1,10 +1,10 @@
|
||||
import * as http from 'http'
|
||||
import { WebSocketServer } from 'ws'
|
||||
|
||||
import { getDbClient } from './database/client'
|
||||
import { EventRepository } from './repositories/event-repository'
|
||||
import { WebSocketServerAdapter } from './adapters/web-socket-server-adapter'
|
||||
import { getDbClient } from './database/client'
|
||||
import { webSocketAdapterFactory } from './factories/websocket-adapter-factory'
|
||||
import { WebSocketServerAdapter } from './adapters/web-socket-server-adapter'
|
||||
|
||||
const server = http.createServer()
|
||||
const wss = new WebSocketServer({ server, maxPayload: 1024 * 1024 })
|
||||
@ -23,8 +23,12 @@ adapter.listen(port)
|
||||
process.on('SIGINT', async function () {
|
||||
console.log('\rCaught interrupt signal')
|
||||
wss.clients.forEach((client) => client.terminate())
|
||||
await new Promise((resolve, reject) => wss.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined)))
|
||||
await new Promise((resolve, reject) => server.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined)))
|
||||
await new Promise((resolve, reject) =>
|
||||
wss.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined))
|
||||
)
|
||||
await new Promise((resolve, reject) =>
|
||||
server.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined))
|
||||
)
|
||||
dbClient.destroy()
|
||||
process.exit()
|
||||
})
|
||||
|
@ -1,12 +1,39 @@
|
||||
import {
|
||||
__,
|
||||
always,
|
||||
applySpec,
|
||||
complement,
|
||||
cond,
|
||||
equals,
|
||||
evolve,
|
||||
filter,
|
||||
forEach,
|
||||
forEachObjIndexed,
|
||||
groupBy,
|
||||
identity,
|
||||
ifElse,
|
||||
invoker,
|
||||
is,
|
||||
isEmpty,
|
||||
isNil,
|
||||
modulo,
|
||||
nth,
|
||||
omit,
|
||||
pipe,
|
||||
prop,
|
||||
propSatisfies,
|
||||
T,
|
||||
toPairs,
|
||||
} from 'ramda'
|
||||
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 } from '../@types/event'
|
||||
import { IEventRepository, IQueryResult } from '../@types/repositories'
|
||||
import { SubscriptionFilter } from '../@types/subscription'
|
||||
import { isGenericTagQuery } from '../utils/filter'
|
||||
import { toBuffer, toJSON } from '../utils/transform'
|
||||
import { EventDelegatorMetadataKey } from '../constants/base'
|
||||
import { EventId } from '../@types/base'
|
||||
import { isGenericTagQuery } from '../utils/filter'
|
||||
import { SubscriptionFilter } from '../@types/subscription'
|
||||
|
||||
const even = pipe(modulo(__, 2), equals(0))
|
||||
|
||||
@ -31,7 +58,7 @@ export class EventRepository implements IEventRepository {
|
||||
const queries = filters.map((currentFilter) => {
|
||||
const builder = this.dbClient<DBEvent>('events')
|
||||
|
||||
forEachObjIndexed((tableField: string, filterName: string) => {
|
||||
forEachObjIndexed((tableFields: string[], filterName: string) => {
|
||||
builder.andWhere((bd) => {
|
||||
cond([
|
||||
[isEmpty, () => void bd.whereRaw('1 = 0')],
|
||||
@ -40,27 +67,38 @@ export class EventRepository implements IEventRepository {
|
||||
pipe(
|
||||
groupByLengthSpec,
|
||||
evolve({
|
||||
exact: (pubkeys: string[]) => void bd.whereIn(tableField, pubkeys.map(toBuffer)),
|
||||
even: forEach((prefix: string) => void bd.orWhereRaw(
|
||||
`substring("${tableField}" from 1 for ?) = ?`,
|
||||
[prefix.length >> 1, toBuffer(prefix)]
|
||||
)),
|
||||
odd: forEach((prefix: string) => void bd.orWhereRaw(
|
||||
`substring("${tableField}" from 1 for ?) BETWEEN ? AND ?`,
|
||||
[
|
||||
(prefix.length >> 1) + 1,
|
||||
`\\x${prefix}0`,
|
||||
`\\x${prefix}f`
|
||||
],
|
||||
)),
|
||||
exact: (pubkeys: string[]) =>
|
||||
tableFields.forEach((tableField) =>
|
||||
void bd.orWhereIn(tableField, pubkeys.map(toBuffer))
|
||||
),
|
||||
even: forEach((prefix: string) =>
|
||||
tableFields.forEach((tableField) =>
|
||||
void bd.orWhereRaw(
|
||||
`substring("${tableField}" from 1 for ?) = ?`,
|
||||
[prefix.length >> 1, toBuffer(prefix)]
|
||||
)
|
||||
)
|
||||
),
|
||||
odd: forEach((prefix: string) =>
|
||||
tableFields.forEach((tableField) =>
|
||||
void bd.orWhereRaw(
|
||||
`substring("${tableField}" from 1 for ?) BETWEEN ? AND ?`,
|
||||
[
|
||||
(prefix.length >> 1) + 1,
|
||||
`\\x${prefix}0`,
|
||||
`\\x${prefix}f`,
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
}),
|
||||
),
|
||||
],
|
||||
])(currentFilter[filterName] as string[])
|
||||
})
|
||||
})({
|
||||
authors: 'event_pubkey',
|
||||
ids: 'event_id',
|
||||
authors: ['event_pubkey', 'event_delegator'],
|
||||
ids: ['event_id'],
|
||||
})
|
||||
|
||||
if (Array.isArray(currentFilter.kinds)) {
|
||||
@ -95,7 +133,7 @@ export class EventRepository implements IEventRepository {
|
||||
forEach((criterion: string[]) => void orWhereRaw(
|
||||
'"event_tags" @> ?',
|
||||
[
|
||||
JSON.stringify([[filterName[1], criterion]]) as any
|
||||
JSON.stringify([[filterName[1], criterion]]) as any,
|
||||
],
|
||||
bd,
|
||||
)),
|
||||
@ -111,6 +149,7 @@ export class EventRepository implements IEventRepository {
|
||||
if (subqueries.length) {
|
||||
query.union(subqueries, true)
|
||||
}
|
||||
console.log(query.toString())
|
||||
|
||||
return query
|
||||
}
|
||||
@ -128,6 +167,11 @@ export class EventRepository implements IEventRepository {
|
||||
event_tags: pipe(prop('tags'), toJSON),
|
||||
event_content: prop('content'),
|
||||
event_signature: pipe(prop('sig'), toBuffer),
|
||||
event_delegator: ifElse(
|
||||
propSatisfies(is(String), EventDelegatorMetadataKey),
|
||||
pipe(prop(EventDelegatorMetadataKey as any), toBuffer),
|
||||
always(null),
|
||||
),
|
||||
})(event)
|
||||
|
||||
return this.dbClient('events')
|
||||
@ -148,6 +192,11 @@ export class EventRepository implements IEventRepository {
|
||||
event_tags: pipe(prop('tags'), toJSON),
|
||||
event_content: prop('content'),
|
||||
event_signature: pipe(prop('sig'), toBuffer),
|
||||
event_delegator: ifElse(
|
||||
propSatisfies(is(String), EventDelegatorMetadataKey),
|
||||
pipe(prop(EventDelegatorMetadataKey as any), toBuffer),
|
||||
always(null),
|
||||
),
|
||||
})(event)
|
||||
|
||||
return this.dbClient('events')
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Schema from 'joi'
|
||||
|
||||
import {
|
||||
idSchema,
|
||||
kindSchema,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import Schema from 'joi'
|
||||
import { MessageType } from '../@types/messages'
|
||||
import { subscriptionSchema } from './base-schema'
|
||||
|
||||
import { eventSchema } from './event-schema'
|
||||
import { filterSchema } from './filter-schema'
|
||||
import { MessageType } from '../@types/messages'
|
||||
import { subscriptionSchema } from './base-schema'
|
||||
|
||||
export const eventMessageSchema = Schema.array().ordered(
|
||||
Schema.string().valid('EVENT').required(),
|
||||
@ -22,10 +22,19 @@ export const closeMessageSchema = Schema.array().ordered(
|
||||
).label('CLOSE message')
|
||||
|
||||
export const messageSchema = Schema.alternatives()
|
||||
.conditional(Joi.ref('.'), {
|
||||
.conditional(Schema.ref('.'), {
|
||||
switch: [
|
||||
{ is: Joi.array().ordered(Joi.string().equal(MessageType.EVENT)).items(Joi.any()), then: eventMessageSchema },
|
||||
{ is: Joi.array().ordered(Joi.string().equal(MessageType.REQ)).items(Joi.any()), then: reqMessageSchema },
|
||||
{ is: Joi.array().ordered(Joi.string().equal(MessageType.CLOSE)).items(Joi.any()), then: closeMessageSchema },
|
||||
{
|
||||
is: Schema.array().ordered(Schema.string().equal(MessageType.EVENT)).items(Schema.any()),
|
||||
then: eventMessageSchema,
|
||||
},
|
||||
{
|
||||
is: Schema.array().ordered(Schema.string().equal(MessageType.REQ)).items(Schema.any()),
|
||||
then: reqMessageSchema,
|
||||
},
|
||||
{
|
||||
is: Schema.array().ordered(Schema.string().equal(MessageType.CLOSE)).items(Schema.any()),
|
||||
then: closeMessageSchema,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -1,11 +1,12 @@
|
||||
import * as secp256k1 from '@noble/secp256k1'
|
||||
import { applySpec, pipe, prop } from 'ramda'
|
||||
import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda'
|
||||
|
||||
import { CanonicalEvent, Event } from '../@types/event'
|
||||
import { SubscriptionFilter } from '../@types/subscription'
|
||||
import { EventKinds, EventTags } from '../constants/base'
|
||||
import { isGenericTagQuery } from './filter'
|
||||
import { fromBuffer } from './transform'
|
||||
import { isGenericTagQuery } from './filter'
|
||||
import { Rune } from './runes'
|
||||
import { SubscriptionFilter } from '../@types/subscription'
|
||||
|
||||
export const serializeEvent = (event: Partial<Event>): CanonicalEvent => [
|
||||
0,
|
||||
@ -41,13 +42,6 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(filter.authors) &&
|
||||
!filter.authors.some(startsWith(event.pubkey))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof filter.since === 'number' && event.created_at < filter.since) {
|
||||
return false
|
||||
}
|
||||
@ -56,18 +50,34 @@ 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 (Array.isArray(filter.authors)) {
|
||||
if (
|
||||
!filter.authors.some(startsWith(event.pubkey))
|
||||
) {
|
||||
if (isDelegatedEvent(event)) {
|
||||
const delegation = event.tags.find((tag) => tag[0] === EventTags.Delegation)
|
||||
|
||||
if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) {
|
||||
return false
|
||||
if (!filter.authors.some(startsWith(delegation[1]))) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
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
|
||||
|
||||
@ -89,6 +99,61 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve
|
||||
return true
|
||||
}
|
||||
|
||||
export const isDelegatedEvent = (event: Event): boolean => {
|
||||
return event.tags.some((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
|
||||
}
|
||||
|
||||
export const isDelegatedEventValid = async (event: Event): Promise<boolean> => {
|
||||
const delegation = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
|
||||
if (!delegation) {
|
||||
return false
|
||||
}
|
||||
|
||||
const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}`
|
||||
|
||||
const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag))
|
||||
|
||||
// Token generation to be decided:
|
||||
// const serializedDelegationTag = [
|
||||
// delegation[0], // 'delegation'
|
||||
// delegation[1], // <delegator>
|
||||
// event.pubkey, // <delegatee>
|
||||
// delegation[2], // <rules>
|
||||
// ]
|
||||
// const token = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializedDelegationTag)))
|
||||
|
||||
// Validate delegation signature
|
||||
const verification = await secp256k1.schnorr.verify(delegation[3], token, delegation[1])
|
||||
if (!verification) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate rune
|
||||
const runifiedEvent = (converge(
|
||||
curry(mergeLeft),
|
||||
[
|
||||
omit(['tags']),
|
||||
pipe(
|
||||
prop('tags') as any,
|
||||
reduceBy(
|
||||
(acc, tag) => ([...acc, tag[1]]),
|
||||
[],
|
||||
nth(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
) as any)(event)
|
||||
|
||||
try {
|
||||
const [result] = Rune.from(delegation[2]).test(runifiedEvent)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Invalid rune')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isEventIdValid = async (event: Event): Promise<boolean> => {
|
||||
const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event))))
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Event } from './@types/event'
|
||||
import { SubscriptionId } from './@types/subscription'
|
||||
import {
|
||||
EndOfStoredEventsNotice,
|
||||
MessageType,
|
||||
Notice,
|
||||
OutgoingMessage,
|
||||
} from './@types/messages'
|
||||
} from '../@types/messages'
|
||||
import { Event } from '../@types/event'
|
||||
import { SubscriptionId } from '../@types/subscription'
|
||||
|
||||
export const createNotice = (notice: string): Notice => {
|
||||
return [MessageType.NOTICE, notice]
|
242
src/utils/runes.ts
Normal file
242
src/utils/runes.ts
Normal file
@ -0,0 +1,242 @@
|
||||
|
||||
const punctuations = /[!"#\$%&'()*+-.\/:;<=>?@\[\\\]^`{|}~]/
|
||||
|
||||
const hasPunctuation = (input) => punctuations.test(input)
|
||||
|
||||
// Reference: https://github.com/rustyrussell/runes/blob/master/runes/runes.py
|
||||
|
||||
export class Alternative {
|
||||
public constructor(
|
||||
private readonly field: string,
|
||||
private readonly cond: string,
|
||||
private readonly value: string,
|
||||
) {
|
||||
if (Array.from(this.field).some(hasPunctuation)) {
|
||||
throw Error('Field is not valid')
|
||||
}
|
||||
|
||||
if (!new Set(['!', '=', '/', '^', '$', '~', '<', '>', '}', '{', '#']).has(this.cond)) {
|
||||
throw new Error('Cond not valid')
|
||||
}
|
||||
}
|
||||
|
||||
public test(values: Record<string, any>): string | undefined {
|
||||
if (this.cond === '#') {
|
||||
return
|
||||
}
|
||||
|
||||
const why = (cond: boolean, field: string, explanation: string): string | undefined =>
|
||||
(cond) ? undefined : `${field}: ${explanation}`
|
||||
|
||||
if (!(this.field in values)) {
|
||||
return why(this.cond === '!', this.field, 'is missing')
|
||||
}
|
||||
|
||||
if (typeof values[this.field] === 'function') {
|
||||
return values[this.field](this)
|
||||
}
|
||||
|
||||
const val = String(values[this.field])
|
||||
|
||||
switch (this.cond) {
|
||||
case '!':
|
||||
return why(false, this.field, 'is present')
|
||||
case '=':
|
||||
return why(val === this.value, this.field, `!= ${this.value}`)
|
||||
case '/':
|
||||
return why(val !== this.value, this.field, `= ${this.value}`)
|
||||
case '^':
|
||||
return why(val.startsWith(this.value), this.field, `does not start with ${this.value}`)
|
||||
case '$':
|
||||
return why(val.endsWith(this.value), this.field, `does not end with ${this.value}`)
|
||||
case '~':
|
||||
return why(values[this.field].includes(this.value), this.field, `does not contain ${this.value}`)
|
||||
case '<':
|
||||
case '>':
|
||||
const actualInt = Number.parseInt(val)
|
||||
if (Number.isNaN(actualInt)) {
|
||||
return why(false, this.field, 'not an integer field')
|
||||
}
|
||||
const restrictionVal = Number.parseInt(this.value)
|
||||
if (Number.isNaN(restrictionVal)) {
|
||||
return why(false, this.field, 'not a valid integer')
|
||||
}
|
||||
|
||||
if (this.cond === '<') {
|
||||
return why(actualInt < restrictionVal, this.field, `>= ${restrictionVal}`)
|
||||
} else {
|
||||
return why(actualInt > restrictionVal, this.field, `<= ${restrictionVal}`)
|
||||
}
|
||||
case '{':
|
||||
return why(val < this.value, this.field, `is the same or ordered after ${this.value}`)
|
||||
case '{':
|
||||
return why(val > this.value, this.field, `is the same or ordered before ${this.value}`)
|
||||
default:
|
||||
throw new Error('Invalid condition')
|
||||
}
|
||||
}
|
||||
|
||||
public encode(): string {
|
||||
return `${this.field}${this.cond}${this.value.replace(/[\\|&]/g, '\\$&')}`
|
||||
}
|
||||
|
||||
public valueOf(): string {
|
||||
return this.encode()
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.encode()
|
||||
}
|
||||
|
||||
public static decode(encodedStr: string): [Alternative, string] {
|
||||
let cond = undefined
|
||||
let endOff = 0
|
||||
|
||||
while (endOff < encodedStr.length) {
|
||||
if (hasPunctuation(encodedStr[endOff])) {
|
||||
cond = encodedStr[endOff]
|
||||
break
|
||||
}
|
||||
endOff++
|
||||
}
|
||||
|
||||
if (typeof cond === 'undefined') {
|
||||
throw new Error(`${encodedStr} does not contain any operator`)
|
||||
}
|
||||
|
||||
const field = encodedStr.slice(0, endOff++)
|
||||
|
||||
let value = ''
|
||||
|
||||
while (endOff < encodedStr.length) {
|
||||
if (encodedStr[endOff] === '|') {
|
||||
endOff++
|
||||
break
|
||||
}
|
||||
|
||||
if (encodedStr[endOff] === '&') {
|
||||
break
|
||||
}
|
||||
|
||||
if (encodedStr[endOff] === '\\') {
|
||||
endOff++
|
||||
}
|
||||
|
||||
value += encodedStr[endOff++]
|
||||
}
|
||||
|
||||
return [new Alternative(field, cond, value), encodedStr.slice(endOff)]
|
||||
}
|
||||
|
||||
public static from(encodedStr: string): Alternative {
|
||||
const [field, cond, value] = encodedStr.replace(/\s+/g, '').split(new RegExp(`(${punctuations.source})`, 'g'))
|
||||
|
||||
return new Alternative(field, cond, value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Restriction {
|
||||
public constructor(
|
||||
private readonly alternatives: Alternative[]
|
||||
) {
|
||||
if (!alternatives.length) {
|
||||
throw new Error('Restriction must have some alternatives')
|
||||
}
|
||||
}
|
||||
|
||||
public test(values: Record<string, any>): string | undefined {
|
||||
const reasons: string[] = []
|
||||
for (const alternative of this.alternatives) {
|
||||
const reason = alternative.test(values)
|
||||
if (typeof reason === 'undefined') {
|
||||
return
|
||||
}
|
||||
reasons.push(reason)
|
||||
}
|
||||
|
||||
return reasons.join(' AND ')
|
||||
}
|
||||
|
||||
public encode(): string {
|
||||
return this.alternatives.map((alternative) => alternative.encode()).join('|')
|
||||
}
|
||||
|
||||
public valueOf(): string {
|
||||
return this.encode()
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.encode()
|
||||
}
|
||||
|
||||
public static decode(encodedStr: string): [Restriction, string] {
|
||||
let encStr = encodedStr
|
||||
let alternative: Alternative
|
||||
const alternatives: Alternative[] = []
|
||||
while (encStr.length) {
|
||||
if (encStr.startsWith('&')) {
|
||||
encStr = encStr.slice(1)
|
||||
break
|
||||
}
|
||||
|
||||
[alternative, encStr] = Alternative.decode(encStr)
|
||||
|
||||
alternatives.push(alternative)
|
||||
}
|
||||
|
||||
return [new Restriction(alternatives), encStr]
|
||||
}
|
||||
|
||||
public static from(encodedStr: string): Restriction {
|
||||
const [restriction, remainder] = Restriction.decode(encodedStr.replace(/\s+/g, ''))
|
||||
|
||||
if (remainder.length) {
|
||||
throw new Error(`Restriction had extra characters at end: ${remainder}`)
|
||||
}
|
||||
|
||||
return restriction
|
||||
}
|
||||
}
|
||||
|
||||
export class Rune {
|
||||
public constructor(
|
||||
private readonly restrictions: Restriction[] = []
|
||||
) { }
|
||||
|
||||
public test(values: Record<string, string | string[]>): [boolean, string] {
|
||||
for (const restriction of this.restrictions) {
|
||||
const reasons = restriction.test(values)
|
||||
if (typeof reasons !== 'undefined') {
|
||||
return [false, reasons]
|
||||
}
|
||||
}
|
||||
|
||||
return [true, '']
|
||||
}
|
||||
|
||||
public encode() {
|
||||
return this.restrictions.map((restriction) => restriction.encode()).join('&')
|
||||
}
|
||||
|
||||
public valueOf() {
|
||||
return this.encode()
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.encode()
|
||||
}
|
||||
|
||||
public static from(encodedStr: string): Rune {
|
||||
const restrictions: Restriction[] = []
|
||||
let restriction: Restriction
|
||||
let encStr = encodedStr.replace(/\s+/g, '')
|
||||
|
||||
while (encStr.length) {
|
||||
[restriction, encStr] = Restriction.decode(encStr)
|
||||
restrictions.push(restriction)
|
||||
}
|
||||
|
||||
return new Rune(restrictions)
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { mergeDeepRight } from 'ramda'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import packageJson from '../package.json'
|
||||
import { ISettings } from './@types/settings'
|
||||
import { ISettings } from '../@types/settings'
|
||||
import packageJson from '../../package.json'
|
||||
|
||||
let _settings: ISettings
|
||||
|
@ -4,7 +4,7 @@ export const streamMap = (fn: (chunk) => any) => new Transform({
|
||||
objectMode: true,
|
||||
transform(chunk, _encoding, callback) {
|
||||
callback(null, fn(chunk))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const streamEach = (writeFn: (chunk: any) => void) => new PassThrough({
|
||||
|
@ -8,4 +8,5 @@ const getValidationConfig = () => ({
|
||||
|
||||
export const validateSchema = (schema: Joi.Schema) => (input: any) => schema.validate(input, getValidationConfig())
|
||||
|
||||
export const attemptValidation = (schema: Joi.Schema) => (input: any) => Joi.attempt(input, schema, getValidationConfig())
|
||||
export const attemptValidation = (schema: Joi.Schema) =>
|
||||
(input: any) => Joi.attempt(input, schema, getValidationConfig())
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
||||
import * as chai from 'chai'
|
||||
import knex, { Knex } from 'knex'
|
||||
import * as sinon from 'sinon'
|
||||
import knex, { Knex } from 'knex'
|
||||
import sinonChai from 'sinon-chai'
|
||||
|
||||
import { Event } from '../../../src/@types/event'
|
||||
import { IEventRepository } from '../../../src/@types/repositories'
|
||||
import { SubscriptionFilter } from '../../../src/@types/subscription'
|
||||
@ -21,7 +22,7 @@ describe('EventRepository', () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
dbClient = knex({
|
||||
client: 'pg'
|
||||
client: 'pg',
|
||||
})
|
||||
|
||||
repository = new EventRepository(dbClient)
|
||||
@ -70,7 +71,7 @@ describe('EventRepository', () => {
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
|
||||
expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc')
|
||||
expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\') or "event_delegator" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc')
|
||||
})
|
||||
|
||||
it('selects events by two authors', () => {
|
||||
@ -78,14 +79,14 @@ describe('EventRepository', () => {
|
||||
{
|
||||
authors: [
|
||||
'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793',
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
||||
]
|
||||
}
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
|
||||
expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc')
|
||||
expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\') or "event_delegator" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc')
|
||||
})
|
||||
|
||||
it('selects events by one author prefix (even length)', () => {
|
||||
@ -93,13 +94,13 @@ describe('EventRepository', () => {
|
||||
{
|
||||
authors: [
|
||||
'22e804',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
|
||||
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\') order by "event_created_at" asc')
|
||||
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_delegator" from 1 for 3) = X\'22e804\') order by "event_created_at" asc')
|
||||
})
|
||||
|
||||
it('selects events by one author prefix (odd length)', () => {
|
||||
@ -107,13 +108,13 @@ describe('EventRepository', () => {
|
||||
{
|
||||
authors: [
|
||||
'22e804f',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
|
||||
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc')
|
||||
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc')
|
||||
})
|
||||
|
||||
it('selects events by two author prefix (first even, second odd)', () => {
|
||||
@ -122,13 +123,13 @@ describe('EventRepository', () => {
|
||||
authors: [
|
||||
'22e804',
|
||||
'32e1827',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
|
||||
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\') order by "event_created_at" asc')
|
||||
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_delegator" from 1 for 3) = X\'22e804\' or substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\') order by "event_created_at" asc')
|
||||
})
|
||||
})
|
||||
|
||||
@ -154,9 +155,9 @@ describe('EventRepository', () => {
|
||||
{
|
||||
ids: [
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
]
|
||||
}
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
@ -169,8 +170,8 @@ describe('EventRepository', () => {
|
||||
{
|
||||
ids: [
|
||||
'abcd',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
@ -183,8 +184,8 @@ describe('EventRepository', () => {
|
||||
{
|
||||
ids: [
|
||||
'abc',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
@ -198,8 +199,8 @@ describe('EventRepository', () => {
|
||||
ids: [
|
||||
'abcdef',
|
||||
'abc',
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
@ -359,7 +360,7 @@ describe('EventRepository', () => {
|
||||
|
||||
const query = repository.findByFilters(filters).toString()
|
||||
|
||||
expect(query).to.equal('(select * from "events" where "event_kind" in (1)) union (select * from "events" where (substring("event_id" from 1 for 3) BETWEEN E\'\\\\xaaaaa0\' AND E\'\\\\xaaaaaf\') order by "event_created_at" asc) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc')
|
||||
expect(query).to.equal('(select * from "events" where "event_kind" in (1)) union (select * from "events" where (substring("event_id" from 1 for 3) BETWEEN E\'\\\\xaaaaa0\' AND E\'\\\\xaaaaaf\') order by "event_created_at" asc) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\' or substring("event_delegator" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -430,7 +431,7 @@ describe('EventRepository', () => {
|
||||
|
||||
const query = (repository as any).insert(event).toString()
|
||||
|
||||
expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\') on conflict do nothing')
|
||||
expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, NULL, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\') on conflict do nothing')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { assocPath, omit, range } from 'ramda'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { Event } from '../../../src/@types/event'
|
||||
import { eventSchema } from '../../../src/schemas/event-schema'
|
||||
import { validateSchema } from '../../../src/utils/validation'
|
||||
@ -18,33 +19,33 @@ describe('NIP-01', () => {
|
||||
'e',
|
||||
'c58e83bb744e4c29642db7a5c3bd1519516ad5c51f6ba5f90c451d03c1961210',
|
||||
'',
|
||||
'root'
|
||||
'root',
|
||||
],
|
||||
[
|
||||
'e',
|
||||
'd0d78967b734628cec7bdfa2321c71c1f1c48e211b4b54333c3b0e94e7e99166',
|
||||
'',
|
||||
'reply'
|
||||
'reply',
|
||||
],
|
||||
[
|
||||
'p',
|
||||
'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29'
|
||||
'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29',
|
||||
],
|
||||
[
|
||||
'p',
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245',
|
||||
],
|
||||
[
|
||||
'e',
|
||||
'6fed2aae1e4f7d8b535774e4f7061c10e2ff20df1ef047da09462c7937925cd5'
|
||||
'6fed2aae1e4f7d8b535774e4f7061c10e2ff20df1ef047da09462c7937925cd5',
|
||||
],
|
||||
[
|
||||
'p',
|
||||
'2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5'
|
||||
]
|
||||
'2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5',
|
||||
],
|
||||
],
|
||||
'content': '',
|
||||
'sig': '313a9b8cd68267a51da84e292c0937d1f3686c6757c4584f50fcedad2b13fad755e6226924f79880fb5aa9de95c04231a4823981513ac9e7092bad7488282a96'
|
||||
'sig': '313a9b8cd68267a51da84e292c0937d1f3686c6757c4584f50fcedad2b13fad755e6226924f79880fb5aa9de95c04231a4823981513ac9e7092bad7488282a96',
|
||||
}
|
||||
})
|
||||
|
||||
@ -57,66 +58,66 @@ describe('NIP-01', () => {
|
||||
|
||||
const cases = {
|
||||
id: [
|
||||
{ message: 'must be a string', transform: assocPath(['id'], null), },
|
||||
{ message: 'must only contain lowercase characters', transform: assocPath(['id'], 'F'.repeat(64)), },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['id'], 'not hex'), },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(65)), },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(63)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['id'], ''), },
|
||||
{ message: 'is required', transform: omit(['id']), },
|
||||
{ message: 'must be a string', transform: assocPath(['id'], null) },
|
||||
{ message: 'must only contain lowercase characters', transform: assocPath(['id'], 'F'.repeat(64)) },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['id'], 'not hex') },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(65)) },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(63)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['id'], '') },
|
||||
{ message: 'is required', transform: omit(['id']) },
|
||||
],
|
||||
pubkey: [
|
||||
{ message: 'must be a string', transform: assocPath(['pubkey'], null), },
|
||||
{ message: 'must only contain lowercase characters', transform: assocPath(['pubkey'], 'F'.repeat(64)), },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['pubkey'], 'not hex'), },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(65)), },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(63)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['pubkey'], ''), },
|
||||
{ message: 'is required', transform: omit(['pubkey']), },
|
||||
{ message: 'must be a string', transform: assocPath(['pubkey'], null) },
|
||||
{ message: 'must only contain lowercase characters', transform: assocPath(['pubkey'], 'F'.repeat(64)) },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['pubkey'], 'not hex') },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(65)) },
|
||||
{ message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(63)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['pubkey'], '') },
|
||||
{ message: 'is required', transform: omit(['pubkey']) },
|
||||
],
|
||||
created_at: [
|
||||
{ message: 'must be a number', transform: assocPath(['created_at'], null), },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['created_at'], -1), },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['created_at'], Math.PI), },
|
||||
{ message: 'is required', transform: omit(['created_at']), },
|
||||
{ message: 'must be a number', transform: assocPath(['created_at'], null) },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['created_at'], -1) },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['created_at'], Math.PI) },
|
||||
{ message: 'is required', transform: omit(['created_at']) },
|
||||
],
|
||||
kind: [
|
||||
{ message: 'must be a number', transform: assocPath(['kind'], null), },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['kind'], -1), },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['kind'], Math.PI), },
|
||||
{ message: 'is required', transform: omit(['kind']), },
|
||||
{ message: 'must be a number', transform: assocPath(['kind'], null) },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['kind'], -1) },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['kind'], Math.PI) },
|
||||
{ message: 'is required', transform: omit(['kind']) },
|
||||
],
|
||||
content: [
|
||||
{ message: 'must be a string', transform: assocPath(['content'], null), },
|
||||
{ message: 'length must be less than or equal to 65536 characters long', transform: assocPath(['content'], ' '.repeat(64 * 1024 + 1)), },
|
||||
{ message: 'is required', transform: omit(['content']), },
|
||||
{ message: 'must be a string', transform: assocPath(['content'], null) },
|
||||
{ message: 'length must be less than or equal to 65536 characters long', transform: assocPath(['content'], ' '.repeat(64 * 1024 + 1)) },
|
||||
{ message: 'is required', transform: omit(['content']) },
|
||||
],
|
||||
sig: [
|
||||
{ message: 'must be a string', transform: assocPath(['sig'], null), },
|
||||
{ message: 'must only contain lowercase characters', transform: assocPath(['sig'], 'F'.repeat(128)), },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['sig'], 'not hex'), },
|
||||
{ message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(129)), },
|
||||
{ message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(127)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['sig'], ''), },
|
||||
{ message: 'is required', transform: omit(['sig']), },
|
||||
{ message: 'must be a string', transform: assocPath(['sig'], null) },
|
||||
{ message: 'must only contain lowercase characters', transform: assocPath(['sig'], 'F'.repeat(128)) },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['sig'], 'not hex') },
|
||||
{ message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(129)) },
|
||||
{ message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(127)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['sig'], '') },
|
||||
{ message: 'is required', transform: omit(['sig']) },
|
||||
],
|
||||
tags: [
|
||||
{ message: 'must be an array', transform: assocPath(['tags'], null), },
|
||||
{ message: 'is required', transform: omit(['tags']), },
|
||||
{ message: 'must be an array', transform: assocPath(['tags'], null) },
|
||||
{ message: 'is required', transform: omit(['tags']) },
|
||||
{ message: 'must contain less than or equal to 500 items', transform: assocPath(['tags'], range(0, 501).map(() => (['x', 'x']))) },
|
||||
],
|
||||
tag: [
|
||||
{ message: 'must be an array', transform: assocPath(['tags', 0], null), },
|
||||
{ message: 'must be an array', transform: assocPath(['tags', 0], null) },
|
||||
{ message: 'must contain less than or equal to 10 items', transform: assocPath(['tags', 0], range(0, 11).map(() => 'x')) },
|
||||
],
|
||||
identifier: [
|
||||
{ message: 'must be a string', transform: assocPath(['tags', 0, 0], null), },
|
||||
{ message: 'length must be less than or equal to 255 characters long', transform: assocPath(['tags', 0, 0], ' '.repeat(256)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['tags', 0, 0], ''), },
|
||||
{ message: 'must be a string', transform: assocPath(['tags', 0, 0], null) },
|
||||
{ message: 'length must be less than or equal to 255 characters long', transform: assocPath(['tags', 0, 0], ' '.repeat(256)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['tags', 0, 0], '') },
|
||||
],
|
||||
value: [
|
||||
{ message: 'must be a string', transform: assocPath(['tags', 0, 1], null), },
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['tags', 0, 1], ' '.repeat(1024 + 1)), },
|
||||
{ message: 'must be a string', transform: assocPath(['tags', 0, 1], null) },
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['tags', 0, 1], ' '.repeat(1024 + 1)) },
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import { assocPath, range } from 'ramda'
|
||||
import { SubscriptionFilter } from '../../../src/@types/subscription'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { filterSchema } from '../../../src/schemas/filter-schema'
|
||||
import { SubscriptionFilter } from '../../../src/@types/subscription'
|
||||
import { validateSchema } from '../../../src/utils/validation'
|
||||
|
||||
describe('NIP-01', () => {
|
||||
@ -30,71 +31,71 @@ describe('NIP-01', () => {
|
||||
|
||||
const cases = {
|
||||
ids: [
|
||||
{ message: 'must be an array', transform: assocPath(['ids'], null), },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['ids'], range(0, 257).map(() => 'f')), },
|
||||
{ message: 'must be an array', transform: assocPath(['ids'], null) },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['ids'], range(0, 257).map(() => 'f')) },
|
||||
],
|
||||
prefixOrId: [
|
||||
{ message: 'length must be less than or equal to 64 characters long', transform: assocPath(['ids', 0], 'f'.repeat(65)), },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['ids', 0], 'not hex'), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['ids', 0], ''), },
|
||||
{ message: 'length must be less than or equal to 64 characters long', transform: assocPath(['ids', 0], 'f'.repeat(65)) },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['ids', 0], 'not hex') },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['ids', 0], '') },
|
||||
],
|
||||
authors: [
|
||||
{ message: 'must be an array', transform: assocPath(['authors'], null), },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['authors'], range(0, 257).map(() => 'f')), },
|
||||
{ message: 'must be an array', transform: assocPath(['authors'], null) },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['authors'], range(0, 257).map(() => 'f')) },
|
||||
],
|
||||
prefixOrAuthor: [
|
||||
{ message: 'length must be less than or equal to 64 characters long', transform: assocPath(['authors', 0], 'f'.repeat(65)), },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['authors', 0], 'not hex'), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['authors', 0], ''), },
|
||||
{ message: 'length must be less than or equal to 64 characters long', transform: assocPath(['authors', 0], 'f'.repeat(65)) },
|
||||
{ message: 'must only contain hexadecimal characters', transform: assocPath(['authors', 0], 'not hex') },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['authors', 0], '') },
|
||||
],
|
||||
kinds: [
|
||||
{ message: 'must be an array', transform: assocPath(['kinds'], null), },
|
||||
{ message: 'must contain less than or equal to 20 items', transform: assocPath(['kinds'], range(0, 21).map(() => 1)), },
|
||||
{ message: 'must be an array', transform: assocPath(['kinds'], null) },
|
||||
{ message: 'must contain less than or equal to 20 items', transform: assocPath(['kinds'], range(0, 21).map(() => 1)) },
|
||||
],
|
||||
kind: [
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['kinds', 0], -1), },
|
||||
{ message: 'must be a number', transform: assocPath(['kinds', 0], null), },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['kinds', 0], Math.PI), },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['kinds', 0], -1) },
|
||||
{ message: 'must be a number', transform: assocPath(['kinds', 0], null) },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['kinds', 0], Math.PI) },
|
||||
],
|
||||
since: [
|
||||
{ message: 'must be a number', transform: assocPath(['since'], null), },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['since'], -1), },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['since'], Math.PI), },
|
||||
{ message: 'must be a number', transform: assocPath(['since'], null) },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['since'], -1) },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['since'], Math.PI) },
|
||||
],
|
||||
until: [
|
||||
{ message: 'must be a number', transform: assocPath(['until'], null), },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['until'], -1), },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['until'], Math.PI), },
|
||||
{ message: 'must be a number', transform: assocPath(['until'], null) },
|
||||
{ message: 'must be greater than or equal to 0', transform: assocPath(['until'], -1) },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['until'], Math.PI) },
|
||||
],
|
||||
limit: [
|
||||
{ message: 'must be a number', transform: assocPath(['limit'], null), },
|
||||
{ message: 'must be greater than or equal to 1', transform: assocPath(['limit'], -1), },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['limit'], Math.PI), },
|
||||
{ message: 'must be less than or equal to 10000', transform: assocPath(['limit'], 10001), },
|
||||
{ message: 'must be a number', transform: assocPath(['limit'], null) },
|
||||
{ message: 'must be greater than or equal to 1', transform: assocPath(['limit'], -1) },
|
||||
{ message: 'must be a multiple of 1', transform: assocPath(['limit'], Math.PI) },
|
||||
{ message: 'must be less than or equal to 10000', transform: assocPath(['limit'], 10001) },
|
||||
],
|
||||
'#e': [
|
||||
{ message: 'must be an array', transform: assocPath(['#e'], null), },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#e'], range(0, 1024 + 1).map(() => 'f')), },
|
||||
{ message: 'must be an array', transform: assocPath(['#e'], null) },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#e'], range(0, 1024 + 1).map(() => 'f')) },
|
||||
],
|
||||
'#e[0]': [
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['#e', 0], ''), },
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['#e', 0], '') },
|
||||
],
|
||||
'#p': [
|
||||
{ message: 'must be an array', transform: assocPath(['#p'], null), },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#p'], range(0, 1024 + 1).map(() => 'f')), },
|
||||
{ message: 'must be an array', transform: assocPath(['#p'], null) },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#p'], range(0, 1024 + 1).map(() => 'f')) },
|
||||
],
|
||||
'#p[0]': [
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['#p', 0], ''), },
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['#p', 0], '') },
|
||||
],
|
||||
'#r': [
|
||||
{ message: 'must be an array', transform: assocPath(['#r'], null), },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#r'], range(0, 1024 + 1).map(() => 'f')), },
|
||||
{ message: 'must be an array', transform: assocPath(['#r'], null) },
|
||||
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#r'], range(0, 1024 + 1).map(() => 'f')) },
|
||||
],
|
||||
'#r[0]': [
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)), },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['#r', 0], ''), },
|
||||
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)) },
|
||||
{ message: 'is not allowed to be empty', transform: assocPath(['#r', 0], '') },
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { expect } from 'chai'
|
||||
import { range } from 'ramda'
|
||||
import { Event } from '../../../src/@types/event'
|
||||
|
||||
import { Event } from '../../../src/@types/event'
|
||||
import { getEvents } from '../data/events'
|
||||
import { IncomingMessage } from '../../../src/@types/messages'
|
||||
import { messageSchema } from '../../../src/schemas/message-schema'
|
||||
import { validateSchema } from '../../../src/utils/validation'
|
||||
import { getEvents } from '../data/events'
|
||||
|
||||
describe('NIP-01', () => {
|
||||
let message: IncomingMessage
|
||||
@ -20,7 +20,7 @@ describe('NIP-01', () => {
|
||||
events.forEach((event) => {
|
||||
message = [
|
||||
'EVENT',
|
||||
event
|
||||
event,
|
||||
] as any
|
||||
|
||||
const result = validateSchema(messageSchema)(message)
|
||||
|
@ -1,6 +1,17 @@
|
||||
import { expect } from 'chai'
|
||||
import { Event, CanonicalEvent } from '../../../src/@types/event'
|
||||
import { isEphemeralEvent, isEventIdValid, isEventMatchingFilter, isEventSignatureValid, isNullEvent, isReplaceableEvent, serializeEvent } from '../../../src/utils/event'
|
||||
|
||||
import { CanonicalEvent, Event } from '../../../src/@types/event'
|
||||
import {
|
||||
isDelegatedEvent,
|
||||
isDelegatedEventValid,
|
||||
isEphemeralEvent,
|
||||
isEventIdValid,
|
||||
isEventMatchingFilter,
|
||||
isEventSignatureValid,
|
||||
isNullEvent,
|
||||
isReplaceableEvent,
|
||||
serializeEvent,
|
||||
} from '../../../src/utils/event'
|
||||
import { EventKinds } from '../../../src/constants/base'
|
||||
|
||||
describe('NIP-01', () => {
|
||||
@ -226,7 +237,7 @@ describe('NIP-01', () => {
|
||||
'kind': 1,
|
||||
'tags': [],
|
||||
'content': 'learning terraform rn!',
|
||||
'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848'
|
||||
'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848',
|
||||
}
|
||||
})
|
||||
|
||||
@ -256,7 +267,7 @@ describe('NIP-01', () => {
|
||||
'kind': 1,
|
||||
'tags': [],
|
||||
'content': 'learning terraform rn!',
|
||||
'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848'
|
||||
'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848',
|
||||
}
|
||||
})
|
||||
|
||||
@ -335,55 +346,89 @@ describe('isNullEvent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-27', () => {
|
||||
describe('isEventMatchingFilter', () => {
|
||||
describe('#m filter', () => {
|
||||
let event: Event
|
||||
beforeEach(() => {
|
||||
event = {
|
||||
tags: [
|
||||
[
|
||||
'm',
|
||||
'group',
|
||||
],
|
||||
],
|
||||
} as any
|
||||
})
|
||||
// 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 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 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 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 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 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 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 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
|
||||
})
|
||||
// it('returns false given non-multicast event and filter is empty', () => {
|
||||
// event.tags = []
|
||||
// expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('NIP-26', () => {
|
||||
let event: Event
|
||||
beforeEach(() => {
|
||||
event = {
|
||||
'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
|
||||
'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
|
||||
'created_at': 1660896109,
|
||||
'kind': 1,
|
||||
'tags': [
|
||||
[
|
||||
'delegation',
|
||||
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
|
||||
'kind=1&created_at>1640995200',
|
||||
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1',
|
||||
],
|
||||
],
|
||||
'content': 'Hello world',
|
||||
'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6',
|
||||
}
|
||||
})
|
||||
|
||||
describe('isDelegatedEvent', () => {
|
||||
it('returns true if event contains delegation tag', () => {
|
||||
expect(isDelegatedEvent(event)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('isDelegatedEventValid', () => {
|
||||
it('resolves with true if delegated event is valid', async () => {
|
||||
expect(await isDelegatedEventValid(event)).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { fromBuffer, toBuffer, toJSON } from '../../../src/utils/transform'
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user