Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(deps): bump follow-redirects from 1.15.2 to 1.15.5 #357

Closed
Closed
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5bd1269
Modify for ipv6.nostr.wirednet.jp
imksoo May 14, 2023
0f4f522
Merge branch 'main' of https://github.com/Cameri/nostream
imksoo May 14, 2023
7ed31fa
Merge branch 'main' of https://github.com/Cameri/nostream
imksoo Dec 11, 2023
7197159
Add event_tags table and update event-repository.ts
imksoo Dec 11, 2023
0ec2a4b
Update docker-compose.yml with new PostgreSQL and pgAdmin versions
imksoo Dec 11, 2023
c34d148
Remove foreign key constraint on event_id in event_tags table
imksoo Dec 11, 2023
4d13107
Fix tag name length check in insertTags method
imksoo Dec 11, 2023
8bd68e5
Update log_min_duration_statement in postgresql.conf
imksoo Dec 11, 2023
79bebc9
Refactor event tag insertion in EventRepository
imksoo Dec 12, 2023
e81bf71
Add PostgreSQL trigger for event tags
imksoo Dec 12, 2023
6824c0c
Add batch processing for event updates
imksoo Dec 12, 2023
9926dad
Refactor event tag processing logic
imksoo Dec 12, 2023
8b68de4
Add index on event_id in event_tags table
imksoo Dec 13, 2023
3bfcaed
Update PostgreSQL trigger and cursor query
imksoo Dec 14, 2023
e1cf50e
Add process_event_tags_direct function to handle event tags
imksoo Dec 14, 2023
49acefe
Add shm_size to nostream-db service in docker-compose.yml
imksoo Dec 14, 2023
a5ec630
Add index for event_tags table and optimize loop in trigger function
imksoo Dec 14, 2023
4fbba33
change docker-compose.yml
imksoo Jan 8, 2024
cc7ba4b
Merge branch 'main' of https://github.com/Cameri/nostream
imksoo Jan 8, 2024
246eb61
Merge branch 'main' of https://github.com/Cameri/nostream
imksoo Jan 13, 2024
af4d295
Revert "Merge branch 'main' of https://github.com/Cameri/nostream"
imksoo Jan 14, 2024
e0663b4
chore(deps): bump follow-redirects from 1.15.2 to 1.15.5
dependabot[bot] Jan 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Revert "Merge branch 'main' of https://github.com/Cameri/nostream"
This reverts commit 246eb61, reversing
changes made to cc7ba4b.
imksoo committed Jan 14, 2024
commit af4d2953484e02eff98691523e05279de5bc53f2
3 changes: 0 additions & 3 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -112,6 +112,3 @@ Running `nostream` for the first time creates the settings file in `<project_roo
| limits.message.rateLimits[].period | Rate limit period in milliseconds. |
| limits.message.rateLimits[].rate | Maximum number of messages during period. |
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ NIPs with a relay-specific implementation are listed here.
- [x] NIP-16: Event Treatment
- [x] NIP-20: Command Results
- [x] NIP-22: Event `created_at` Limits
- [ ] NIP-26: Delegated Event Signing (REMOVED)
- [x] NIP-26: Delegated Event Signing
- [x] NIP-28: Public Chat
- [x] NIP-33: Parameterized Replaceable Events
- [x] NIP-40: Expiration Timestamp
18 changes: 0 additions & 18 deletions migrations/20240111204900_remove_delegator_from_events_table.js

This file was deleted.

1,790 changes: 606 additions & 1,184 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nostream",
"version": "2.0.0",
"version": "1.25.2",
"description": "A Nostr relay written in Typescript.",
"supportedNips": [
1,
@@ -13,6 +13,7 @@
16,
20,
22,
26,
28,
33,
40
@@ -72,7 +73,7 @@
"devDependencies": {
"@commitlint/cli": "17.2.0",
"@commitlint/config-conventional": "17.2.0",
"@cucumber/cucumber": "10.2.1",
"@cucumber/cucumber": "8.7.0",
"@cucumber/pretty-formatter": "1.0.0",
"@semantic-release/commit-analyzer": "9.0.2",
"@semantic-release/git": "10.0.1",
@@ -115,7 +116,7 @@
},
"dependencies": {
"@noble/secp256k1": "1.7.1",
"axios": "1.6.5",
"axios": "1.2.6",
"bech32": "2.0.0",
"body-parser": "1.20.1",
"debug": "4.3.4",
9 changes: 0 additions & 9 deletions resources/default-settings.yaml
Original file line number Diff line number Diff line change
@@ -56,15 +56,6 @@ limits:
- "::1"
- "10.10.10.1"
- "::ffff:10.10.10.1"
admissionCheck:
rateLimits:
- description: 30 admission checks/min or 1 check every 2 seconds
period: 60000
rate: 30
ipWhitelist:
- "::1"
- "10.10.10.1"
- "::ffff:10.10.10.1"
connection:
rateLimits:
- period: 1000
7 changes: 6 additions & 1 deletion src/@types/event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ContextMetadata, EventId, Pubkey, Tag } from './base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'

export interface BaseEvent {
id: EventId
@@ -21,6 +21,10 @@ export type UnsignedEvent = Omit<Event, 'sig'>

export type UnidentifiedEvent = Omit<UnsignedEvent, 'id'>

export interface DelegatedEvent extends Event {
[EventDelegatorMetadataKey]?: Pubkey
}

export interface ExpiringEvent extends Event {
[EventExpirationTimeMetadataKey]?: number
}
@@ -38,6 +42,7 @@ export interface DBEvent {
event_content: string
event_tags: Tag[]
event_signature: Buffer
event_delegator?: Buffer | null
event_deduplication?: string | null
first_seen: Date
deleted_at?: Date
1 change: 1 addition & 0 deletions src/@types/repositories.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ export interface IEventRepository {
create(event: Event): Promise<number>
upsert(event: Event): Promise<number>
findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]>
insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise<number>
deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise<number>
}

6 changes: 0 additions & 6 deletions src/@types/settings.ts
Original file line number Diff line number Diff line change
@@ -112,14 +112,8 @@ export interface InvoiceLimits {
ipWhitelist?: string[]
}

export interface AdmissionCheckLimits {
rateLimits: RateLimit[]
ipWhitelist?: string[]
}

export interface Limits {
invoice?: InvoiceLimits
admissionCheck?: AdmissionCheckLimits
connection?: ConnectionLimits
client?: ClientLimits
event?: EventLimits
2 changes: 1 addition & 1 deletion src/@types/subscription.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ export type SubscriptionId = string

export interface SubscriptionFilter {
ids?: EventId[]
kinds?: (EventKinds | number)[]
kinds?: EventKinds[]
since?: number
until?: number
authors?: Pubkey[]
2 changes: 2 additions & 0 deletions src/constants/base.ts
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ export enum EventTags {
Event = 'e',
Pubkey = 'p',
// Multicast = 'm',
Delegation = 'delegation',
Deduplication = 'd',
Expiration = 'expiration',
Invoice = 'bolt11',
@@ -48,6 +49,7 @@ export enum PaymentsProcessors {
LNBITS = 'lnbits',
}

export const EventDelegatorMetadataKey = Symbol('Delegator')
export const EventDeduplicationMetadataKey = Symbol('Deduplication')
export const ContextMetadataKey = Symbol('Context')
export const EventExpirationTimeMetadataKey = Symbol('Expiration')
70 changes: 0 additions & 70 deletions src/controllers/admission/get-admission-check-controller.ts

This file was deleted.

This file was deleted.

21 changes: 21 additions & 0 deletions src/factories/delegated-event-strategy-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { isDeleteEvent, isEphemeralEvent, isReplaceableEvent } from '../utils/event'
import { DefaultEventStrategy } from '../handlers/event-strategies/default-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'

export const delegatedEventStrategyFactory = (
eventRepository: IEventRepository,
): Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]> =>
([event, adapter]: [Event, IWebSocketAdapter]) => {
if (isEphemeralEvent(event)) {
return new EphemeralEventStrategy(adapter)
} else if (isReplaceableEvent(event) || isDeleteEvent(event)) {
return
}

return new DefaultEventStrategy(adapter, eventRepository)
}
15 changes: 14 additions & 1 deletion src/factories/message-handler-factory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { IEventRepository, IUserRepository } from '../@types/repositories'
import { IncomingMessage, MessageType } from '../@types/messages'
import { createSettings } from './settings-factory'
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
import { EventMessageHandler } from '../handlers/event-message-handler'
import { eventStrategyFactory } from './event-strategy-factory'
import { isDelegatedEvent } from '../utils/event'
import { IWebSocketAdapter } from '../@types/adapters'
import { slidingWindowRateLimiterFactory } from './rate-limiter-factory'
import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler'
@@ -15,6 +18,16 @@ export const messageHandlerFactory = (
switch (message[0]) {
case MessageType.EVENT:
{
if (isDelegatedEvent(message[1])) {
return new DelegatedEventMessageHandler(
adapter,
delegatedEventStrategyFactory(eventRepository),
userRepository,
createSettings,
slidingWindowRateLimiterFactory,
)
}

return new EventMessageHandler(
adapter,
eventStrategyFactory(eventRepository),
@@ -26,7 +39,7 @@ export const messageHandlerFactory = (
case MessageType.REQ:
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
case MessageType.CLOSE:
return new UnsubscribeMessageHandler(adapter)
return new UnsubscribeMessageHandler(adapter,)
default:
throw new Error(`Unknown message type: ${String(message[0]).substring(0, 64)}`)
}
75 changes: 75 additions & 0 deletions src/handlers/delegated-event-message-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { EventDelegatorMetadataKey, EventTags } from '../constants/base'
import { createCommandResult } from '../utils/messages'
import { createLogger } from '../factories/logger-factory'
import { DelegatedEvent } from '../@types/event'
import { EventMessageHandler } from './event-message-handler'
import { IMessageHandler } from '../@types/message-handlers'
import { IncomingEventMessage } from '../@types/messages'
import { isDelegatedEventValid } from '../utils/event'
import { WebSocketAdapterEvent } from '../constants/adapter'

const debug = createLogger('delegated-event-message-handler')

export class DelegatedEventMessageHandler extends EventMessageHandler implements IMessageHandler {
public async handleMessage(message: IncomingEventMessage): Promise<void> {
const [, event] = message

let reason = await this.isEventValid(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}

if (await this.isRateLimited(event)) {
debug('event %s rejected: rate-limited')
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'rate-limited: slow down'))
return
}

reason = this.canAcceptEvent(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}

reason = await this.isUserAdmitted(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}

const [, delegator] = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
const delegatedEvent: DelegatedEvent = {
...event,
[EventDelegatorMetadataKey]: delegator,
}

const strategy = this.strategyFactory([delegatedEvent, this.webSocket])

if (typeof strategy?.execute !== 'function') {
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: event not supported'))
return
}

try {
await strategy.execute(delegatedEvent)
} catch (error) {
console.error('error handling message', message, error)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: unable to process event'))
}
}

protected async isEventValid(event: DelegatedEvent): Promise<string | undefined> {
const reason = await super.isEventValid(event)
if (reason) {
return reason
}

if (!await isDelegatedEventValid(event)) {
return 'invalid: delegation verification failed'
}
}
}
8 changes: 7 additions & 1 deletion src/handlers/event-strategies/delete-event-strategy.ts
Original file line number Diff line number Diff line change
@@ -32,10 +32,16 @@ export class DeleteEventStrategy implements IEventStrategy<Event, Promise<void>>
)

if (eventIdsToDelete.length) {
await this.eventRepository.deleteByPubkeyAndIds(
const count = await this.eventRepository.deleteByPubkeyAndIds(
event.pubkey,
eventIdsToDelete
)
if (!count) {
await this.eventRepository.insertStubs(
event.pubkey,
eventIdsToDelete,
)
}
}

const count = await this.eventRepository.create(event)
40 changes: 0 additions & 40 deletions src/handlers/request-handlers/nodeinfo-handler.ts

This file was deleted.

7 changes: 1 addition & 6 deletions src/handlers/request-handlers/root-request-handler.ts
Original file line number Diff line number Diff line change
@@ -3,15 +3,14 @@ import { path } from 'ramda'

import { createSettings } from '../../factories/settings-factory'
import { FeeSchedule } from '../../@types/settings'
import { fromBech32 } from '../../utils/transform'
import packageJson from '../../../package.json'

export const rootRequestHandler = (request: Request, response: Response, next: NextFunction) => {
const settings = createSettings()

if (request.header('accept') === 'application/nostr+json') {
const {
info: { name, description, pubkey: rawPubkey, contact, relay_url },
info: { name, description, pubkey, contact, relay_url },
} = settings

const paymentsUrl = new URL(relay_url)
@@ -20,10 +19,6 @@ export const rootRequestHandler = (request: Request, response: Response, next: N

const content = settings.limits?.event?.content

const pubkey = rawPubkey.startsWith('npub1')
? fromBech32(rawPubkey)
: rawPubkey

const relayInformationDocument = {
name,
description,
46 changes: 41 additions & 5 deletions src/repositories/event-repository.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
forEach,
forEachObjIndexed,
groupBy,
identity,
ifElse,
invoker,
is,
@@ -28,8 +29,8 @@
toPairs,
} from 'ramda'

import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base'
import { DatabaseClient, EventId } from '../@types/base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base'
import { DatabaseClient, EventId, Tag } from '../@types/base'
import { DBEvent, Event } from '../@types/event'
import { IEventRepository, IQueryResult } from '../@types/repositories'
import { toBuffer, toJSON } from '../utils/transform'
@@ -107,8 +108,8 @@
])(currentFilter[filterName] as string[])
})
})({
authors: ['event_pubkey'],
ids: ['event_id'],
authors: ['event_pubkey', 'event_delegator'],
ids: ['events.event_id'],
})

if (Array.isArray(currentFilter.kinds)) {
@@ -182,6 +183,11 @@
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),
),
remote_address: path([ContextMetadataKey as any, 'remoteAddress', 'address']),
expires_at: ifElse(
propSatisfies(is(Number), EventExpirationTimeMetadataKey),
@@ -211,6 +217,11 @@
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_deduplication: ifElse(
propSatisfies(isNil, EventDeduplicationMetadataKey),
pipe(paths([['pubkey'], ['kind']]), toJSON),
@@ -222,7 +233,6 @@
prop(EventExpirationTimeMetadataKey as any),
always(null),
),
deleted_at: always(null),
})(event)

const query = this.masterDbClient('events')
@@ -237,8 +247,8 @@
.merge(omit(['event_pubkey', 'event_kind', 'event_deduplication'])(row))
.where('events.event_created_at', '<', row.event_created_at)

await this.removeEventTagsByEventId(event.id);

Check failure on line 250 in src/repositories/event-repository.ts

GitHub Actions / Lint code

Extra semicolon
await this.insertEventTags(event.id, event.tags);

Check failure on line 251 in src/repositories/event-repository.ts

GitHub Actions / Lint code

Extra semicolon

return {
then: <T1, T2>(onfulfilled: (value: number) => T1 | PromiseLike<T1>, onrejected: (reason: any) => T2 | PromiseLike<T2>) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected),
@@ -247,10 +257,36 @@
} as Promise<number>
}

public async insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise<number> {
debug('inserting stubs for %s: %o', pubkey, eventIdsToDelete)
const date = new Date()
await this.removeEventTagsByEventIds(eventIdsToDelete);

Check failure on line 263 in src/repositories/event-repository.ts

GitHub Actions / Lint code

Extra semicolon

return this.masterDbClient('events').insert(
eventIdsToDelete.map(
applySpec({
event_id: pipe(identity, toBuffer),
event_pubkey: pipe(always(pubkey), toBuffer),
event_created_at: always(Math.floor(date.getTime() / 1000)),
event_kind: always(5),
event_tags: always('[]'),
event_content: always(''),
event_signature: pipe(always(''), toBuffer),
event_delegator: always(null),
event_deduplication: pipe(always([pubkey, 5]), toJSON),
expires_at: always(null),
deleted_at: always(date.toISOString()),
})
)
)
.onConflict()
.ignore() as Promise<any>
}

public async deleteByPubkeyAndIds(pubkey: string, eventIdsToDelete: EventId[]): Promise<number> {
debug('deleting events from %s: %o', pubkey, eventIdsToDelete)

await this.removeEventTagsByEventIds(eventIdsToDelete);

Check failure on line 289 in src/repositories/event-repository.ts

GitHub Actions / Lint code

Extra semicolon

return this.masterDbClient('events')
.where('event_pubkey', toBuffer(pubkey))
@@ -267,8 +303,8 @@
await this.masterDbClient('event_tags').insert({
event_id: event_id,
tag_name,
tag_value

Check failure on line 306 in src/repositories/event-repository.ts

GitHub Actions / Lint code

Missing trailing comma
});

Check failure on line 307 in src/repositories/event-repository.ts

GitHub Actions / Lint code

Extra semicolon
}
}
}
10 changes: 0 additions & 10 deletions src/routes/admissions/index.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import express from 'express'

import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler'
import admissionRouter from './admissions'
import callbacksRouter from './callbacks'
import { getHealthRequestHandler } from '../handlers/request-handlers/get-health-request-handler'
import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler'
@@ -15,12 +13,7 @@ router.get('/', rootRequestHandler)
router.get('/healthz', getHealthRequestHandler)
router.get('/terms', getTermsRequestHandler)

router.get('/.well-known/nodeinfo', nodeinfoHandler)
router.get('/nodeinfo/2.1', nodeinfo21Handler)
router.get('/nodeinfo/2.0', nodeinfo21Handler)

router.use('/invoices', rateLimiterMiddleware, invoiceRouter)
router.use('/admissions', rateLimiterMiddleware, admissionRouter)
router.use('/callbacks', rateLimiterMiddleware, callbacksRouter)

export default router
2 changes: 1 addition & 1 deletion src/routes/invoices/index.ts
Original file line number Diff line number Diff line change
@@ -12,4 +12,4 @@ invoiceRouter
.get('/:invoiceId/status', withController(createGetInvoiceStatusController))
.post('/', urlencoded({ extended: true }), withController(createPostInvoiceController))

export default invoiceRouter
export default invoiceRouter
61 changes: 59 additions & 2 deletions src/utils/event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event'
import { createCipheriv, getRandomValues } from 'crypto'
import { EventId, Pubkey, Tag } from '../@types/base'
@@ -12,6 +12,7 @@ import { EventKindsRange } from '../@types/settings'
import { fromBuffer } from './transform'
import { getLeadingZeroBits } from './proof-of-work'
import { isGenericTagQuery } from './filter'
import { RuneLike } from './runes/rune-like'
import { SubscriptionFilter } from '../@types/subscription'
import { WebSocketServerAdapterEvent } from '../constants/adapter'

@@ -67,7 +68,18 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve
if (
!filter.authors.some(startsWith(event.pubkey))
) {
return false
if (isDelegatedEvent(event)) {
const delegation = event.tags.find((tag) => tag[0] === EventTags.Delegation)
if (typeof delegation === 'undefined') {
return false
}

if (!filter.authors.some(startsWith(delegation[1]))) {
return false
}
} else {
return false
}
}
}

@@ -104,6 +116,51 @@ 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
}


// Validate rune
const runifiedEvent = (converge(
curry(mergeLeft),
[
omit(['tags']),
pipe(
prop('tags') as any,
reduceBy<EventTags, string[]>(
(acc, tag) => ([...acc, tag[1]]),
[],
nth(0) as any,
),
),
],
) as any)(event)

let result: boolean
try {
[result] = RuneLike.from(delegation[2]).test(runifiedEvent)
} catch (error) {
result = false
}

if (!result) {
return false
}

const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}`

