bpist-squalk-forum/src/lib/mentions.ts
2026-05-26 15:49:38 +01:00

76 lines
2.1 KiB
TypeScript

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<string>();
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<string, QuotedEvent>();
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<string | undefined> {
try {
const TIMEOUT = Symbol();
const result = await Promise.race([
loadRelayList(pubkey),
new Promise<typeof TIMEOUT>((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<string>,
): Promise<Map<string, string>> {
const hints = new Map<string, string>();
await Promise.all(
[...pubkeys].map(async (pk) => {
const url = await relayHintFor(pk);
if (url) hints.set(pk, url);
}),
);
return hints;
}