Skip to content

Commit 450a1ce

Browse files
authored
Merge branch 'main' into bugfix/nip11-relay-url-path
2 parents fc75e61 + b3542f6 commit 450a1ce

22 files changed

+1156
-57
lines changed

.env.example

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# --- REQUIRED ---
2+
SECRET=change_me_to_something_long_and_random # Generate: openssl rand -hex 128
3+
4+
# --- POSTGRESQL ---
5+
DB_HOST=localhost
6+
DB_PORT=5432
7+
DB_NAME=nostr_ts_relay
8+
DB_USER=nostr_ts_relay
9+
DB_PASSWORD=nostr_ts_relay
10+
# Alternatively, use a URI:
11+
# DB_URI=postgresql://nostr_ts_relay:nostr_ts_relay@localhost:5432/nostr_ts_relay
12+
13+
# --- DB POOL TUNING (Optional) ---
14+
# DB_MIN_POOL_SIZE=0
15+
# DB_MAX_POOL_SIZE=3
16+
# DB_ACQUIRE_CONNECTION_TIMEOUT=60000
17+
18+
# --- REDIS (Required for Rate Limiting) ---
19+
REDIS_HOST=localhost
20+
REDIS_PORT=6379
21+
# REDIS_USER=default
22+
# REDIS_PASSWORD=
23+
# Alternatively, use a URI:
24+
# REDIS_URI=redis://localhost:6379
25+
26+
# --- SERVER CONFIG ---
27+
RELAY_PORT=8008
28+
WORKER_COUNT=2 # Defaults to CPU count. Use 1 or 2 for local testing.
29+
# NOSTR_CONFIG_DIR=.nostr # Where settings.yaml lives
30+
31+
# --- DEBUGGING ---
32+
# Useful namespaces: maintenance-worker, database-client:*, cache-client, etc.
33+
# DEBUG=maintenance-worker
34+
35+
# --- RELAY PRIVATE KEY (Optional) ---
36+
# RELAY_PRIVATE_KEY=your_hex_private_key
37+
38+
# --- PAYMENTS (Only if enabled in settings.yaml) ---
39+
# ZEBEDEE_API_KEY=
40+
# NODELESS_API_KEY=
41+
# NODELESS_WEBHOOK_SECRET=
42+
# OPENNODE_API_KEY=
43+
# LNBITS_API_KEY=
44+
45+
# --- READ REPLICAS (Optional) ---
46+
# READ_REPLICA_ENABLED=false
47+
# READ_REPLICAS=2
48+
# RR0_DB_HOST=localhost
49+
# RR0_DB_PORT=5432
50+
# RR0_DB_NAME=nostr_ts_relay
51+
# RR0_DB_USER=your_psql_username
52+
# RR0_DB_PASSWORD=your_psql_password
53+
# RR0_DB_MIN_POOL_SIZE=0
54+
# RR0_DB_MAX_POOL_SIZE=3
55+
# RR0_DB_ACQUIRE_CONNECTION_TIMEOUT=60000
56+
# RR1_DB_HOST=localhost
57+
# RR1_DB_PORT=5432
58+
# RR1_DB_NAME=nostr_ts_relay
59+
# RR1_DB_USER=your_psql_username
60+
# RR1_DB_PASSWORD=your_psql_password
61+
# RR1_DB_MIN_POOL_SIZE=0
62+
# RR1_DB_MAX_POOL_SIZE=3
63+
# RR1_DB_ACQUIRE_CONNECTION_TIMEOUT=60000
64+
65+
# --- TOR (Optional) ---
66+
# TOR_HOST=localhost
67+
# TOR_CONTROL_PORT=9051
68+
# TOR_PASSWORD=
69+
# HIDDEN_SERVICE_PORT=80

