import { NostrEvent, NostrFilter, NPool, NRelay1 } from "@nostrify/nostrify"; import { nip19 } from "@nostr/tools"; import { lookupUsernameByPubkey } from "./directory.ts"; import config from "./config.ts"; import Article from "./models/article.ts"; const relayPool = new NPool({ open: (url) => new NRelay1(url), // deno-lint-ignore require-await reqRouter: async (filters) => new Map( config.relay_urls.map((url) => [url, filters]), ), // deno-lint-ignore require-await eventRouter: async (_event) => [], }); async function fetchWithTimeout(filters: NostrFilter[]) { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("relay timeout")), config.query_timeout) ); const eventsPromise = relayPool.query(filters); const events = await Promise.race([eventsPromise, timeoutPromise]); return events; } export async function fetchReplaceableEvent( pubkey: string, identifier: string, ) { let events = await fetchWithTimeout([{ authors: [pubkey], kinds: [30023], "#d": [identifier], limit: 1, }]) as NostrEvent[]; if (events.length > 0) { return events[0]; } else { events = await fetchWithTimeout([{ authors: [pubkey], kinds: [30024], "#d": [identifier], limit: 1, }]) as NostrEvent[]; return events.length > 0 ? events[0] : null; } } export async function fetchArticlesByAuthor( pubkey: string, limit: number = 10, ) { const events = await fetchWithTimeout([{ authors: [pubkey], kinds: [30023], limit: limit, }]) as NostrEvent[]; const articles = events.map((a) => new Article(a)); const sortedArticles = articles.sort((a, b) => b.publishedAt - a.publishedAt); // The limit seems to apply per relay, not per pool query return sortedArticles.slice(0, limit); } export async function fetchProfileEvent(pubkey: string) { const events = await fetchWithTimeout([{ authors: [pubkey], kinds: [0], limit: 1, }]) as NostrEvent[]; return events.length > 0 ? events[0] : null; } export async function nostrUriToUrl(uri: string): Promise { const bech32 = uri.replace(/^nostr:/, ""); if (bech32.match(/^(naddr|nprofile|npub)/)) { try { const r = nip19.decode(bech32); let username; switch (r.type) { case "naddr": username = await lookupUsernameByPubkey(r.data.pubkey); if (username) return `/${bech32}`; break; case "nprofile": username = await lookupUsernameByPubkey(r.data.pubkey); if (username) return `/@${username}`; break; case "npub": username = await lookupUsernameByPubkey(r.data); if (username) return `/@${username}`; break; } } catch (e) { console.error(e); } } return `${config.njump_url}/${bech32}`; } export async function replaceNostrUris(markdown: string): Promise { const nostrUriRegex = /(nostr:|nprofile|naddr|nevent|nrelay|npub)[a-z0-9]+/g; const matches = markdown.match(nostrUriRegex); if (!matches) return markdown; for (const uri of matches) { const url = await nostrUriToUrl(uri); markdown = markdown.replace(uri, url); } return markdown; } export async function verifyNip05Address( address: string, pubkey: string, ): Promise { const [username, host] = address.split("@"); const url = `https://${host}/.well-known/nostr.json?name=${username}`; try { const res = await fetch(url); if (res.status === 404 || !res.ok) return false; const data = await res.json(); return data.names && data.names[username] === pubkey; } catch (_e) { return false; } }