const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag))

return secp256k1.schnorr.verify(delegation[3], token, delegation[1])
}

export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEvent): Promise<string> => {
const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event))))

6 changes: 5 additions & 1 deletion test/integration/features/nip-09/nip-09.feature
Original file line number Diff line number Diff line change
@@ -21,10 +21,14 @@ Feature: NIP-09
And Alice drafts a text_note event with content "Twitter > Nostr"
When Alice sends a delete event for their last event
And Alice sends their last draft event successfully
And Alice subscribes to author Alice
Then Alice receives 1 delete event from Alice and EOSE

Scenario: Alice sends a delete before deleted set_metadata
Given someone called Alice
And someone called Bob
And Alice drafts a set_metadata event
When Alice sends a delete event for their last event
Then Alice sends their last draft event successfully
And Alice sends their last draft event unsuccessfully
And Alice subscribes to author Alice
Then Alice receives 1 delete event from Alice and EOSE
21 changes: 0 additions & 21 deletions test/integration/features/nip-33/nip-33.feature
Original file line number Diff line number Diff line change
@@ -30,24 +30,3 @@ Feature: NIP-33 Parameterized replaceable events
And Alice sends a parameterized_replaceable_event_1 event with content "third" and tag d containing "friends"
And Bob subscribes to author Alice
Then Bob receives a parameterized_replaceable_event_1 event from Alice with content "third" and tag d containing "friends"

