/** * Relay manager for user relay preferences * Handles inbox/outbox relays and blocked relays */ import { fetchRelayLists } from '../user-data.js'; import { getBlockedRelays } from '../nostr/auth-handler.js'; import { config } from './config.js'; import { sessionManager } from '../auth/session-manager.js'; import { nostrClient } from './nostr-client.js'; import { KIND } from '../../types/kind-lookup.js'; import type { NostrEvent } from '../../types/nostr.js'; class RelayManager { private userInbox: string[] = []; private userOutbox: string[] = []; private userLocalRelaysRead: string[] = []; // Local relays (kind 10432) marked read or unmarked private userLocalRelaysWrite: string[] = []; // Local relays (kind 10432) marked write or unmarked private blockedRelays: Set = new Set(); /** * Load user relay preferences */ async loadUserPreferences(pubkey: string): Promise { try { // Fetch relay lists (includes both kind 10002 and 10432) with timeout const { inbox, outbox } = await Promise.race([ fetchRelayLists(pubkey), new Promise<{ inbox: string[]; outbox: string[] }>((resolve) => setTimeout(() => resolve({ inbox: [], outbox: [] }), 5000) ) ]); this.userInbox = inbox; this.userOutbox = outbox; } catch (error) { // If relay list fetch fails, use empty lists (default relays will be used) this.userInbox = []; this.userOutbox = []; } try { // Also fetch local relays separately to track read/write indicators // Local relays are used as external cache const relayList = [ ...config.defaultRelays, ...config.profileRelays ]; const localRelayEvents = await Promise.race([ nostrClient.fetchEvents( [{ kinds: [KIND.LOCAL_RELAYS], authors: [pubkey], limit: 1 }], relayList, { useCache: true, cacheResults: true, timeout: 5000 } ), new Promise((resolve) => setTimeout(() => resolve([]), 5000) ) ]); const localRelaysRead: string[] = []; const localRelaysWrite: string[] = []; for (const event of localRelayEvents) { for (const tag of event.tags) { if (tag[0] === 'r' && tag[1]) { const url = tag[1].trim(); if (!url) continue; const markers = tag.slice(2); // If no markers, relay is both read and write if (markers.length === 0) { if (!localRelaysRead.includes(url)) { localRelaysRead.push(url); } if (!localRelaysWrite.includes(url)) { localRelaysWrite.push(url); } continue; } // Check for explicit markers const hasRead = markers.includes('read'); const hasWrite = markers.includes('write'); // Determine read/write permissions // If only 'read' marker: read=true, write=false // If only 'write' marker: read=false, write=true // If both or neither: both true (default behavior) const read = hasRead || (!hasRead && !hasWrite); const write = hasWrite || (!hasRead && !hasWrite); if (read && !localRelaysRead.includes(url)) { localRelaysRead.push(url); } if (write && !localRelaysWrite.includes(url)) { localRelaysWrite.push(url); } } } } this.userLocalRelaysRead = localRelaysRead; this.userLocalRelaysWrite = localRelaysWrite; } catch (error) { // If local relay fetch fails, use empty lists this.userLocalRelaysRead = []; this.userLocalRelaysWrite = []; } // Get blocked relays this.blockedRelays = getBlockedRelays(); } /** * Clear user preferences (on logout) */ clearUserPreferences(): void { this.userInbox = []; this.userOutbox = []; this.userLocalRelaysRead = []; this.userLocalRelaysWrite = []; this.blockedRelays.clear(); } /** * Filter out blocked relays */ private filterBlocked(relays: string[]): string[] { if (this.blockedRelays.size === 0) return relays; return relays.filter((r) => !this.blockedRelays.has(r)); } /** * Normalize and deduplicate relay URLs */ private normalizeRelays(relays: string[]): string[] { // Normalize URLs (remove trailing slashes, etc.) const normalized = relays.map((r) => { let url = r.trim(); if (url.endsWith('/')) { url = url.slice(0, -1); } return url; }); // Deduplicate return [...new Set(normalized)]; } /** * Get relays for reading operations */ getReadRelays(baseRelays: string[], includeUserInbox = true): string[] { let relays = [...baseRelays]; // Add user inbox if logged in if (includeUserInbox && sessionManager.isLoggedIn() && this.userInbox.length > 0) { relays = [...relays, ...this.userInbox]; } // Add local relays marked for read (used as external cache) if (includeUserInbox && sessionManager.isLoggedIn() && this.userLocalRelaysRead.length > 0) { relays = [...relays, ...this.userLocalRelaysRead]; } // Normalize and deduplicate relays = this.normalizeRelays(relays); // Filter blocked relays return this.filterBlocked(relays); } /** * Get relays for publishing operations */ getPublishRelays(baseRelays: string[], includeUserOutbox = true): string[] { let relays = [...baseRelays]; // Add user outbox if logged in if (includeUserOutbox && sessionManager.isLoggedIn() && this.userOutbox.length > 0) { relays = [...relays, ...this.userOutbox]; } // Add local relays marked for write (used as external cache) if (includeUserOutbox && sessionManager.isLoggedIn() && this.userLocalRelaysWrite.length > 0) { relays = [...relays, ...this.userLocalRelaysWrite]; } // Normalize and deduplicate relays = this.normalizeRelays(relays); // Filter blocked relays return this.filterBlocked(relays); } /** * Get relays for reading threads (kind 11) */ getThreadReadRelays(): string[] { return this.getReadRelays(config.defaultRelays); } /** * Get relays for reading comments (kind 1111) */ getCommentReadRelays(): string[] { return this.getReadRelays(config.defaultRelays); } /** * Get relays for reading kind 1 feed */ getFeedReadRelays(): string[] { return this.getReadRelays(config.defaultRelays); } /** * Get relays for reading kind 1 responses */ getFeedResponseReadRelays(): string[] { return this.getReadRelays([ ...config.defaultRelays, 'wss://aggr.nostr.land' ]); } /** * Get relays for reading profiles (kind 0) */ getProfileReadRelays(): string[] { return this.getReadRelays([ ...config.defaultRelays, ...config.profileRelays ]); } /** * Get all available relays (for comprehensive searches) * Combines default relays, profile relays, and user relays */ getAllAvailableRelays(): string[] { const allBaseRelays = [ ...config.defaultRelays, ...config.profileRelays ]; return this.getReadRelays(allBaseRelays, true); } /** * Get relays for reading payment targets (kind 10133) */ getPaymentTargetReadRelays(): string[] { return this.getReadRelays([ ...config.defaultRelays, ...config.profileRelays ]); } /** * Get relays for reading user status (kind 30315) */ getUserStatusReadRelays(): string[] { return this.getReadRelays([ ...config.defaultRelays, ...config.profileRelays ]); } /** * Get relays for publishing threads (kind 11) */ getThreadPublishRelays(): string[] { return this.getPublishRelays([ ...config.defaultRelays, ...config.threadPublishRelays ]); } /** * Get relays for publishing comments (kind 1111) * If replying, include target's inbox and relay hints from the event being replied to */ getCommentPublishRelays(targetInbox?: string[], replyRelayHints?: string[]): string[] { let relays = this.getPublishRelays(config.defaultRelays); // If replying to an event with relay hints, add them if (replyRelayHints && replyRelayHints.length > 0) { relays = [...relays, ...replyRelayHints]; } // If replying, add target's inbox if (targetInbox && targetInbox.length > 0) { relays = [...relays, ...targetInbox]; } // Normalize and filter after combining all relays relays = this.normalizeRelays(relays); relays = this.filterBlocked(relays); return relays; } /** * Get relays for publishing kind 1 posts * If replying, include target's inbox and relay hints from the event being replied to */ getFeedPublishRelays(targetInbox?: string[], replyRelayHints?: string[]): string[] { let relays = this.getPublishRelays(config.defaultRelays); // If replying to an event with relay hints, add them if (replyRelayHints && replyRelayHints.length > 0) { relays = [...relays, ...replyRelayHints]; } // If replying, add target's inbox if (targetInbox && targetInbox.length > 0) { relays = [...relays, ...targetInbox]; } // Normalize and filter after combining all relays relays = this.normalizeRelays(relays); relays = this.filterBlocked(relays); return relays; } /** * Get relays for publishing reactions (kind 7) * If reacting to an event, include relay hints from that event * Includes user outbox and local relays, excludes read-only relays (aggregators) */ getReactionPublishRelays(reactionRelayHints?: string[]): string[] { // Start with default relays, excluding read-only relays const baseRelays = config.defaultRelays.filter( relay => !config.readOnlyRelays.includes(relay) ); // Get publish relays (includes user outbox and local relays) let relays = this.getPublishRelays(baseRelays, true); // If reacting to an event with relay hints, add them (but filter out read-only) if (reactionRelayHints && reactionRelayHints.length > 0) { const filteredHints = reactionRelayHints.filter( relay => !config.readOnlyRelays.includes(relay) ); relays = [...relays, ...filteredHints]; } // Normalize and filter after combining all relays relays = this.normalizeRelays(relays); relays = this.filterBlocked(relays); // Ensure we always have at least the default relays (minus aggregators) to publish to if (relays.length === 0) { // Fallback to default relays if we somehow ended up with an empty list relays = baseRelays.length > 0 ? baseRelays : config.defaultRelays.filter( relay => !config.readOnlyRelays.includes(relay) ); } // Log for debugging console.log('[RelayManager] Reaction publish relays:', { baseRelays, userOutbox: this.userOutbox, userLocalRelaysWrite: this.userLocalRelaysWrite, reactionRelayHints, finalRelays: relays }); return relays; } /** * Get relays for publishing file metadata (kind 1063) * Includes GIF relays in addition to normal publish relays */ getFileMetadataPublishRelays(): string[] { // Start with normal publish relays let relays = this.getPublishRelays(config.defaultRelays); // Add GIF relays relays = [...relays, ...config.gifRelays]; // Normalize and deduplicate relays = this.normalizeRelays(relays); // Filter blocked relays return this.filterBlocked(relays); } /** * Update blocked relays (called when user preferences change) */ updateBlockedRelays(blocked: Set): void { this.blockedRelays = blocked; } /** * Get local relays (kind 10432) - both read and write */ getLocalRelays(): string[] { const allLocalRelays = new Set(); this.userLocalRelaysRead.forEach(r => allLocalRelays.add(r)); this.userLocalRelaysWrite.forEach(r => allLocalRelays.add(r)); return Array.from(allLocalRelays); } } export const relayManager = new RelayManager();