UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.

204 lines (183 loc) 7.13 kB
import createDebug from "debug"; import type { NDKEvent } from "../../events/index.js"; import type { NDK } from "../../ndk/index.js"; import { chooseRelayCombinationForPubkeys } from "../../outbox/index.js"; import { getRelaysForFilterWithAuthors } from "../../outbox/read/with-authors.js"; import { getWriteRelaysFor } from "../../outbox/write.js"; import type { NDKFilter } from "../../subscription/index.js"; import type { Hexpubkey } from "../../user/index.js"; import { normalizeRelayUrl } from "../../utils/normalize-url.js"; import type { NDKRelay } from "../index.js"; import type { NDKPool } from "../pool/index.js"; import { NDKRelaySet } from "./index.js"; const d = createDebug("ndk:outbox:calculate"); /** * Creates a NDKRelaySet for the specified event. * TODO: account for relays where tagged pubkeys or hashtags * tend to write to. * @param ndk {NDK} * @param event {Event} * @returns Promise<NDKRelaySet> */ export async function calculateRelaySetFromEvent( ndk: NDK, event: NDKEvent, requiredRelayCount?: number, ): Promise<NDKRelaySet> { const relays: Set<NDKRelay> = new Set(); // get the author's write relays const authorWriteRelays = await getWriteRelaysFor(ndk, event.pubkey); if (authorWriteRelays) { authorWriteRelays.forEach((relayUrl) => { const relay = ndk.pool?.getRelay(relayUrl); if (relay) relays.add(relay); }); } // get all the hinted relays let relayHints = event.tags .filter((tag) => ["a", "e"].includes(tag[0])) .map((tag) => tag[2]) // verify it's a valid URL .filter((url: string | undefined) => url?.startsWith("wss://")) .filter((url: string) => { try { new URL(url); return true; } catch { return false; } }) .map((url: string) => normalizeRelayUrl(url)); // make unique relayHints = Array.from(new Set(relayHints)).slice(0, 5); relayHints.forEach((relayUrl) => { const relay = ndk.pool?.getRelay(relayUrl, true, true); if (relay) { d("Adding relay hint %s", relayUrl); relays.add(relay); } }); const pTags = event.getMatchingTags("p").map((tag) => tag[1]); if (pTags.length < 5) { const pTaggedRelays = Array.from( chooseRelayCombinationForPubkeys(ndk, pTags, "read", { preferredRelays: new Set(authorWriteRelays), }).keys(), ); pTaggedRelays.forEach((relayUrl) => { const relay = ndk.pool?.getRelay(relayUrl, false, true); if (relay) { d("Adding p-tagged relay %s", relayUrl); relays.add(relay); } }); } else { d("Too many p-tags to consider %d", pTags.length); } ndk.pool?.permanentAndConnectedRelays().forEach((relay: NDKRelay) => relays.add(relay)); // if we have less than the required relay count, add from ndk.explicitRelayUrls (skipping the ones that we already have) if (requiredRelayCount && relays.size < requiredRelayCount) { const explicitRelays = ndk.explicitRelayUrls ?.filter((url) => !Array.from(relays).some((r) => r.url === url)) .slice(0, requiredRelayCount - relays.size); explicitRelays?.forEach((url) => { const relay = ndk.pool?.getRelay(url, false, true); if (relay) { d("Adding explicit relay %s", url); relays.add(relay); } }); } return new NDKRelaySet(relays, ndk); } /** * Creates a map of relay URLs that should receive a subset of the filter. * * The filter is broken up into the filter that each relay should receive. * @param ndk * @param filter * @param pool * @param relayGoalPerAuthor - Number of relays to query for each author (default: 2) * @returns Promise<NDKRelaySet> */ export function calculateRelaySetsFromFilter( ndk: NDK, filters: NDKFilter[], pool: NDKPool, relayGoalPerAuthor?: number, ): Map<WebSocket["url"], NDKFilter[]> { const result = new Map<WebSocket["url"], NDKFilter[]>(); const authors = new Set<Hexpubkey>(); filters.forEach((filter) => { if (filter.authors) { filter.authors.forEach((author) => authors.add(author)); } }); // if this filter has authors, get write relays for each // one of them and add them to the map if (authors.size > 0) { const authorToRelaysMap = getRelaysForFilterWithAuthors(ndk, Array.from(authors), relayGoalPerAuthor); // initialize all result with all the relayUrls we are going to return for (const relayUrl of authorToRelaysMap.keys()) { result.set(relayUrl, []); } // go through all the authorToRelaysMap and replace the authors of each filter with // the resulting author set for (const filter of filters) { if (filter.authors) { // replace authors with the authors for each relay for (const [relayUrl, authors] of authorToRelaysMap.entries()) { const authorFilterAndRelayPubkeyIntersection = filter.authors.filter((author) => authors.includes(author), ); result.set(relayUrl, [ ...result.get(relayUrl)!, { ...filter, // Overwrite authors sent to this relay with the authors that were // present in the filter and are also present in the relay authors: authorFilterAndRelayPubkeyIntersection, }, ]); } } else { // if the filter doesn't have authors, add it to all relays for (const relayUrl of authorToRelaysMap.keys()) { result.set(relayUrl, [...result.get(relayUrl)!, filter]); } } } } else { // If we don't, add the explicit relays if (ndk.explicitRelayUrls) { ndk.explicitRelayUrls.forEach((relayUrl) => { result.set(relayUrl, filters); }); } } if (result.size === 0) { // If we don't have any relays, add all the permanent relays pool.permanentAndConnectedRelays() .slice(0, 5) .forEach((relay) => { result.set(relay.url, filters); }); } return result; } /** * Calculates a number of RelaySets for each filter. * @param ndk * @param filters * @param pool * @param relayGoalPerAuthor - Number of relays to query for each author (default: 2) */ export function calculateRelaySetsFromFilters( ndk: NDK, filters: NDKFilter[], pool: NDKPool, relayGoalPerAuthor?: number, ): Map<WebSocket["url"], NDKFilter[]> { const a = calculateRelaySetsFromFilter(ndk, filters, pool, relayGoalPerAuthor); return a; }