Scenario: Alice deletes a parameterized replaceable event
Given someone called Alice
When Alice sends a parameterized_replaceable_event_1 event with content "exercise" and tag d containing "2023-resolutions"
And Alice sends a delete event for their last event
And Alice subscribes to author Alice
Then Alice receives 1 delete event from Alice and EOSE

Scenario: Alice deletes and replaces a parameterized replaceable event
Given someone called Alice
And Alice sends a parameterized_replaceable_event_1 event with content "gym" and tag d containing "2024-resolutions"
And Alice sends a delete event for their last event
When Alice sends a parameterized_replaceable_event_1 event with content "exercise" and tag d containing "2024-resolutions"
And Alice subscribes to parameterized_replaceable_event_1 events from Alice
Then Alice receives a parameterized_replaceable_event_1 event from Alice with content "exercise" and tag d containing "2024-resolutions"

Scenario: Alice deletes before sending parameterized replaceable event
Given someone called Alice
And Alice drafts a parameterized_replaceable_event_2 event with content "don't worry about it" and tag d containing "topsycrets"
When Alice sends a delete event for their last event
And Alice sends their last draft event successfully
42 changes: 2 additions & 40 deletions test/integration/features/nip-33/nip-33.feature.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Then, When, World } from '@cucumber/cucumber'
import { Then, When } from '@cucumber/cucumber'
import { expect } from 'chai'
import WebSocket from 'ws'

