1// NIP-66 relay discovery. Monitors publish kind 30166 records about the2// relays they probe; a relay may say the same things about itself, signed3// with its own key, so a directory finds it without a probe. The d tag is4// the relay's primary URL, the tags restate what NIP-11 says, and the5// content is the NIP-11 document. Nothing here is measured, so there are6// no rtt or geohash tags: those are a monitor's to add.7import type { Relay } from "./relay.ts";8import { nip11, type NipIdentifier } from "./nip11.ts";910export interface Discovery {11 url: string;12 tags: string[][];13 content: string;14}1516// relayTypes names the relay in the vocabulary of nostr-protocol/nips#1282,17// from the read and write rules. Every relay here is one NIP-29 group, so18// one that is not open on both sides is a community manager. Every relay19// has NIP-50 search.20export function relayTypes(writes: string, reads: string): string[] {21 const out: string[] = [];22 if (writes === "open" && reads === "open") out.push("PublicInbox");23 else if (writes === "owner" && reads === "open") out.push("PublicOutbox");24 else if (writes === "owner" && reads === "members") out.push("PrivateStorage");25 else out.push("CommunityManagerRelays");26 out.push("SearchRelays");27 return out;28}2930// discovery builds the record's tags and content for the relay at `host`.31export function discovery(relay: Relay, host: string): Discovery {32 const p = relay.settings.policy;33 const doc = nip11(relay, host);34 const lim = (doc.limitation ?? {}) as Record<string, unknown>;35 const url = new URL(relay.relayURL(host)).toString();36 const tags: string[][] = [["-"], ["d", url], ["n", "clearnet"]];37 for (const t of relayTypes(p.writes, p.reads)) tags.push(["T", t]);38 for (const n of doc.supported_nips as NipIdentifier[]) tags.push(["N", String(n)]);39 const req = (key: string, on: boolean) => tags.push(["R", (on ? "" : "!") + key]);40 req("auth", lim.auth_required === true);41 req("writes", lim.restricted_writes === true);42 req("pow", typeof lim.min_pow_difficulty === "number" && lim.min_pow_difficulty > 0);43 req("payment", lim.payment_required === true);44 for (const t of p.tags) tags.push(["t", t]);45 const langs = new Set<string>();46 for (const l of p.languageTags) {47 const primary = l.split("-")[0].toLowerCase();48 if (primary.length === 2) langs.add(primary);49 }50 for (const l of langs) tags.push(["l", l, "ISO-639-1"]);51 for (const k of relay.settings.listKinds("allow")) tags.push(["k", String(k)]);52 for (const k of relay.settings.listKinds("block")) tags.push(["k", "!" + k]);53 return { url, tags, content: JSON.stringify(doc) };54}