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

/**
* 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();