5 changed files with 917 additions and 2 deletions
@ -0,0 +1,169 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import RelayInfoDisplay from './RelayInfoDisplay.svelte'; |
||||||
|
import { fetchRelayInfos, type RelayInfoWithMetadata } from '$lib/utils/relay_info_service'; |
||||||
|
|
||||||
|
const { |
||||||
|
relays, |
||||||
|
inboxRelays = [], |
||||||
|
outboxRelays = [], |
||||||
|
showLabels = true, |
||||||
|
compact = false |
||||||
|
} = $props<{ |
||||||
|
relays: string[]; |
||||||
|
inboxRelays?: string[]; |
||||||
|
outboxRelays?: string[]; |
||||||
|
showLabels?: boolean; |
||||||
|
compact?: boolean; |
||||||
|
}>(); |
||||||
|
|
||||||
|
let relayInfos = $state<RelayInfoWithMetadata[]>([]); |
||||||
|
let isLoading = $state(true); |
||||||
|
|
||||||
|
type CategorizedRelay = { |
||||||
|
relay: string; |
||||||
|
category: 'both' | 'inbox' | 'outbox' | 'other'; |
||||||
|
label: string; |
||||||
|
}; |
||||||
|
|
||||||
|
// AI-NOTE: Updated to show only top-3 inboxes and top-3 outboxes as intended |
||||||
|
const categorizedRelays = $derived(() => { |
||||||
|
const inbox = new Set(inboxRelays); |
||||||
|
const outbox = new Set(outboxRelays); |
||||||
|
|
||||||
|
console.log('[RelayInfoList] Categorizing relays:', { |
||||||
|
relays: relays.length, |
||||||
|
inboxRelays: inboxRelays.length, |
||||||
|
outboxRelays: outboxRelays.length |
||||||
|
}); |
||||||
|
|
||||||
|
// Create a map of all relays with their categories |
||||||
|
const relayCategories = new Map<string, CategorizedRelay>(); |
||||||
|
|
||||||
|
// Process inbox relays (up to top 3) |
||||||
|
const topInboxRelays = inboxRelays.slice(0, 3); |
||||||
|
topInboxRelays.forEach((relay: string) => { |
||||||
|
const isOutbox = outbox.has(relay); |
||||||
|
if (isOutbox) { |
||||||
|
relayCategories.set(relay, { relay, category: 'both', label: 'Inbox & Outbox' }); |
||||||
|
} else { |
||||||
|
relayCategories.set(relay, { relay, category: 'inbox', label: 'Recipient Inbox' }); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Process outbox relays (up to top 3) |
||||||
|
const topOutboxRelays = outboxRelays.slice(0, 3); |
||||||
|
topOutboxRelays.forEach((relay: string) => { |
||||||
|
if (!relayCategories.has(relay)) { |
||||||
|
relayCategories.set(relay, { relay, category: 'outbox', label: 'Sender Outbox' }); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Only include relays that are actually in the top-3 lists |
||||||
|
// This ensures we only show the intended top-3 inboxes and top-3 outboxes |
||||||
|
const categorized = Array.from(relayCategories.values()); |
||||||
|
console.log('[RelayInfoList] Categorized relays count:', categorized.length); |
||||||
|
return categorized; |
||||||
|
}); |
||||||
|
|
||||||
|
// Group by category |
||||||
|
const groupedRelays = $derived(() => { |
||||||
|
const categorized = categorizedRelays(); |
||||||
|
console.log('[RelayInfoList] Grouping categorized relays'); |
||||||
|
|
||||||
|
const groups = { |
||||||
|
both: categorized.filter((r: CategorizedRelay) => r.category === 'both'), |
||||||
|
inbox: categorized.filter((r: CategorizedRelay) => r.category === 'inbox'), |
||||||
|
outbox: categorized.filter((r: CategorizedRelay) => r.category === 'outbox'), |
||||||
|
other: categorized.filter((r: CategorizedRelay) => r.category === 'other') |
||||||
|
}; |
||||||
|
|
||||||
|
console.log('[RelayInfoList] Grouped relays:', Object.fromEntries( |
||||||
|
Object.entries(groups).map(([key, relays]) => [key, relays.length]) |
||||||
|
)); |
||||||
|
return groups; |
||||||
|
}); |
||||||
|
|
||||||
|
async function loadRelayInfos() { |
||||||
|
isLoading = true; |
||||||
|
try { |
||||||
|
const categorized = categorizedRelays(); |
||||||
|
const relayUrls = categorized.map(r => r.relay); |
||||||
|
relayInfos = await fetchRelayInfos(relayUrls); |
||||||
|
} catch (error) { |
||||||
|
console.warn('[RelayInfoList] Error loading relay infos:', error); |
||||||
|
} finally { |
||||||
|
isLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Load relay info when categorized relays change |
||||||
|
$effect(() => { |
||||||
|
const categorized = categorizedRelays(); |
||||||
|
console.log('[RelayInfoList] Categorized relays changed:', { |
||||||
|
total: categorized.length, |
||||||
|
byCategory: Object.fromEntries( |
||||||
|
Object.entries(groupedRelays()).map(([key, relays]) => [key, relays.length]) |
||||||
|
) |
||||||
|
}); |
||||||
|
if (categorized.length > 0) { |
||||||
|
loadRelayInfos(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Get relay info for a specific relay |
||||||
|
function getRelayInfo(relayUrl: string): RelayInfoWithMetadata | undefined { |
||||||
|
return relayInfos.find(info => info.url === relayUrl); |
||||||
|
} |
||||||
|
|
||||||
|
// Category colors |
||||||
|
const categoryColors = { |
||||||
|
both: 'bg-green-100 dark:bg-green-900 border-green-200 dark:border-green-700 text-green-800 dark:text-green-200', |
||||||
|
inbox: 'bg-blue-100 dark:bg-blue-900 border-blue-200 dark:border-blue-700 text-blue-800 dark:text-blue-200', |
||||||
|
outbox: 'bg-purple-100 dark:bg-purple-900 border-purple-200 dark:border-purple-700 text-purple-800 dark:text-purple-200', |
||||||
|
other: 'bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-200' |
||||||
|
}; |
||||||
|
|
||||||
|
const categoryIcons = { |
||||||
|
both: '🔄', |
||||||
|
inbox: '📥', |
||||||
|
outbox: '📤', |
||||||
|
other: '🌐' |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="space-y-2"> |
||||||
|
{#if showLabels && !compact} |
||||||
|
{@const categorizedCount = categorizedRelays().length} |
||||||
|
{@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorizedRelays())} |
||||||
|
<div class="text-sm font-medium text-gray-700 dark:text-gray-300"> |
||||||
|
Publishing to {categorizedCount} relay(s): |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
{#if isLoading} |
||||||
|
<div class="flex items-center justify-center py-2"> |
||||||
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"></div> |
||||||
|
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Loading relay info...</span> |
||||||
|
</div> |
||||||
|
{:else} |
||||||
|
{@const categorized = categorizedRelays()} |
||||||
|
{@const debugCategorized = console.log('[RelayInfoList] Debug - categorized relays:', categorized)} |
||||||
|
|
||||||
|
<div class="space-y-1"> |
||||||
|
{#each categorized as { relay, category, label }} |
||||||
|
<div class="p-2 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700"> |
||||||
|
<div class="flex items-center justify-between"> |
||||||
|
<span class="text-sm font-mono text-gray-900 dark:text-gray-100"> |
||||||
|
{relay} |
||||||
|
</span> |
||||||
|
{#if category === 'both'} |
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400 italic"> |
||||||
|
common relay |
||||||
|
</span> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{/each} |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
@ -0,0 +1,252 @@ |
|||||||
|
import { get } from "svelte/store"; |
||||||
|
import { ndkInstance } from "../ndk"; |
||||||
|
import { userStore } from "../stores/userStore"; |
||||||
|
import { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; |
||||||
|
import type NDK from "@nostr-dev-kit/ndk"; |
||||||
|
import { nip19 } from "nostr-tools"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches user's outbox relays from NIP-65 relay list |
||||||
|
* @param ndk NDK instance |
||||||
|
* @param user User to fetch outbox relays for |
||||||
|
* @returns Promise that resolves to array of outbox relay URLs |
||||||
|
*/ |
||||||
|
async function getUseroutboxRelays(ndk: NDK, user: NDKUser): Promise<string[]> { |
||||||
|
try { |
||||||
|
console.debug('[kind24_utils] Fetching outbox relays for user:', user.pubkey); |
||||||
|
const relayList = await ndk.fetchEvent( |
||||||
|
{ |
||||||
|
kinds: [10002], |
||||||
|
authors: [user.pubkey], |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (!relayList) { |
||||||
|
console.debug('[kind24_utils] No relay list found for user'); |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
console.debug('[kind24_utils] Found relay list event:', relayList.id); |
||||||
|
console.debug('[kind24_utils] Relay list tags:', relayList.tags); |
||||||
|
|
||||||
|
const outboxRelays: string[] = []; |
||||||
|
relayList.tags.forEach((tag) => { |
||||||
|
console.debug('[kind24_utils] Processing tag:', tag); |
||||||
|
if (tag[0] === 'r' && tag[1]) { |
||||||
|
// NIP-65: r tags with optional inbox/outbox markers
|
||||||
|
const marker = tag[2]; |
||||||
|
if (!marker || marker === 'outbox' || marker === 'inbox') { |
||||||
|
// If no marker or marker is 'outbox', it's a outbox relay
|
||||||
|
// If marker is 'inbox', it's also a outbox relay (NIP-65 allows both)
|
||||||
|
outboxRelays.push(tag[1]); |
||||||
|
console.debug('[kind24_utils] Added outbox relay:', tag[1]); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
console.debug('[kind24_utils] Final outbox relays:', outboxRelays); |
||||||
|
return outboxRelays; |
||||||
|
} catch (error) { |
||||||
|
console.info('[kind24_utils] Error fetching user outbox relays:', error); |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches user's inbox relays from NIP-65 relay list |
||||||
|
* @param ndk NDK instance |
||||||
|
* @param user User to fetch inbox relays for |
||||||
|
* @returns Promise that resolves to array of inbox relay URLs |
||||||
|
*/ |
||||||
|
async function getUserinboxRelays(ndk: NDK, user: NDKUser): Promise<string[]> { |
||||||
|
try { |
||||||
|
console.debug('[kind24_utils] Fetching inbox relays for user:', user.pubkey); |
||||||
|
const relayList = await ndk.fetchEvent( |
||||||
|
{ |
||||||
|
kinds: [10002], |
||||||
|
authors: [user.pubkey], |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (!relayList) { |
||||||
|
console.debug('[kind24_utils] No relay list found for user'); |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
console.debug('[kind24_utils] Found relay list event:', relayList.id); |
||||||
|
console.debug('[kind24_utils] Relay list tags:', relayList.tags); |
||||||
|
|
||||||
|
const inboxRelays: string[] = []; |
||||||
|
relayList.tags.forEach((tag) => { |
||||||
|
console.debug('[kind24_utils] Processing tag:', tag); |
||||||
|
if (tag[0] === 'r' && tag[1]) { |
||||||
|
// NIP-65: r tags with optional inbox/outbox markers
|
||||||
|
const marker = tag[2]; |
||||||
|
if (!marker || marker === 'inbox' || marker === 'outbox') { |
||||||
|
// If no marker or marker is 'inbox', it's a inbox relay
|
||||||
|
// If marker is 'outbox', it's also a inbox relay (NIP-65 allows both)
|
||||||
|
inboxRelays.push(tag[1]); |
||||||
|
console.debug('[kind24_utils] Added inbox relay:', tag[1]); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
console.debug('[kind24_utils] Final inbox relays:', inboxRelays); |
||||||
|
return inboxRelays; |
||||||
|
} catch (error) { |
||||||
|
console.info('[kind24_utils] Error fetching user inbox relays:', error); |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a kind 24 public message reply according to NIP-A4 |
||||||
|
* @param content The message content |
||||||
|
* @param recipientPubkey The recipient's pubkey |
||||||
|
* @param originalEvent The original event being replied to (optional) |
||||||
|
* @returns Promise resolving to publish result with relay information |
||||||
|
*/ |
||||||
|
export async function createKind24Reply( |
||||||
|
content: string, |
||||||
|
recipientPubkey: string, |
||||||
|
originalEvent?: NDKEvent |
||||||
|
): Promise<{ success: boolean; eventId?: string; error?: string; relays?: string[] }> { |
||||||
|
const ndk = get(ndkInstance); |
||||||
|
if (!ndk?.activeUser) { |
||||||
|
return { success: false, error: "Not logged in" }; |
||||||
|
} |
||||||
|
|
||||||
|
if (!content.trim()) { |
||||||
|
return { success: false, error: "Message content cannot be empty" }; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Get sender's outbox relays (NIP-65)
|
||||||
|
const senderoutboxRelays = await getUseroutboxRelays(ndk, ndk.activeUser); |
||||||
|
|
||||||
|
// Get recipient's inbox relays (NIP-65)
|
||||||
|
const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); |
||||||
|
const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); |
||||||
|
|
||||||
|
// According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver
|
||||||
|
// and the outbox relay of the sender
|
||||||
|
const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; |
||||||
|
|
||||||
|
// Prioritize common relays between sender and recipient for better privacy
|
||||||
|
const commonRelays = senderoutboxRelays.filter(relay =>
|
||||||
|
recipientinboxRelays.includes(relay) |
||||||
|
); |
||||||
|
const senderOnlyRelays = senderoutboxRelays.filter(relay =>
|
||||||
|
!recipientinboxRelays.includes(relay) |
||||||
|
); |
||||||
|
const recipientOnlyRelays = recipientinboxRelays.filter(relay =>
|
||||||
|
!senderoutboxRelays.includes(relay) |
||||||
|
); |
||||||
|
|
||||||
|
// Prioritize: common relays first, then sender outbox, then recipient inbox
|
||||||
|
const prioritizedRelays = [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; |
||||||
|
|
||||||
|
if (prioritizedRelays.length === 0) { |
||||||
|
return { success: false, error: "No relays available for publishing" }; |
||||||
|
} |
||||||
|
|
||||||
|
// Create the kind 24 event
|
||||||
|
const event = new NDKEvent(ndk); |
||||||
|
event.kind = 24; |
||||||
|
|
||||||
|
// Build content with quoted message if replying
|
||||||
|
let finalContent = content; |
||||||
|
if (originalEvent) { |
||||||
|
// Use multiple relays for better discoverability
|
||||||
|
const nevent = nip19.neventEncode({
|
||||||
|
id: originalEvent.id, |
||||||
|
relays: prioritizedRelays.slice(0, 3) // Use first 3 relays
|
||||||
|
}); |
||||||
|
const quotedContent = originalEvent.content ? originalEvent.content.slice(0, 200) : "No content"; |
||||||
|
// Use a more visible quote format with a clickable link
|
||||||
|
finalContent = `> QUOTED: ${quotedContent}\n> LINK: ${nevent}\n\n${content}`; |
||||||
|
console.log("[kind24_utils] Reply content:", finalContent); |
||||||
|
} |
||||||
|
|
||||||
|
event.content = finalContent; |
||||||
|
event.created_at = Math.floor(Date.now() / 1000); |
||||||
|
|
||||||
|
// Add p tag for recipient with relay URL
|
||||||
|
const tags: string[][] = [ |
||||||
|
["p", recipientPubkey, prioritizedRelays[0]] // Use first relay as primary
|
||||||
|
]; |
||||||
|
|
||||||
|
// Add q tag if replying to an original event
|
||||||
|
if (originalEvent) { |
||||||
|
const nevent = nip19.neventEncode({
|
||||||
|
id: originalEvent.id, |
||||||
|
relays: prioritizedRelays.slice(0, 3) // Use first 3 relays
|
||||||
|
}); |
||||||
|
tags.push(["q", nevent, prioritizedRelays[0]]); |
||||||
|
} |
||||||
|
|
||||||
|
event.tags = tags; |
||||||
|
event.pubkey = ndk.activeUser.pubkey; |
||||||
|
|
||||||
|
// Sign the event
|
||||||
|
await event.sign(); |
||||||
|
|
||||||
|
// Publish to relays
|
||||||
|
const relaySet = NDKRelaySet.fromRelayUrls(prioritizedRelays, ndk); |
||||||
|
const publishedToRelays = await event.publish(relaySet); |
||||||
|
|
||||||
|
if (publishedToRelays.size > 0) { |
||||||
|
return { success: true, eventId: event.id, relays: prioritizedRelays }; |
||||||
|
} else { |
||||||
|
return { success: false, error: "Failed to publish to any relays", relays: prioritizedRelays }; |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error("[kind24_utils] Error creating kind 24 reply:", error); |
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error"
|
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets optimal relay set for kind 24 messages between two users |
||||||
|
* @param senderPubkey The sender's pubkey |
||||||
|
* @param recipientPubkey The recipient's pubkey |
||||||
|
* @returns Promise resolving to relay URLs prioritized by commonality |
||||||
|
*/ |
||||||
|
export async function getKind24RelaySet( |
||||||
|
senderPubkey: string, |
||||||
|
recipientPubkey: string |
||||||
|
): Promise<string[]> { |
||||||
|
const ndk = get(ndkInstance); |
||||||
|
if (!ndk) { |
||||||
|
throw new Error("NDK not available"); |
||||||
|
} |
||||||
|
|
||||||
|
// Get sender's outbox relays (NIP-65)
|
||||||
|
const senderUser = ndk.getUser({ pubkey: senderPubkey }); |
||||||
|
const senderoutboxRelays = await getUseroutboxRelays(ndk, senderUser); |
||||||
|
|
||||||
|
// Get recipient's inbox relays (NIP-65)
|
||||||
|
const recipientUser = ndk.getUser({ pubkey: recipientPubkey }); |
||||||
|
const recipientinboxRelays = await getUserinboxRelays(ndk, recipientUser); |
||||||
|
|
||||||
|
// According to NIP-A4: Messages MUST be sent to the NIP-65 inbox relays of each receiver
|
||||||
|
// and the outbox relay of the sender
|
||||||
|
const targetRelays = [...new Set([...senderoutboxRelays, ...recipientinboxRelays])]; |
||||||
|
|
||||||
|
// Prioritize common relays between sender and recipient for better privacy
|
||||||
|
const commonRelays = senderoutboxRelays.filter((relay: string) =>
|
||||||
|
recipientinboxRelays.includes(relay) |
||||||
|
); |
||||||
|
const senderOnlyRelays = senderoutboxRelays.filter((relay: string) =>
|
||||||
|
!recipientinboxRelays.includes(relay) |
||||||
|
); |
||||||
|
const recipientOnlyRelays = recipientinboxRelays.filter((relay: string) =>
|
||||||
|
!senderoutboxRelays.includes(relay) |
||||||
|
); |
||||||
|
|
||||||
|
// Prioritize: common relays first, then sender outbox, then recipient inbox
|
||||||
|
return [...commonRelays, ...senderOnlyRelays, ...recipientOnlyRelays]; |
||||||
|
} |
||||||
@ -0,0 +1,166 @@ |
|||||||
|
/** |
||||||
|
* Simplifies a URL by removing protocol and common prefixes |
||||||
|
* @param url The URL to simplify |
||||||
|
* @returns Simplified URL string |
||||||
|
*/ |
||||||
|
function simplifyUrl(url: string): string { |
||||||
|
try { |
||||||
|
const urlObj = new URL(url); |
||||||
|
return urlObj.hostname + (urlObj.port ? `:${urlObj.port}` : ''); |
||||||
|
} catch { |
||||||
|
// If URL parsing fails, return the original string
|
||||||
|
return url; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface RelayInfo { |
||||||
|
name?: string; |
||||||
|
description?: string; |
||||||
|
icon?: string; |
||||||
|
pubkey?: string; |
||||||
|
contact?: string; |
||||||
|
supported_nips?: number[]; |
||||||
|
software?: string; |
||||||
|
version?: string; |
||||||
|
tags?: string[]; |
||||||
|
payments_url?: string; |
||||||
|
limitation?: { |
||||||
|
auth_required?: boolean; |
||||||
|
payment_required?: boolean; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export interface RelayInfoWithMetadata extends RelayInfo { |
||||||
|
url: string; |
||||||
|
shortUrl: string; |
||||||
|
hasNip11: boolean; |
||||||
|
triedNip11: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches relay information using NIP-11 |
||||||
|
* @param url The relay URL to fetch info for |
||||||
|
* @returns Promise resolving to relay info or undefined if failed |
||||||
|
*/ |
||||||
|
export async function fetchRelayInfo(url: string): Promise<RelayInfoWithMetadata | undefined> { |
||||||
|
try { |
||||||
|
// Convert WebSocket URL to HTTP URL for NIP-11
|
||||||
|
const httpUrl = url.replace('ws://', 'http://').replace('wss://', 'https://'); |
||||||
|
|
||||||
|
const response = await fetch(httpUrl, { |
||||||
|
headers: {
|
||||||
|
'Accept': 'application/nostr+json', |
||||||
|
'User-Agent': 'Alexandria/1.0' |
||||||
|
}, |
||||||
|
// Add timeout to prevent hanging
|
||||||
|
signal: AbortSignal.timeout(5000) |
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
console.warn(`[RelayInfo] HTTP ${response.status} for ${url}`); |
||||||
|
return { |
||||||
|
url, |
||||||
|
shortUrl: simplifyUrl(url), |
||||||
|
hasNip11: false, |
||||||
|
triedNip11: true |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const relayInfo = await response.json() as RelayInfo; |
||||||
|
|
||||||
|
return { |
||||||
|
...relayInfo, |
||||||
|
url, |
||||||
|
shortUrl: simplifyUrl(url), |
||||||
|
hasNip11: Object.keys(relayInfo).length > 0, |
||||||
|
triedNip11: true |
||||||
|
}; |
||||||
|
} catch (error) { |
||||||
|
console.warn(`[RelayInfo] Failed to fetch info for ${url}:`, error); |
||||||
|
return { |
||||||
|
url, |
||||||
|
shortUrl: simplifyUrl(url), |
||||||
|
hasNip11: false, |
||||||
|
triedNip11: true |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches relay information for multiple relays in parallel |
||||||
|
* @param urls Array of relay URLs to fetch info for |
||||||
|
* @returns Promise resolving to array of relay info objects |
||||||
|
*/ |
||||||
|
export async function fetchRelayInfos(urls: string[]): Promise<RelayInfoWithMetadata[]> { |
||||||
|
if (urls.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
const promises = urls.map(url => fetchRelayInfo(url)); |
||||||
|
const results = await Promise.allSettled(promises); |
||||||
|
|
||||||
|
return results |
||||||
|
.map(result => result.status === 'fulfilled' ? result.value : undefined) |
||||||
|
.filter((info): info is RelayInfoWithMetadata => info !== undefined); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets relay type label based on relay URL and info |
||||||
|
* @param relayUrl The relay URL |
||||||
|
* @param relayInfo Optional relay info |
||||||
|
* @returns String describing the relay type |
||||||
|
*/ |
||||||
|
export function getRelayTypeLabel(relayUrl: string, relayInfo?: RelayInfoWithMetadata): string { |
||||||
|
// Check if it's a local relay
|
||||||
|
if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) { |
||||||
|
return 'Local'; |
||||||
|
} |
||||||
|
|
||||||
|
// Check if it's a community relay
|
||||||
|
if (relayUrl.includes('nostr.band') || relayUrl.includes('noswhere.com') ||
|
||||||
|
relayUrl.includes('damus.io') || relayUrl.includes('nostr.wine')) { |
||||||
|
return 'Community'; |
||||||
|
} |
||||||
|
|
||||||
|
// Check if it's a user's relay (likely inbox/outbox)
|
||||||
|
if (relayUrl.includes('relay.nsec.app') || relayUrl.includes('relay.snort.social')) { |
||||||
|
return 'User'; |
||||||
|
} |
||||||
|
|
||||||
|
// Use relay name if available
|
||||||
|
if (relayInfo?.name) { |
||||||
|
return relayInfo.name; |
||||||
|
} |
||||||
|
|
||||||
|
// Fallback to domain
|
||||||
|
try { |
||||||
|
const domain = new URL(relayUrl).hostname; |
||||||
|
return domain.replace('www.', ''); |
||||||
|
} catch { |
||||||
|
return 'Unknown'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets relay icon URL or fallback |
||||||
|
* @param relayInfo Relay info object |
||||||
|
* @param relayUrl Relay URL as fallback |
||||||
|
* @returns Icon URL or undefined |
||||||
|
*/ |
||||||
|
export function getRelayIcon(relayInfo?: RelayInfoWithMetadata, relayUrl?: string): string | undefined { |
||||||
|
if (relayInfo?.icon) { |
||||||
|
return relayInfo.icon; |
||||||
|
} |
||||||
|
|
||||||
|
// Generate favicon URL from relay URL
|
||||||
|
if (relayUrl) { |
||||||
|
try { |
||||||
|
const url = new URL(relayUrl); |
||||||
|
return `${url.protocol}//${url.hostname}/favicon.ico`; |
||||||
|
} catch { |
||||||
|
// Invalid URL, return undefined
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return undefined; |
||||||
|
} |
||||||
Loading…
Reference in new issue