139 lines
3.6 KiB
TypeScript
139 lines
3.6 KiB
TypeScript
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<string> {
|
|
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<string> {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|