import { createEvent, createSubscription, sendEvent, waitForEventCount, waitForNextEvent } from '../helpers'
import { createEvent, sendEvent, waitForEventCount, waitForNextEvent } from '../helpers'
import { EventKinds, EventTags } from '../../../../src/constants/base'
import { Event } from '../../../../src/@types/event'
import { isDraft } from '../shared'

When(/^(\w+) sends a parameterized_replaceable_event_0 event with content "([^"]+)" and tag (\w) containing "([^"]+)"$/, async function(
name: string,
@@ -109,40 +108,3 @@ Then(/(\w+) receives (\d+) parameterized_replaceable_event_0 events? from (\w+)
expect(events[0].pubkey).to.equal(this.parameters.identities[author].pubkey)
expect(events[0].content).to.equal(content)
})

When(/^(\w+) subscribes to parameterized_replaceable_event_1 events from (\w+)$/, async function (this: World<Record<string, any>>, name: string, author: string) {
const ws = this.parameters.clients[name] as WebSocket
const authorPubkey = this.parameters.identities[author].pubkey
const subscription = {
name: `test-${Math.random()}`,
filters: [
{ kinds: [EventKinds.PARAMETERIZED_REPLACEABLE_FIRST + 1], authors: [authorPubkey] },
],
}
this.parameters.subscriptions[name].push(subscription)

await createSubscription(ws, subscription.name, subscription.filters)
})


Then(/^(\w+) drafts a parameterized_replaceable_event_2 event with content "([^"]+?)" and tag (\w+) containing "([^"]+?)"$/, async function (
name: string,
content: string,
tagName: string,
tagValue: string,
) {
const { pubkey, privkey } = this.parameters.identities[name]

const event: Event = await createEvent({
pubkey,
kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST + 2,
content,
tags: [
[tagName, tagValue],
],
}, privkey)

event[isDraft] = true

this.parameters.events[name].push(event)
})
13 changes: 3 additions & 10 deletions test/integration/features/shared.ts
Original file line number Diff line number Diff line change
@@ -16,10 +16,8 @@ import Sinon from 'sinon'
import { connect, createIdentity, createSubscription, sendEvent } from './helpers'
import { getMasterDbClient, getReadReplicaDbClient } from '../../../src/database/client'
import { AppWorker } from '../../../src/app/worker'
import { CacheClient } from '../../../src/@types/cache'
import { DatabaseClient } from '../../../src/@types/base'
import { Event } from '../../../src/@types/event'
import { getCacheClient } from '../../../src/cache/client'
import { SettingsStatic } from '../../../src/utils/settings'
import { workerFactory } from '../../../src/factories/worker-factory'

@@ -29,7 +27,6 @@ let worker: AppWorker

let dbClient: DatabaseClient
let rrDbClient: DatabaseClient
let cacheClient: CacheClient

export const streams = new WeakMap<WebSocket, Observable<unknown>>()

@@ -38,9 +35,7 @@ BeforeAll({ timeout: 1000 }, async function () {
process.env.SECRET = Math.random().toString().repeat(6)
dbClient = getMasterDbClient()
rrDbClient = getReadReplicaDbClient()
cacheClient = getCacheClient()
await dbClient.raw('SELECT 1=1')
await rrDbClient.raw('SELECT 1=1')
Sinon.stub(SettingsStatic, 'watchSettings')
const settings = SettingsStatic.createSettings()

@@ -59,11 +54,7 @@ BeforeAll({ timeout: 1000 }, async function () {

AfterAll(async function() {
worker.close(async () => {
await Promise.all([
dbClient.destroy(),
rrDbClient.destroy(),
cacheClient.disconnect(),
])
await Promise.all([dbClient.destroy(), rrDbClient.destroy()])
})
})

@@ -84,6 +75,8 @@ After(async function () {
}
this.parameters.clients = {}

const dbClient = getMasterDbClient()

await dbClient('events')
.whereIn('event_pubkey', Object
.values(this.parameters.identities as Record<string, { pubkey: string }>)
46 changes: 46 additions & 0 deletions test/unit/factories/delegated-event-strategy-factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect } from 'chai'

import { DefaultEventStrategy } from '../../../src/handlers/event-strategies/default-event-strategy'
import { delegatedEventStrategyFactory } from '../../../src/factories/delegated-event-strategy-factory'
import { EphemeralEventStrategy } from '../../../src/handlers/event-strategies/ephemeral-event-strategy'
import { Event } from '../../../src/@types/event'
import { EventKinds } from '../../../src/constants/base'
import { Factory } from '../../../src/@types/base'
import { IEventRepository } from '../../../src/@types/repositories'
import { IEventStrategy } from '../../../src/@types/message-handlers'
import { IWebSocketAdapter } from '../../../src/@types/adapters'

describe('delegatedEventStrategyFactory', () => {
let eventRepository: IEventRepository
let event: Event
let adapter: IWebSocketAdapter
let factory: Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]>

beforeEach(() => {
eventRepository = {} as any
event = {} as any
adapter = {} as any

factory = delegatedEventStrategyFactory(eventRepository)
})

it('returns EphemeralEventStrategy given a set_metadata event', () => {
event.kind = EventKinds.EPHEMERAL_FIRST
expect(factory([event, adapter])).to.be.an.instanceOf(EphemeralEventStrategy)
})

it('returns DefaultEventStrategy given a text_note event', () => {
event.kind = EventKinds.TEXT_NOTE
expect(factory([event, adapter])).to.be.an.instanceOf(DefaultEventStrategy)
})

it('returns undefined given a replaceable event', () => {
event.kind = EventKinds.REPLACEABLE_FIRST
expect(factory([event, adapter])).to.be.undefined
})

it('returns undefined given a delete event', () => {
event.kind = EventKinds.DELETE
expect(factory([event, adapter])).to.be.undefined
})
})
14 changes: 14 additions & 0 deletions test/unit/factories/message-handler-factory.spec.ts
Original file line number Diff line number Diff line change
@@ -2,8 +2,10 @@ import { expect } from 'chai'

import { IEventRepository, IUserRepository } from '../../../src/@types/repositories'
import { IncomingMessage, MessageType } from '../../../src/@types/messages'
import { DelegatedEventMessageHandler } from '../../../src/handlers/delegated-event-message-handler'
import { Event } from '../../../src/@types/event'
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
import { EventTags } from '../../../src/constants/base'
import { IWebSocketAdapter } from '../../../src/@types/adapters'
import { messageHandlerFactory } from '../../../src/factories/message-handler-factory'
import { SubscribeMessageHandler } from '../../../src/handlers/subscribe-message-handler'
@@ -36,6 +38,18 @@ describe('messageHandlerFactory', () => {
expect(factory([message, adapter])).to.be.an.instanceOf(EventMessageHandler)
})

it('returns DelegatedEventMessageHandler when given an EVENT message with delegated event', () => {
event.tags = [
[EventTags.Delegation, '', '', ''],
]
message = [
MessageType.EVENT,
event,
]

expect(factory([message, adapter])).to.be.an.instanceOf(DelegatedEventMessageHandler)
})

it('returns SubscribeMessageHandler when given a REQ message', () => {
message = [
MessageType.REQ,
229 changes: 229 additions & 0 deletions test/unit/handlers/delegated-event-message-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import EventEmitter from 'events'
import Sinon from 'sinon'
import sinonChai from 'sinon-chai'

chai.use(sinonChai)
chai.use(chaiAsPromised)

import { IncomingEventMessage, MessageType } from '../../../src/@types/messages'
import { DelegatedEventMessageHandler } from '../../../src/handlers/delegated-event-message-handler'
import { Event } from '../../../src/@types/event'
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
import { EventTags } from '../../../src/constants/base'
import { IUserRepository } from '../../../src/@types/repositories'
import { WebSocketAdapterEvent } from '../../../src/constants/adapter'

const { expect } = chai

describe('DelegatedEventMessageHandler', () => {
let webSocket: EventEmitter
let handler: DelegatedEventMessageHandler
let userRepository: IUserRepository
let event: Event
let message: IncomingEventMessage
let sandbox: Sinon.SinonSandbox

let originalConsoleWarn: any = undefined

beforeEach(() => {
sandbox = Sinon.createSandbox()
originalConsoleWarn = console.warn
console.warn = () => undefined
event = {
content: 'hello',
created_at: 1665546189,
id: 'f'.repeat(64),
kind: 1,
pubkey: 'f'.repeat(64),
sig: 'f'.repeat(128),
tags: [
[EventTags.Delegation, 'delegator', 'rune', 'signature'],
],
}
})

afterEach(() => {
console.warn = originalConsoleWarn
sandbox.restore()
})

describe('handleMessage', () => {
let canAcceptEventStub: Sinon.SinonStub
let isEventValidStub: Sinon.SinonStub
let strategyFactoryStub: Sinon.SinonStub
let onMessageSpy: Sinon.SinonSpy
let strategyExecuteStub: Sinon.SinonStub
let isRateLimitedStub: Sinon.SinonStub
let isUserAdmitted: Sinon.SinonStub

beforeEach(() => {
canAcceptEventStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'canAcceptEvent' as any)
isEventValidStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'isEventValid' as any)
isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any)
strategyExecuteStub = sandbox.stub()
strategyFactoryStub = sandbox.stub().returns({
execute: strategyExecuteStub,
})
onMessageSpy = sandbox.fake.returns(undefined)
webSocket = new EventEmitter()
webSocket.on(WebSocketAdapterEvent.Message, onMessageSpy)
message = [MessageType.EVENT, event]
isRateLimitedStub = sandbox.stub(EventMessageHandler.prototype, 'isRateLimited' as any)
handler = new DelegatedEventMessageHandler(
webSocket as any,
strategyFactoryStub,
userRepository,
() => ({}) as any,
() => ({ hit: async () => false }),
)
})

afterEach(() => {
isEventValidStub.restore()
canAcceptEventStub.restore()
webSocket.removeAllListeners()
})

it('rejects event if it can\'t be accepted', async () => {
canAcceptEventStub.returns('reason')

await handler.handleMessage(message)

expect(canAcceptEventStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
[
MessageType.OK,
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
false,
'reason',
],
)
expect(strategyFactoryStub).not.to.have.been.called
})

it('rejects event if invalid', async () => {
isEventValidStub.returns('reason')

await handler.handleMessage(message)

expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
expect(strategyFactoryStub).not.to.have.been.called
})

it('rejects event if rate-limited', async () => {
isRateLimitedStub.resolves(true)

await handler.handleMessage(message)

expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
[MessageType.OK, event.id, false, 'rate-limited: slow down'],
)
expect(strategyFactoryStub).not.to.have.been.called
})

it('rejects event is user is not admitted', async () => {
isUserAdmitted.resolves('not admitted')

await handler.handleMessage(message)

expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event)
expect(isUserAdmitted).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).to.have.been.calledOnceWithExactly([
MessageType.OK,
event.id,
false,
'not admitted',
])
})

it('does not call strategy if none given', async () => {
isEventValidStub.returns(undefined)
canAcceptEventStub.returns(undefined)
strategyFactoryStub.returns(undefined)

await handler.handleMessage(message)

expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([
event,
webSocket,
])
expect(strategyExecuteStub).not.to.have.been.called
})

it('calls strategy with event', async () => {
isEventValidStub.returns(undefined)
canAcceptEventStub.returns(undefined)

await handler.handleMessage(message)

expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([
event,
webSocket,
])
expect(strategyExecuteStub).to.have.been.calledOnceWithExactly(event)
})

it('does not reject if strategy rejects', async () => {
const error = new Error('mistakes were made')
isEventValidStub.returns(undefined)
canAcceptEventStub.returns(undefined)
strategyExecuteStub.rejects(error)

return expect(handler.handleMessage(message)).to.eventually.be.fulfilled
})
})

describe('isEventValid', () => {
let parentIsEventValidStub: Sinon.SinonStub

beforeEach(() => {
parentIsEventValidStub = Sinon.stub(EventMessageHandler.prototype, 'isEventValid' as any)
event = {
'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
'created_at': 1660896109,
'kind': 1,
'tags': [
[
EventTags.Delegation,
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
'kind=1&created_at>1640995200',
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1',
],
],
'content': 'Hello world',
'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6',
}
})

afterEach(() => {
parentIsEventValidStub.restore()
})

it('returns undefined if event and delegate tag is valid', async () => {
parentIsEventValidStub.resolves(undefined)

expect(await (handler as any).isEventValid(event)).to.be.undefined
})

it('returns reason if event is not valid', () => {
parentIsEventValidStub.resolves('reason')
return expect((handler as any).isEventValid(event)).to.eventually.equal('reason')
})

it('returns reason if delegate signature is not valid', () => {
parentIsEventValidStub.resolves(undefined)

event.tags[0][3] = 'wrong sig'
return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: delegation verification failed')
})
})
})
15 changes: 15 additions & 0 deletions test/unit/handlers/event-strategies/delete-event-strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ describe('DeleteEventStrategy', () => {
let webSocketEmitStub: Sinon.SinonStub
let eventRepositoryCreateStub: Sinon.SinonStub
let eventRepositoryDeleteByPubkeyAndIdsStub: Sinon.SinonStub
let eventRepositoryInsertStubsStub: Sinon.SinonStub

let strategy: IEventStrategy<Event, Promise<void>>

@@ -42,6 +43,7 @@ describe('DeleteEventStrategy', () => {

eventRepositoryCreateStub = sandbox.stub(EventRepository.prototype, 'create')
eventRepositoryDeleteByPubkeyAndIdsStub = sandbox.stub(EventRepository.prototype, 'deleteByPubkeyAndIds')
eventRepositoryInsertStubsStub = sandbox.stub(EventRepository.prototype, 'insertStubs')

webSocketEmitStub = sandbox.stub()
webSocket = {
@@ -65,6 +67,18 @@ describe('DeleteEventStrategy', () => {
expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
})

it('inserts stubs', async () => {
await strategy.execute(event)

expect(eventRepositoryInsertStubsStub).to.have.been.calledOnceWithExactly(
event.pubkey,
[
'0000000000000000000000000000000000000000000000000000000000000000',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
]
)
})

it('deletes events if it has e tags', async () => {
await strategy.execute(event)

@@ -121,6 +135,7 @@ describe('DeleteEventStrategy', () => {

expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
expect(eventRepositoryDeleteByPubkeyAndIdsStub).not.to.have.been.called
expect(eventRepositoryInsertStubsStub).to.not.have.been.called
expect(webSocketEmitStub).not.to.have.been.called
})
})
36 changes: 27 additions & 9 deletions test/unit/repositories/event-repository.spec.ts
Original file line number Diff line number Diff line change
@@ -74,7 +74,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 limit 500')
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 limit 500')
})

it('selects events by two authors', () => {
@@ -89,7 +89,7 @@ describe('EventRepository', () => {

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 limit 500')
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 limit 500')
})

it('selects events by one author prefix (even length)', () => {
@@ -103,7 +103,7 @@ describe('EventRepository', () => {

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 limit 500')
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 limit 500')
})

it('selects events by one author prefix (odd length)', () => {
@@ -117,7 +117,7 @@ describe('EventRepository', () => {

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 limit 500')
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 limit 500')
})

it('selects events by two author prefix (first even, second odd)', () => {
@@ -132,7 +132,7 @@ describe('EventRepository', () => {

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 limit 500')
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 limit 500')
})
})

@@ -363,7 +363,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 limit 500) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc limit 500) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc limit 500')
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 limit 500) 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 limit 500) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc limit 500) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc limit 500')
})
})
})
@@ -435,7 +435,25 @@ 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", "expires_at", "remote_address") 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"]]\', NULL, \'::1\') 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", "expires_at", "remote_address") 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"]]\', NULL, \'::1\') on conflict do nothing')
})
})

