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;
}
}