.github/workflows/checks.yml

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,6 @@ jobs:
129129
with:
130130
name: integration-coverage-lcov
131131
path: .coverage/integration/lcov.info
132-
sonarcloud:
133-
name: Sonarcloud
134-
needs: [test-units-and-cover, test-integrations-and-cover]
135-
runs-on: ubuntu-latest
136-
steps:
137-
- name: Checkout
138-
uses: actions/checkout@v3
139-
with:
140-
fetch-depth: 0
141-
- uses: actions/download-artifact@v4
142-
name: Download unit & integration coverage reports
143-
with:
144-
path: .coverage
145-
- name: SonarCloud Scan
146-
uses: sonarsource/sonarcloud-github-action@master
147-
env:
148-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
149-
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
150132
post-tests:
151133
name: Post Tests
152134
needs: [test-units-and-cover, test-integrations-and-cover]

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,29 @@ Print the Tor hostname:
230230
./scripts/print_tor_hostname
231231
```
232232
233+
### Importing events from JSON Lines
234+
235+
You can import NIP-01 events from a `.jsonl` file directly into the relay database.
236+
237+
Basic import:
238+
```
239+
npm run import -- ./events.jsonl
240+
```
241+
242+
Set a custom batch size (default: `1000`):
243+
```
244+
npm run import -- ./events.jsonl --batch-size 500
245+
```
246+
247+
The importer:
248+
249+
- Processes the file line-by-line to keep memory usage bounded.
250+
- Validates NIP-01 schema, event id hash, and Schnorr signature before insertion.
251+
- Inserts in database transactions per batch.
252+
- Skips duplicates without failing the whole import.
253+
- Prints progress in the format:
254+
`[Processed: 50,000 | Inserted: 45,000 | Skipped: 5,000 | Errors: 0]`
255+
233256
### Running as a Service
234257
235258
By default this server will run continuously until you stop it with Ctrl+C or until the system restarts.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"lint": "eslint --ext .ts ./src ./test",
3232
"lint:report": "eslint -o .lint-reports/eslint.json -f json --ext .ts ./src ./test",
3333
"lint:fix": "npm run lint -- --fix",
34+
"import": "node -r ts-node/register src/import-events.ts",
3435
"db:migrate": "knex migrate:latest",
3536
"db:migrate:rollback": "knex migrate:rollback",
3637
"db:seed": "knex seed:run",

src/@types/repositories.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ export interface IQueryResult<T> extends Pick<Promise<T>, keyof Promise<T> & Exp
1414

1515
export interface IEventRepository {
1616
create(event: Event): Promise<number>
17+
createMany(events: Event[]): Promise<number>
1718
upsert(event: Event): Promise<number>
19+
upsertMany(events: Event[]): Promise<number>
1820
findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]>
1921
deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise<number>
22+
deleteByPubkeyExceptKinds(pubkey: Pubkey, excludedKinds: number[]): Promise<number>
23+
hasActiveRequestToVanish(pubkey: Pubkey): Promise<boolean>
2024
}
2125

2226
export interface IInvoiceRepository {

src/constants/base.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum EventKinds {
77
DELETE = 5,
88
REPOST = 6,
99
REACTION = 7,
10+
REQUEST_TO_VANISH = 62,
1011
// Channels
1112
CHANNEL_CREATION = 40,
1213
CHANNEL_METADATA = 41,
@@ -36,12 +37,15 @@ export enum EventKinds {
3637
export enum EventTags {
3738
Event = 'e',
3839
Pubkey = 'p',
40+
Relay = 'r',
3941
// Multicast = 'm',
4042
Deduplication = 'd',
4143
Expiration = 'expiration',
4244
Invoice = 'bolt11',
4345
}
4446

47+
export const ALL_RELAYS = 'ALL_RELAYS'
48+
4549
export enum PaymentsProcessors {
4650
LNURL = 'lnurl',
4751
ZEBEDEE = 'zebedee',

src/factories/event-strategy-factory.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent } from '../utils/event'
1+
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent } from '../utils/event'
22
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
33
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
44
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
@@ -9,12 +9,15 @@ import { IEventStrategy } from '../@types/message-handlers'
99
import { IWebSocketAdapter } from '../@types/adapters'
1010
import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy'
1111
import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy'
12+
import { VanishEventStrategy } from '../handlers/event-strategies/vanish-event-strategy'
1213

1314
export const eventStrategyFactory = (
1415
eventRepository: IEventRepository,
1516
): Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]> =>
1617
([event, adapter]: [Event, IWebSocketAdapter]) => {
17-
if (isReplaceableEvent(event)) {
18+
if (isRequestToVanishEvent(event)) {
19+
return new VanishEventStrategy(adapter, eventRepository)
20+
} else if (isReplaceableEvent(event)) {
1821
return new ReplaceableEventStrategy(adapter, eventRepository)
1922
} else if (isEphemeralEvent(event)) {
2023
return new EphemeralEventStrategy(adapter)

src/factories/message-handler-factory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const messageHandlerFactory = (
1818
return new EventMessageHandler(
1919
adapter,
2020
eventStrategyFactory(eventRepository),
21+
eventRepository,
2122
userRepository,
2223
createSettings,
2324
slidingWindowRateLimiterFactory,

src/handlers/event-message-handler.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1-
import { Event, ExpiringEvent } from '../@types/event'
1+
import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
2+
import { Event, ExpiringEvent } from '../@types/event'
23
import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings'
3-
import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, getPublicKey, getRelayPrivateKey, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event'
4+
import {
5+
getEventExpiration,
6+
getEventProofOfWork,
7+
getPubkeyProofOfWork,
8+
getPublicKey,
9+
getRelayPrivateKey,
10+
isEventIdValid,
11+
isEventKindOrRangeMatch,
12+
isEventSignatureValid,
13+
isExpiredEvent,
14+
isRequestToVanishEvent,
15+
} from '../utils/event'
16+
import { IEventRepository, IUserRepository } from '../@types/repositories'
417
import { IEventStrategy, IMessageHandler } from '../@types/message-handlers'
5-
import { ContextMetadataKey } from '../constants/base'
618
import { createCommandResult } from '../utils/messages'
719
import { createLogger } from '../factories/logger-factory'
8-
import { EventExpirationTimeMetadataKey } from '../constants/base'
920
import { Factory } from '../@types/base'
1021
import { IncomingEventMessage } from '../@types/messages'
1122
import { IRateLimiter } from '../@types/utils'
12-
import { IUserRepository } from '../@types/repositories'
1323
import { IWebSocketAdapter } from '../@types/adapters'
1424
import { WebSocketAdapterEvent } from '../constants/adapter'
1525

@@ -19,6 +29,7 @@ export class EventMessageHandler implements IMessageHandler {
1929
public constructor(
2030
protected readonly webSocket: IWebSocketAdapter,
2131
protected readonly strategyFactory: Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]>,
32+
protected readonly eventRepository: IEventRepository,
2233
protected readonly userRepository: IUserRepository,
2334
private readonly settings: () => Settings,
2435
private readonly slidingWindowRateLimiter: Factory<IRateLimiter>,
@@ -57,6 +68,13 @@ export class EventMessageHandler implements IMessageHandler {
5768
return
5869
}
5970

71+
reason = await this.isBlockedByRequestToVanish(event)
72+
if (reason) {
73+
debug('event %s rejected: %s', event.id, reason)
74+
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
75+
return
76+
}
77+
6078
reason = await this.isUserAdmitted(event)
6179
if (reason) {
6280
debug('event %s rejected: %s', event.id, reason)
@@ -190,6 +208,26 @@ export class EventMessageHandler implements IMessageHandler {
190208
if (!await isEventSignatureValid(event)) {
191209
return 'invalid: event signature verification failed'
192210
}
211+
212+
if (event.kind === EventKinds.REQUEST_TO_VANISH && !isRequestToVanishEvent(event, this.settings().info.relay_url)) {
213+
return 'invalid: request to vanish relay tag invalid'
214+
}
215+
}
216+
217+
protected async isBlockedByRequestToVanish(event: Event): Promise<string | undefined> {
218+
if (isRequestToVanishEvent(event)) {
219+
return
220+
}
221+
222+
const relayPubkey = this.getRelayPublicKey()
223+
if (relayPubkey === event.pubkey) {
224+
return
225+
}
226+
227+
const existingVanishRequest = await this.eventRepository.hasActiveRequestToVanish(event.pubkey)
228+
if (existingVanishRequest) {
229+
return 'blocked: request to vanish active for pubkey'
230+
}
193231
}
194232

195233
protected async isRateLimited(event: Event): Promise<boolean> {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createCommandResult } from '../../utils/messages'
2+
import { createLogger } from '../../factories/logger-factory'
3+
import { Event } from '../../@types/event'
4+
import { EventKinds } from '../../constants/base'
5+
import { IEventRepository } from '../../@types/repositories'
6+
import { IEventStrategy } from '../../@types/message-handlers'
7+
import { IWebSocketAdapter } from '../../@types/adapters'
8+
import { WebSocketAdapterEvent } from '../../constants/adapter'
9+
10+
const debug = createLogger('vanish-event-strategy')
11+
12+
export class VanishEventStrategy implements IEventStrategy<Event, Promise<void>> {
13+
public constructor(
14+
private readonly webSocket: IWebSocketAdapter,
15+
private readonly eventRepository: IEventRepository,
16+
) {}
17+
18+
public async execute(event: Event): Promise<void> {
19+
debug('received request to vanish event: %o', event)
20+
21+
await this.eventRepository.deleteByPubkeyExceptKinds(
22+
event.pubkey,
23+
[EventKinds.REQUEST_TO_VANISH],
24+
)
25+
26+
const count = await this.eventRepository.create(event)
27+
28+
this.webSocket.emit(
29+
WebSocketAdapterEvent.Message,
30+
createCommandResult(event.id, true, count ? '' : 'duplicate:')
31+
)
32+
}
33+
}

0 commit comments

Comments
 (0)