describe('insertStubs', () => {
let clock: sinon.SinonFakeTimers

beforeEach(() => {
clock = sinon.useFakeTimers(1673835425)
})

afterEach(() => {
clock.restore()
})

it('insert stubs by pubkey & event ids', () => {
const query = repository.insertStubs('001122', ['aabbcc', 'ddeeff']).toString()

expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at") values (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'aabbcc\', 5, X\'001122\', X\'\', \'[]\', NULL), (\'1970-01-20T08:57:15.425Z\', \'\', 1673835, \'["001122",5]\', NULL, X\'ddeeff\', 5, X\'001122\', X\'\', \'[]\', NULL) on conflict do nothing')
})
})

@@ -462,7 +480,7 @@ describe('EventRepository', () => {

const query = repository.upsert(event).toString()

expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626')
expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL where "events"."event_created_at" < 1564498626')
})

it('replaces event based on event_pubkey, event_kind and event_deduplication', () => {
@@ -480,7 +498,7 @@ describe('EventRepository', () => {

const query = repository.upsert(event).toString()

expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626')
expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL where "events"."event_created_at" < 1564498626')
})
})
})
71 changes: 71 additions & 0 deletions test/unit/utils/event.spec.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ import { expect } from 'chai'
import { CanonicalEvent, Event } from '../../../src/@types/event'
import {
getEventExpiration,
isDelegatedEvent,
isDelegatedEventValid,
isDeleteEvent,
isEphemeralEvent,
isEventIdValid,
@@ -398,6 +400,75 @@ describe('NIP-16', () => {
// })
// })

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
})

it('resolves with false if no delegation tag is found', async () => {
event.tags = []
expect(await isDelegatedEventValid(event)).to.be.false
})

it('resolves with false if delegation signature is invalid', async () => {
event.tags[0][3] = 'f'
expect(await isDelegatedEventValid(event)).to.be.false
})

it('resolves with false if delegation rule is not a valid rune', async () => {
event.tags[0][2] = '@'
expect(await isDelegatedEventValid(event)).to.be.false
})


it('resolves with false if no delegation rule does not match', async () => {
event.tags[0][2] = 'a=1'
expect(await isDelegatedEventValid(event)).to.be.false
})
})

describe('isEventMatchingFilter', () => {
it('returns true if author is delegator', () => {
expect(
isEventMatchingFilter({ authors: ['86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e'] })(event)
).to.be.true
})

it('returns false if author is not delegator', () => {
expect(
isEventMatchingFilter({ authors: ['e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7'] })(event)
).to.be.false
})
})
})

describe('NIP-09', () => {
describe('isDeleteEvent', () => {
it('returns true if event is kind 5', () => {