feat: update server connection mechanism

This commit is contained in:
complex 2025-04-08 21:51:57 +02:00
parent 5300315bf4
commit f68b121bc4

@ -8,6 +8,12 @@ import { NDKUser, NDKEvent, NDKFilter, NDKPrivateKeySigner, NDKSubscription } fr
import express from 'express';
import { createServer } from 'http';
import { ServerConfig, HttpRequest, RateLimitConfig, RateLimitState } from './types';
import WebSocket from 'ws';
// Custom subscription interface for our WebSocket implementation
interface CustomSubscription {
unsubscribe: () => void;
}
/**
* Nostr HTTP Server class
@ -17,7 +23,7 @@ export class NostrHttpServer {
private config: ServerConfig;
private rateLimits: Map<string, RateLimitState>;
private rateLimitConfig: RateLimitConfig;
private subscription: NDKSubscription | null = null;
private subscription: CustomSubscription | null = null;
/**
* Create a new Nostr HTTP Server
@ -48,8 +54,34 @@ export class NostrHttpServer {
for (const relayUrl of this.config.relayUrls) {
try {
// Try to connect to the relay
await this.ndk.connect();
// Create a direct WebSocket connection to test connectivity
const ws = new WebSocket(relayUrl);
let connected = false;
// Wait for connection to establish with a timeout
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
if (!connected) {
ws.close();
reject(new Error('Connection timeout after 5 seconds'));
}
}, 5000);
ws.onopen = () => {
clearTimeout(timeout);
connected = true;
resolve();
};
ws.onerror = (err) => {
clearTimeout(timeout);
reject(new Error(`WebSocket error: ${err.toString()}`));
};
});
// Connection successful, close test connection
ws.close();
// If we get here, connection was successful
connectionStatus.set(relayUrl, true);
console.log(`Relay ${relayUrl}: Connected`);
@ -70,7 +102,7 @@ export class NostrHttpServer {
console.log('Sending unencrypted test event to verify subscription...');
// Create a test event
const testEvent = new NDKEvent(this.ndk, {
const testEvent = {
kind: 21120,
content: 'Test event for subscription verification',
created_at: Math.floor(Date.now() / 1000),
@ -78,14 +110,19 @@ export class NostrHttpServer {
tags: [
['p', this.config.pubkey]
]
});
};
// Sign and publish the event
await testEvent.sign();
await testEvent.publish();
// Sign the event
const signedEvent = await this.signEvent(testEvent);
if (!signedEvent) {
throw new Error('Failed to sign test event');
}
// Publish to all relays
await this.publishToRelays(signedEvent);
console.log('Unencrypted test event published successfully');
console.log('Event ID:', testEvent.id);
console.log('Event ID:', signedEvent.id);
console.log('This event should be received by your subscription without decryption errors');
} catch (error) {
console.error('Error sending test event:', error);
@ -109,7 +146,7 @@ export class NostrHttpServer {
}
// Create the event
const testEvent = new NDKEvent(this.ndk, {
const testEvent = {
kind: 21120,
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000),
@ -117,20 +154,117 @@ export class NostrHttpServer {
tags: [
['p', this.config.pubkey]
]
});
};
// Sign and publish the event
await testEvent.sign();
await testEvent.publish();
// Sign the event
const signedEvent = await this.signEvent(testEvent);
if (!signedEvent) {
throw new Error('Failed to sign encrypted test event');
}
// Publish to all relays
await this.publishToRelays(signedEvent);
console.log('Encrypted test event published successfully');
console.log('Event ID:', testEvent.id);
console.log('Event ID:', signedEvent.id);
console.log('This event should be received and decrypted by your subscription');
} catch (error) {
console.error('Error sending encrypted test event:', error);
}
}
/**
* Sign a Nostr event
* @param event The event to sign
* @returns The signed event or null if signing failed
*/
private async signEvent(event: any): Promise<any | null> {
try {
// Use NDK to sign the event
const ndkEvent = new NDKEvent(this.ndk, event);
await ndkEvent.sign();
return ndkEvent.rawEvent();
} catch (error) {
console.error('Error signing event:', error);
return null;
}
}
/**
* Publish an event to all configured relays
* @param event The event to publish
*/
private async publishToRelays(event: any): Promise<void> {
for (const relayUrl of this.config.relayUrls) {
try {
await this.publishToRelay(event, relayUrl);
} catch (error) {
console.error(`Error publishing to relay ${relayUrl}:`, error);
// Continue to the next relay
}
}
}
/**
* Publish an event to a specific relay
* @param event The event to publish
* @param relayUrl The relay URL
*/
private async publishToRelay(event: any, relayUrl: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
// Create a WebSocket connection
const ws = new WebSocket(relayUrl);
let connected = false;
// Set a timeout for the publish operation
const timeout = setTimeout(() => {
ws.close();
reject(new Error(`Timed out connecting to relay: ${relayUrl}`));
}, 10000);
ws.onopen = () => {
clearTimeout(timeout);
connected = true;
// Send the event
const reqId = `pub-${Date.now()}`;
const reqMsg = JSON.stringify(["EVENT", reqId, event]);
ws.send(reqMsg);
// Wait for OK response
const okTimeout = setTimeout(() => {
ws.close();
resolve(); // Resolve anyway, we don't want to block on OK response
}, 5000);
const messageHandler = (msg: any) => {
try {
const data = JSON.parse(msg.data as string);
if (Array.isArray(data) && data[0] === "OK" && data[1] === reqId) {
clearTimeout(okTimeout);
ws.removeListener('message', messageHandler);
ws.close();
resolve();
}
} catch (e) {
// Ignore parsing errors
}
};
ws.on('message', messageHandler);
};
ws.onerror = (err) => {
clearTimeout(timeout);
reject(new Error(`WebSocket error: ${err.toString()}`));
};
} catch (error) {
reject(error);
}
});
}
/**
* Encrypt content using NIP-44
* @param pubkey The public key of the recipient
@ -151,18 +285,8 @@ export class NostrHttpServer {
*/
public async start(): Promise<void> {
try {
// Connect to NDK
console.log('Connecting to relays:', this.config.relayUrls);
await this.ndk.connect();
console.log('Connected to Nostr relays');
// Verify connection status
const connectedRelays = this.ndk.explicitRelayUrls;
console.log('Connected to relays:', connectedRelays);
if (connectedRelays.length === 0) {
console.warn('Warning: No relays connected. Check your relay URLs.');
}
// We no longer need to connect to NDK since we're using direct WebSocket connections
console.log('Initializing server with relays:', this.config.relayUrls);
// Check individual relay connections
const relayStatus = await this.checkRelayConnections();
@ -188,7 +312,7 @@ export class NostrHttpServer {
<h1>Nostr HTTP Request Server</h1>
<p>Server npub: ${this.config.npub}</p>
<p>Status: Running</p>
<p>Connected to ${this.ndk.explicitRelayUrls.length} relays</p>
<p>Connected to ${this.config.relayUrls.length} relays</p>
<p>Subscription: ${this.subscription ? 'Active' : 'Inactive'}</p>
<p><a href="/test">Send Unencrypted Test Event</a></p>
<p><a href="/test-encrypted">Send Encrypted Test Event</a></p>
@ -246,50 +370,163 @@ export class NostrHttpServer {
* Subscribe to kind 21120 events
*/
private async subscribeToEvents(): Promise<void> {
const filter: NDKFilter = {
kinds: [21120 as any], // Type assertion to bypass NDKKind type check
// Define the filter type properly
interface NostrFilter {
kinds: number[];
'#p'?: string[];
authors?: string[];
}
// Create filter for kind 21120 events
const filter: NostrFilter = {
kinds: [21120], // HTTP Messages event kind
'#p': [this.config.pubkey] // Filter for events tagged with our pubkey
};
console.log('Subscribing with filter:', JSON.stringify(filter, null, 2));
console.log('Connected to relays:', this.ndk.explicitRelayUrls);
console.log('Connected to relays:', this.config.relayUrls);
// Create a subscription with the filter and options
const subscription = this.ndk.subscribe(filter, {
closeOnEose: false,
groupable: false
});
// Close existing subscription if any
if (this.subscription) {
try {
this.subscription.unsubscribe();
} catch (e) {
console.warn('Error unsubscribing from previous subscription:', e);
}
this.subscription = null;
}
// Log subscription status
console.log('Subscription created');
// Create a direct WebSocket connection for each relay
for (const relayUrl of this.config.relayUrls) {
try {
console.log(`Creating direct WebSocket subscription to ${relayUrl}`);
// Set up event handler
subscription.on('event', (event: NDKEvent) => {
console.log('Event details:');
console.log(` ID: ${event.id}`);
console.log(` Kind: ${event.kind}`);
console.log(` Pubkey: ${event.pubkey}`);
console.log(` Created at: ${new Date(event.created_at * 1000).toISOString()}`);
console.log(` Content: ${event.content.substring(0, 100)}${event.content.length > 100 ? '...' : ''}`);
// Create a direct WebSocket connection
const ws = new WebSocket(relayUrl);
let connected = false;
// Log tags
console.log(' Tags:');
event.tags.forEach(tag => {
console.log(` ${tag.join(', ')}`);
});
// Set up event handlers
ws.onopen = () => {
console.log(`WebSocket connected to ${relayUrl}`);
connected = true;
this.processEvent(event).catch(error => {
console.error('Error processing event:', error);
});
});
// Send a REQ message to subscribe
const reqId = `req-${Date.now()}`;
const reqMsg = JSON.stringify(["REQ", reqId, filter]);
console.log(`Sending subscription request: ${reqMsg}`);
// Add EOSE handler to confirm subscription is working
subscription.on('eose', () => {
console.log('EOSE received - subscription is active');
});
// Make sure to log when messages are received
console.log('Waiting for events from relay...');
ws.send(reqMsg);
};
// Store subscription for later reference
this.subscription = subscription;
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data as string);
// Handle different message types
if (Array.isArray(data)) {
console.log('Received message:', JSON.stringify(data).substring(0, 100) + '...');
if (data[0] === "EVENT" && data.length >= 3) {
console.log('Processing event:', data[2].id);
// Convert the raw event to NDKEvent for processing
const rawEvent = data[2];
const event = new NDKEvent(this.ndk, {
id: rawEvent.id,
pubkey: rawEvent.pubkey,
created_at: rawEvent.created_at,
kind: rawEvent.kind,
tags: rawEvent.tags,
content: rawEvent.content,
sig: rawEvent.sig
});
this.processEvent(event).catch(error => {
console.error('Error processing event:', error);
});
}
}
} catch (e) {
console.error('Error processing message:', e);
}
};
ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
ws.onclose = () => {
console.log('WebSocket connection closed');
if (connected) {
console.log('Connection closed, attempting to reconnect...');
// Implement reconnection logic here if needed
}
};
// Wait for connection to establish
await new Promise<void>((resolve, reject) => {
// Set a timeout to prevent hanging
const timeout = setTimeout(() => {
if (!connected) {
reject(new Error('Connection timeout'));
} else {
resolve();
}
}, 5000);
// Resolve immediately if already connected
if (connected) {
clearTimeout(timeout);
resolve();
}
// Override onopen to resolve promise
const originalOnOpen = ws.onopen;
ws.onopen = (ev) => {
clearTimeout(timeout);
if (originalOnOpen) {
originalOnOpen.call(ws, ev);
}
resolve();
};
// Override onerror to reject promise
const originalOnError = ws.onerror;
ws.onerror = (ev) => {
clearTimeout(timeout);
if (originalOnError) {
originalOnError.call(ws, ev);
}
reject(new Error('WebSocket connection error'));
};
});
// Store the subscription for later unsubscription
this.subscription = {
unsubscribe: () => {
try {
ws.close();
} catch (e) {
console.error('Error closing WebSocket:', e);
}
}
};
console.log(`Successfully subscribed to ${relayUrl} for kind 21120 events`);
// If we successfully connected to at least one relay, we can break the loop
break;
} catch (error) {
console.error(`Error creating subscription to ${relayUrl}:`, error);
// Continue to the next relay
}
}
if (!this.subscription) {
console.error('Failed to subscribe to any relay');
throw new Error('Failed to subscribe to any relay');
}
}
/**
@ -479,9 +716,14 @@ ${responseBody}`;
]
};
const event = new NDKEvent(this.ndk, rawEvent);
await event.sign();
await event.publish();
// Sign the event
const signedEvent = await this.signEvent(rawEvent);
if (!signedEvent) {
throw new Error('Failed to sign response event');
}
// Publish to all relays
await this.publishToRelays(signedEvent);
} catch (error) {
console.error('Error sending response event:', error);
throw error;