import { get } from 'svelte/store'; import { nip19 } from 'nostr-tools'; import { ndkInstance } from '$lib/ndk'; import { npubCache } from './npubCache'; import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; import { standardRelays, bootstrapRelays } from "$lib/consts"; // Regular expressions for Nostr identifiers - match the entire identifier including any prefix export const NOSTR_PROFILE_REGEX = /(?': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, char => htmlEscapes[char]); } /** * Get user metadata for a nostr identifier (npub or nprofile) */ export async function getUserMetadata(identifier: string): Promise<{name?: string, displayName?: string, nip05?: string}> { // Remove nostr: prefix if present const cleanId = identifier.replace(/^nostr:/, ''); if (npubCache.has(cleanId)) { return npubCache.get(cleanId)!; } const fallback = { name: `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}` }; try { const ndk = get(ndkInstance); if (!ndk) { npubCache.set(cleanId, fallback); return fallback; } const decoded = nip19.decode(cleanId); if (!decoded) { npubCache.set(cleanId, fallback); return fallback; } // Handle different identifier types let pubkey: string; if (decoded.type === 'npub') { pubkey = decoded.data; } else if (decoded.type === 'nprofile') { pubkey = decoded.data.pubkey; } else { npubCache.set(cleanId, fallback); return fallback; } const user = ndk.getUser({ pubkey: pubkey }); if (!user) { npubCache.set(cleanId, fallback); return fallback; } try { const profile = await user.fetchProfile(); if (!profile) { npubCache.set(cleanId, fallback); return fallback; } const metadata = { name: profile.name || fallback.name, displayName: profile.displayName, nip05: profile.nip05 }; npubCache.set(cleanId, metadata); return metadata; } catch (e) { npubCache.set(cleanId, fallback); return fallback; } } catch (e) { npubCache.set(cleanId, fallback); return fallback; } } /** * Create a profile link element */ function createProfileLink(identifier: string, displayText: string | undefined): string { const cleanId = identifier.replace(/^nostr:/, ''); const escapedId = escapeHtml(cleanId); const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const escapedText = escapeHtml(displayText || defaultText); return `@${escapedText}`; } /** * Create a note link element */ function createNoteLink(identifier: string): string { const cleanId = identifier.replace(/^nostr:/, ''); const shortId = `${cleanId.slice(0, 12)}...${cleanId.slice(-8)}`; const escapedId = escapeHtml(cleanId); const escapedText = escapeHtml(shortId); return `${escapedText}`; } /** * Process Nostr identifiers in text */ export async function processNostrIdentifiers(content: string): Promise { let processedContent = content; // Helper to check if a match is part of a URL function isPartOfUrl(text: string, index: number): boolean { // Look for http(s):// or www. before the match const before = text.slice(Math.max(0, index - 12), index); return /https?:\/\/$|www\.$/i.test(before); } // Process profiles (npub and nprofile) const profileMatches = Array.from(content.matchAll(NOSTR_PROFILE_REGEX)); for (const match of profileMatches) { const [fullMatch] = match; const matchIndex = match.index ?? 0; if (isPartOfUrl(content, matchIndex)) { continue; // skip if part of a URL } let identifier = fullMatch; if (!identifier.startsWith('nostr:')) { identifier = 'nostr:' + identifier; } const metadata = await getUserMetadata(identifier); const displayText = metadata.displayName || metadata.name; const link = createProfileLink(identifier, displayText); processedContent = processedContent.replace(fullMatch, link); } // Process notes (nevent, note, naddr) const noteMatches = Array.from(processedContent.matchAll(NOSTR_NOTE_REGEX)); for (const match of noteMatches) { const [fullMatch] = match; const matchIndex = match.index ?? 0; if (isPartOfUrl(processedContent, matchIndex)) { continue; // skip if part of a URL } let identifier = fullMatch; if (!identifier.startsWith('nostr:')) { identifier = 'nostr:' + identifier; } const link = createNoteLink(identifier); processedContent = processedContent.replace(fullMatch, link); } return processedContent; } export async function getNpubFromNip05(nip05: string): Promise { try { const ndk = get(ndkInstance); if (!ndk) { console.error('NDK not initialized'); return null; } const user = await ndk.getUser({ nip05 }); if (!user || !user.npub) { return null; } return user.npub; } catch (error) { console.error('Error getting npub from nip05:', error); return null; } } /** * Fetches an event using a two-step relay strategy: * 1. First tries standard relays with timeout * 2. Falls back to all relays if not found * Always wraps result as NDKEvent */ export async function fetchEventWithFallback( ndk: NDK, filterOrId: string | NDKFilter, timeoutMs: number = 3000 ): Promise { const allRelays = Array.from(new Set([...standardRelays, ...bootstrapRelays])); const relaySets = [ NDKRelaySet.fromRelayUrls(standardRelays, ndk), NDKRelaySet.fromRelayUrls(allRelays, ndk) ]; async function withTimeout(promise: Promise): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs)) ]); } try { let found: NDKEvent | null = null; // Try standard relays first if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) { found = await withTimeout(ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySets[0])); if (!found) { // Fallback to all relays found = await withTimeout(ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySets[1])); } } else { const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId; const results = await withTimeout(ndk.fetchEvents(filter, undefined, relaySets[0])); found = results instanceof Set ? Array.from(results)[0] as NDKEvent : null; if (!found) { // Fallback to all relays const fallbackResults = await withTimeout(ndk.fetchEvents(filter, undefined, relaySets[1])); found = fallbackResults instanceof Set ? Array.from(fallbackResults)[0] as NDKEvent : null; } } if (!found) { console.warn('Event not found after timeout. Some relays may be offline or slow.'); return null; } // Always wrap as NDKEvent return found instanceof NDKEvent ? found : new NDKEvent(ndk, found); } catch (err) { console.error('Error in fetchEventWithFallback:', err); return null; } }