8 changed files with 477 additions and 151 deletions
@ -0,0 +1,265 @@
@@ -0,0 +1,265 @@
|
||||
/** |
||||
* Comprehensive relay list builder utility |
||||
* Handles all relay selection requirements: |
||||
* - Filters blocked relays |
||||
* - Includes local relays from kind 10432 |
||||
* - Handles author's outboxes/inboxes |
||||
* - Handles user's outboxes/inboxes |
||||
* - Includes relay hints |
||||
* - Includes seen relays |
||||
*/ |
||||
|
||||
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' |
||||
import { normalizeUrl } from '@/lib/url' |
||||
import { getCacheRelayUrls } from './private-relays' |
||||
import client from '@/services/client.service' |
||||
import logger from '@/lib/logger' |
||||
|
||||
export interface RelayListBuilderOptions { |
||||
/** Author's pubkey - will include their outboxes (write relays) */ |
||||
authorPubkey?: string |
||||
/** Logged-in user's pubkey - will include their inboxes (read relays) and outboxes (write relays) */ |
||||
userPubkey?: string |
||||
/** Explicit relay hints (from bech32 IDs or event tags) */ |
||||
relayHints?: string[] |
||||
/** Relays where an event was seen */ |
||||
seenRelays?: string[] |
||||
/** Relays where a containing event was found (for embedded events) */ |
||||
containingEventRelays?: string[] |
||||
/** Whether to include user's own relays (read/write/local) - for profiles/metadata */ |
||||
includeUserOwnRelays?: boolean |
||||
/** Whether to include PROFILE_FETCH_RELAY_URLS - for profiles/metadata */ |
||||
includeProfileFetchRelays?: boolean |
||||
/** Whether to include FAST_READ_RELAY_URLS as fallback */ |
||||
includeFastReadRelays?: boolean |
||||
/** Whether to include FAST_WRITE_RELAY_URLS as fallback */ |
||||
includeFastWriteRelays?: boolean |
||||
/** Whether to include SEARCHABLE_RELAY_URLS - for search */ |
||||
includeSearchableRelays?: boolean |
||||
/** Blocked relays to filter out */ |
||||
blockedRelays?: string[] |
||||
/** Whether to include local relays from kind 10432 */ |
||||
includeLocalRelays?: boolean |
||||
} |
||||
|
||||
/** |
||||
* Build comprehensive relay list according to requirements |
||||
*/ |
||||
export async function buildComprehensiveRelayList(options: RelayListBuilderOptions = {}): Promise<string[]> { |
||||
const { |
||||
authorPubkey, |
||||
userPubkey, |
||||
relayHints = [], |
||||
seenRelays = [], |
||||
containingEventRelays = [], |
||||
includeUserOwnRelays = false, |
||||
includeProfileFetchRelays = false, |
||||
includeFastReadRelays = true, |
||||
includeFastWriteRelays = false, |
||||
includeSearchableRelays = false, |
||||
blockedRelays = [], |
||||
includeLocalRelays = true |
||||
} = options |
||||
|
||||
const relayUrls = new Set<string>() |
||||
const normalizedBlocked = new Set( |
||||
(blockedRelays || []).map(url => { |
||||
const normalized = normalizeUrl(url) || url |
||||
return normalized.toLowerCase() |
||||
}).filter((url): url is string => !!url) |
||||
) |
||||
|
||||
const addRelay = (url: string | undefined) => { |
||||
if (!url) return |
||||
const normalized = normalizeUrl(url) |
||||
if (!normalized) return |
||||
// Filter blocked (case-insensitive comparison)
|
||||
if (normalizedBlocked.has(normalized.toLowerCase())) return |
||||
relayUrls.add(normalized) |
||||
} |
||||
|
||||
// 1. Relay hints (highest priority - explicit hints)
|
||||
relayHints.forEach(addRelay) |
||||
|
||||
// 2. Relays where event was seen
|
||||
seenRelays.forEach(addRelay) |
||||
|
||||
// 3. Relays where containing event was found (for embedded events)
|
||||
containingEventRelays.forEach(addRelay) |
||||
|
||||
// 4. Author's outboxes (write relays) - where they publish
|
||||
if (authorPubkey) { |
||||
try { |
||||
const authorRelayList = await client.fetchRelayList(authorPubkey) |
||||
const authorOutboxes = (authorRelayList.write || []).slice(0, 10) |
||||
authorOutboxes.forEach(addRelay) |
||||
|
||||
// Also include author's read relays (inboxes) for better discovery
|
||||
const authorInboxes = (authorRelayList.read || []).slice(0, 10) |
||||
authorInboxes.forEach(addRelay) |
||||
|
||||
logger.debug('[RelayListBuilder] Added author relays', { |
||||
author: authorPubkey.substring(0, 8), |
||||
outboxes: authorOutboxes.length, |
||||
inboxes: authorInboxes.length |
||||
}) |
||||
} catch (error) { |
||||
logger.debug('[RelayListBuilder] Failed to fetch author relay list', { error }) |
||||
} |
||||
} |
||||
|
||||
// 5. User's own relays (for profiles/metadata)
|
||||
if (includeUserOwnRelays && userPubkey) { |
||||
try { |
||||
const userRelayList = await client.fetchRelayList(userPubkey) |
||||
// Include both read and write
|
||||
const userRead = (userRelayList.read || []).slice(0, 10) |
||||
const userWrite = (userRelayList.write || []).slice(0, 10) |
||||
userRead.forEach(addRelay) |
||||
userWrite.forEach(addRelay) |
||||
|
||||
// Include local relays from kind 10432
|
||||
if (includeLocalRelays) { |
||||
const localRelays = await getCacheRelayUrls(userPubkey) |
||||
localRelays.forEach(addRelay) |
||||
} |
||||
|
||||
logger.debug('[RelayListBuilder] Added user own relays', { |
||||
read: userRead.length, |
||||
write: userWrite.length, |
||||
local: includeLocalRelays ? (await getCacheRelayUrls(userPubkey)).length : 0 |
||||
}) |
||||
} catch (error) { |
||||
logger.debug('[RelayListBuilder] Failed to fetch user relay list', { error }) |
||||
} |
||||
} else if (userPubkey) { |
||||
// Even if not including user's own relays, still include user's inboxes for reading
|
||||
try { |
||||
const userRelayList = await client.fetchRelayList(userPubkey) |
||||
const userInboxes = (userRelayList.read || []).slice(0, 10) |
||||
userInboxes.forEach(addRelay) |
||||
|
||||
// Include local relays from kind 10432 if enabled
|
||||
if (includeLocalRelays) { |
||||
const localRelays = await getCacheRelayUrls(userPubkey) |
||||
localRelays.forEach(addRelay) |
||||
} |
||||
} catch (error) { |
||||
logger.debug('[RelayListBuilder] Failed to fetch user inboxes', { error }) |
||||
} |
||||
} |
||||
|
||||
// 6. Profile fetch relays (for profiles/metadata)
|
||||
if (includeProfileFetchRelays) { |
||||
PROFILE_FETCH_RELAY_URLS.forEach(addRelay) |
||||
} |
||||
|
||||
// 7. Fast read relays (fallback)
|
||||
if (includeFastReadRelays) { |
||||
FAST_READ_RELAY_URLS.forEach(addRelay) |
||||
} |
||||
|
||||
// 8. Fast write relays (for writing)
|
||||
if (includeFastWriteRelays) { |
||||
FAST_WRITE_RELAY_URLS.forEach(addRelay) |
||||
} |
||||
|
||||
// 9. Searchable relays (for search)
|
||||
if (includeSearchableRelays) { |
||||
SEARCHABLE_RELAY_URLS.forEach(addRelay) |
||||
} |
||||
|
||||
return Array.from(relayUrls) |
||||
} |
||||
|
||||
/** |
||||
* Build relay list for reading replies/comments |
||||
* READ from: FAST_READ_RELAY_URLS + user's inboxes + local relays + OP author's outboxes |
||||
*/ |
||||
export async function buildReplyReadRelayList( |
||||
opAuthorPubkey: string | undefined, |
||||
userPubkey: string | undefined, |
||||
blockedRelays: string[] = [] |
||||
): Promise<string[]> { |
||||
return buildComprehensiveRelayList({ |
||||
authorPubkey: opAuthorPubkey, |
||||
userPubkey, |
||||
includeFastReadRelays: true, |
||||
includeLocalRelays: true, |
||||
blockedRelays |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Build relay list for writing replies/comments |
||||
* WRITE to: OP author's outboxes + OP author's inboxes + reply-to author's inboxes + user's outboxes + local relay |
||||
*/ |
||||
export async function buildReplyWriteRelayList( |
||||
opAuthorPubkey: string | undefined, |
||||
replyToAuthorPubkey: string | undefined, |
||||
userPubkey: string | undefined, |
||||
blockedRelays: string[] = [] |
||||
): Promise<string[]> { |
||||
const relayUrls = new Set<string>() |
||||
const normalizedBlocked = new Set( |
||||
(blockedRelays || []).map(url => { |
||||
const normalized = normalizeUrl(url) || url |
||||
return normalized.toLowerCase() |
||||
}).filter((url): url is string => !!url) |
||||
) |
||||
|
||||
const addRelay = (url: string | undefined) => { |
||||
if (!url) return |
||||
const normalized = normalizeUrl(url) |
||||
if (!normalized) return |
||||
// Filter blocked (case-insensitive comparison)
|
||||
if (normalizedBlocked.has(normalized.toLowerCase())) return |
||||
relayUrls.add(normalized) |
||||
} |
||||
|
||||
// OP author's outboxes
|
||||
if (opAuthorPubkey) { |
||||
try { |
||||
const opRelayList = await client.fetchRelayList(opAuthorPubkey) |
||||
const opOutboxes = (opRelayList.write || []).slice(0, 10) |
||||
opOutboxes.forEach(addRelay) |
||||
|
||||
// OP author's inboxes
|
||||
const opInboxes = (opRelayList.read || []).slice(0, 10) |
||||
opInboxes.forEach(addRelay) |
||||
} catch (error) { |
||||
logger.debug('[RelayListBuilder] Failed to fetch OP author relay list', { error }) |
||||
} |
||||
} |
||||
|
||||
// Reply-to author's inboxes
|
||||
if (replyToAuthorPubkey && replyToAuthorPubkey !== opAuthorPubkey) { |
||||
try { |
||||
const replyToRelayList = await client.fetchRelayList(replyToAuthorPubkey) |
||||
const replyToInboxes = (replyToRelayList.read || []).slice(0, 10) |
||||
replyToInboxes.forEach(addRelay) |
||||
} catch (error) { |
||||
logger.debug('[RelayListBuilder] Failed to fetch reply-to author relay list', { error }) |
||||
} |
||||
} |
||||
|
||||
// User's outboxes
|
||||
if (userPubkey) { |
||||
try { |
||||
const userRelayList = await client.fetchRelayList(userPubkey) |
||||
const userOutboxes = (userRelayList.write || []).slice(0, 10) |
||||
userOutboxes.forEach(addRelay) |
||||
|
||||
// User's local relay (kind 10432)
|
||||
const localRelays = await getCacheRelayUrls(userPubkey) |
||||
localRelays.forEach(addRelay) |
||||
} catch (error) { |
||||
logger.debug('[RelayListBuilder] Failed to fetch user relay list', { error }) |
||||
} |
||||
} |
||||
|
||||
// Fast write relays as fallback
|
||||
FAST_WRITE_RELAY_URLS.forEach(addRelay) |
||||
|
||||
return Array.from(relayUrls) |
||||
} |
||||
Loading…
Reference in new issue