You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
12 KiB
409 lines
12 KiB
/** |
|
* 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<string> = new Set(); |
|
|
|
/** |
|
* Load user relay preferences |
|
*/ |
|
async loadUserPreferences(pubkey: string): Promise<void> { |
|
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<NostrEvent[]>((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<string>): void { |
|
this.blockedRelays = blocked; |
|
} |
|
|
|
/** |
|
* Get local relays (kind 10432) - both read and write |
|
*/ |
|
getLocalRelays(): string[] { |
|
const allLocalRelays = new Set<string>(); |
|
this.userLocalRelaysRead.forEach(r => allLocalRelays.add(r)); |
|
this.userLocalRelaysWrite.forEach(r => allLocalRelays.add(r)); |
|
return Array.from(allLocalRelays); |
|
} |
|
} |
|
|
|
export const relayManager = new RelayManager();
|
|
|