import { get } from 'svelte/store'; import { nip19 } from 'nostr-tools'; import { ndkInstance } from '$lib/ndk'; import { npubCache } from './npubCache'; import { NDKUser } from "@nostr-dev-kit/ndk"; const badgeCheckSvg = '' const graduationCapSvg = ''; // 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}> { // 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 }; 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 */ export 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 profile link element with a NIP-05 verification indicator. */ export async function createProfileLinkWithVerification(identifier: string, displayText: string | undefined): Promise { const ndk = get(ndkInstance) as NDK; if (!ndk) { return createProfileLink(identifier, displayText); } const cleanId = identifier.replace(/^nostr:/, ''); const isNpub = cleanId.startsWith('npub'); let user: NDKUser; if (isNpub) { user = ndk.getUser({ npub: cleanId }); } else { user = ndk.getUser({ pubkey: cleanId }); } const profile = await user.fetchProfile(); const nip05 = profile?.nip05; if (!nip05) { return createProfileLink(identifier, displayText); } const defaultText = `${cleanId.slice(0, 8)}...${cleanId.slice(-4)}`; const escapedText = escapeHtml(displayText || defaultText); const displayIdentifier = profile?.displayName ?? profile?.name ?? escapedText; const isVerified = await user.validateNip05(nip05); if (!isVerified) { return createProfileLink(identifier, displayText); } // TODO: Make this work with an enum in case we add more types. const type = nip05.endsWith('edu') ? 'edu' : 'standard'; switch (type) { case 'edu': return `${graduationCapSvg}@${displayIdentifier}`; case 'standard': return `${badgeCheckSvg}@${displayIdentifier}`; } } /** * 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; } }