/** * Unified user data fetcher * Consolidates profile, status, and relay list fetching */ import { nostrClient } from './nostr/nostr-client.js'; import { relayManager } from './nostr/relay-manager.js'; import { cacheProfile, getProfile, getProfiles } from './cache/profile-cache.js'; import { config } from './nostr/config.js'; import type { NostrEvent } from '../types/nostr.js'; // Re-export profile types and functions export interface ProfileData { name?: string; about?: string; picture?: string; website?: string[]; nip05?: string[]; lud16?: string[]; } /** * Parse profile from kind 0 event */ export function parseProfile(event: NostrEvent): ProfileData { const profile: ProfileData = {}; // Try to parse from tags first (preferred) const nameTag = event.tags.find((t) => t[0] === 'name'); if (nameTag && nameTag[1]) profile.name = nameTag[1]; const aboutTag = event.tags.find((t) => t[0] === 'about'); if (aboutTag && aboutTag[1]) profile.about = aboutTag[1]; const pictureTag = event.tags.find((t) => t[0] === 'picture'); if (pictureTag && pictureTag[1]) profile.picture = pictureTag[1]; // Multiple tags for website, nip05, lud16 profile.website = event.tags.filter((t) => t[0] === 'website').map((t) => t[1]).filter(Boolean); profile.nip05 = event.tags.filter((t) => t[0] === 'nip05').map((t) => t[1]).filter(Boolean); profile.lud16 = event.tags.filter((t) => t[0] === 'lud16').map((t) => t[1]).filter(Boolean); // Fallback to JSON content if tags not found if (!profile.name || !profile.about) { try { const json = JSON.parse(event.content); if (json.name && !profile.name) profile.name = json.name; if (json.about && !profile.about) profile.about = json.about; if (json.picture && !profile.picture) profile.picture = json.picture; if (json.website && profile.website.length === 0) { profile.website = Array.isArray(json.website) ? json.website : [json.website]; } if (json.nip05 && profile.nip05.length === 0) { profile.nip05 = Array.isArray(json.nip05) ? json.nip05 : [json.nip05]; } if (json.lud16 && profile.lud16.length === 0) { profile.lud16 = Array.isArray(json.lud16) ? json.lud16 : [json.lud16]; } } catch { // Invalid JSON, ignore } } return profile; } /** * Fetch profile for a pubkey */ export async function fetchProfile( pubkey: string, relays?: string[] ): Promise { // Try cache first const cached = await getProfile(pubkey); if (cached) { return parseProfile(cached.event); } // Fetch from relays const relayList = relays || [ ...config.defaultRelays, ...config.profileRelays ]; const events = await nostrClient.fetchEvents( [{ kinds: [0], authors: [pubkey], limit: 1 }], relayList, { useCache: true, cacheResults: true } ); if (events.length === 0) return null; const event = events[0]; await cacheProfile(event); return parseProfile(event); } /** * Fetch multiple profiles */ export async function fetchProfiles( pubkeys: string[], relays?: string[] ): Promise> { const profiles = new Map(); // Check cache first const cached = await getProfiles(pubkeys); for (const [pubkey, cachedProfile] of cached.entries()) { profiles.set(pubkey, parseProfile(cachedProfile.event)); } // Fetch missing profiles const missing = pubkeys.filter((p) => !profiles.has(p)); if (missing.length === 0) return profiles; const relayList = relays || [ ...config.defaultRelays, ...config.profileRelays ]; const events = await nostrClient.fetchEvents( [{ kinds: [0], authors: missing, limit: 1 }], relayList, { useCache: true, cacheResults: true } ); for (const event of events) { await cacheProfile(event); profiles.set(event.pubkey, parseProfile(event)); } return profiles; } /** * Parse user status from kind 30315 event */ export function parseUserStatus(event: NostrEvent): string | null { if (event.kind !== 30315) return null; // Check for d tag with value "general" const dTag = event.tags.find((t) => t[0] === 'd' && t[1] === 'general'); if (!dTag) return null; return event.content || null; } /** * Fetch user status for a pubkey */ export async function fetchUserStatus( pubkey: string, relays?: string[] ): Promise { const relayList = relays || [ ...config.defaultRelays, ...config.profileRelays ]; const events = await nostrClient.fetchEvents( [ { kinds: [30315], authors: [pubkey], '#d': ['general'], limit: 1 } as any // NIP-38 uses #d tag for parameterized replaceable events ], relayList, { useCache: true, cacheResults: true } ); if (events.length === 0) return null; return parseUserStatus(events[0]); } export interface RelayInfo { url: string; read: boolean; write: boolean; } /** * Parse relay list from event */ export function parseRelayList(event: NostrEvent): RelayInfo[] { const relays: RelayInfo[] = []; for (const tag of event.tags) { if (tag[0] === 'r' && tag[1]) { const url = tag[1]; const markers = tag.slice(2); // If no markers, relay is both read and write if (markers.length === 0) { relays.push({ url, read: true, write: true }); continue; } // Check for explicit markers const hasRead = markers.includes('read'); const hasWrite = markers.includes('write'); // If only 'read' marker: read=true, write=false // If only 'write' marker: read=false, write=true // If both or neither explicitly: both true (default behavior) const read = hasRead || (!hasRead && !hasWrite); const write = hasWrite || (!hasRead && !hasWrite); relays.push({ url, read, write }); } } return relays; } /** * Fetch relay lists for a pubkey (kind 10002 and 10432) */ export async function fetchRelayLists( pubkey: string, relays?: string[] ): Promise<{ inbox: string[]; outbox: string[]; }> { const relayList = relays || [ ...config.defaultRelays, ...config.profileRelays ]; // Fetch both kind 10002 and 10432 const events = await nostrClient.fetchEvents( [ { kinds: [10002], authors: [pubkey], limit: 1 }, { kinds: [10432], authors: [pubkey], limit: 1 } ], relayList, { useCache: true, cacheResults: true } ); const inbox: string[] = []; const outbox: string[] = []; for (const event of events) { const relayInfos = parseRelayList(event); for (const info of relayInfos) { if (info.read && !inbox.includes(info.url)) { inbox.push(info.url); } if (info.write && !outbox.includes(info.url)) { outbox.push(info.url); } } } // Deduplicate return { inbox: [...new Set(inbox)], outbox: [...new Set(outbox)] }; }