import * as nip19 from "@nostr/tools/nip19"; import { loadRelayList } from "@nostr/gadgets/lists"; const RELAY_HINT_TIMEOUT_MS = 1500; export function extractMentionPubkeys(content: string): string[] { const re = /nostr:(npub1[a-z0-9]+|nprofile1[a-z0-9]+)/gi; const out = new Set(); for (const m of content.matchAll(re)) { try { const decoded = nip19.decode(m[1]); if (decoded.type === "npub") out.add(decoded.data); else if (decoded.type === "nprofile") out.add(decoded.data.pubkey); } catch { // Invalid bech32, skip } } return [...out]; } export type QuotedEvent = { id: string; relay?: string; author?: string; }; export function extractQuotedEvents(content: string): QuotedEvent[] { const re = /nostr:(note1[a-z0-9]+|nevent1[a-z0-9]+)/gi; const seen = new Map(); for (const m of content.matchAll(re)) { try { const decoded = nip19.decode(m[1]); if (decoded.type === "note") { if (!seen.has(decoded.data)) seen.set(decoded.data, { id: decoded.data }); } else if (decoded.type === "nevent") { const { id, relays, author } = decoded.data; if (!seen.has(id)) seen.set(id, { id, relay: relays?.[0], author }); } } catch { // Invalid bech32, skip } } return [...seen.values()]; } export async function relayHintFor( pubkey: string, ): Promise { try { const TIMEOUT = Symbol(); const result = await Promise.race([ loadRelayList(pubkey), new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), RELAY_HINT_TIMEOUT_MS), ), ]); if (result === TIMEOUT) return undefined; return result.items.find((r) => r.write)?.url; } catch { return undefined; } } export async function buildPTagHints( pubkeys: Iterable, ): Promise> { const hints = new Map(); await Promise.all( [...pubkeys].map(async (pk) => { const url = await relayHintFor(pk); if (url) hints.set(pk, url); }), ); return hints; }