UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit

277 lines (235 loc) 9.03 kB
import { nip19 } from "nostr-tools"; import type { NDK } from "../ndk/index.js"; import { NDKRelay } from "../relay/index.js"; import { NDKRelaySet } from "../relay/sets/index.js"; import type { EventPointer } from "../user/index.js"; import type { NDKFilter, NDKSubscription } from "./index.js"; /** * Don't generate subscription Ids longer than this amount of characters * (plus 4-chars random number) */ const MAX_SUBID_LENGTH = 20; /** * Checks if a subscription is fully guaranteed to have been filled. * * This is useful to determine if a cache hit fully satisfies a subscription. * * @param subscription * @returns */ export function queryFullyFilled(subscription: NDKSubscription): boolean { if (filterIncludesIds(subscription.filter)) { if (resultHasAllRequestedIds(subscription)) { return true; } } return false; } /** * Compares whether a filter includes another filter. * @param filter1 Filter to compare from * @param filter2 Filter to compare to * @example * const filter1 = { authors: ["a", "b"] }; * const filter2 = { authors: ["a", "b", "c"] }; * compareFilter(filter1, filter2); // true * * const filter1 = { authors: ["a", "b"] }; * const filter2 = { authors: ["a", "c"] }; * compareFilter(filter1, filter2); // false * @returns */ export function compareFilter(filter1: NDKFilter, filter2: NDKFilter) { // Make sure the filters have the same number of keys if (Object.keys(filter1).length !== Object.keys(filter2).length) return false; for (const [key, value] of Object.entries(filter1)) { const valuesInFilter2 = filter2[key as keyof NDKFilter] as string[]; if (!valuesInFilter2) return false; if (Array.isArray(value) && Array.isArray(valuesInFilter2)) { const v: string[] = value as string[]; // make sure all values in the filter are in the other filter for (const valueInFilter2 of valuesInFilter2) { const val: string = valueInFilter2 as string; if (!v.includes(val)) { return false; } } } else { if (valuesInFilter2 !== value) return false; } } return true; } function filterIncludesIds(filter: NDKFilter): boolean { return !!filter.ids; } function resultHasAllRequestedIds(subscription: NDKSubscription): boolean { const ids = subscription.filter.ids; return !!ids && ids.length === subscription.eventFirstSeen.size; } /** * Generates a subscription ID based on the subscriptions and filter. * * When some of the subscriptions specify a subId, those are used, * joining them with a comma. * * If none of the subscriptions specify a subId, a subId is generated * by joining all the filter keys, and expanding the kinds with the requested kinds. */ export function generateSubId(subscriptions: NDKSubscription[], filters: NDKFilter[]): string { const subIds = subscriptions.map((sub) => sub.subId).filter(Boolean); const subIdParts: string[] = []; const filterNonKindKeys = new Set<string>(); const filterKinds = new Set<number>(); if (subIds.length > 0) { subIdParts.push(Array.from(new Set(subIds)).join(",")); } else { for (const filter of filters) { for (const key of Object.keys(filter)) { if (key === "kinds") { filter.kinds?.forEach((k) => filterKinds.add(k)); } else { filterNonKindKeys.add(key); } } } if (filterKinds.size > 0) { subIdParts.push(`kinds:${Array.from(filterKinds).join(",")}`); } if (filterNonKindKeys.size > 0) { subIdParts.push(Array.from(filterNonKindKeys).join(",")); } } let subId = subIdParts.join("-"); if (subId.length > MAX_SUBID_LENGTH) subId = subId.substring(0, MAX_SUBID_LENGTH); // Add the random string to the resulting subId subId += `-${Math.floor(Math.random() * 999).toString()}`; return subId; } /** * Creates a valid nostr filter to REQ events that are tagging a NIP-19 bech32 * @param id Bech32 of the event * @example * const bech32 = "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr" * const filter = filterForEventsTaggingId(bech32); * // filter => { "#e": [<id>] } * * @example * const bech32 = "naddr1qvzqqqr4gupzpjjwt0eqm6as279wf079c0j42jysp2t4s37u8pg5w2dfyktxgkntqqxnzde38yen2desxqmn2d3332u3ff"; * const filter = filterForEventsTaggingId(bech32); * // filter => { "#a": ["30023:ca4e5bf20debb0578ae4bfc5c3e55548900a975847dc38514729a92596645a6b:1719357007561"]} */ export function filterForEventsTaggingId(id: string): NDKFilter | undefined { try { const decoded = nip19.decode(id); switch (decoded.type) { case "naddr": return { "#a": [`${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`], }; case "nevent": return { "#e": [decoded.data.id] }; case "note": return { "#e": [decoded.data] }; case "nprofile": return { "#p": [decoded.data.pubkey] }; case "npub": return { "#p": [decoded.data] }; } } catch {} } /** * Creates a valid nostr filter from a bech32 encoding along with a relay set (if one is present in the encoding). * @param id Bech32 of the event * @param ndk * @returns * @example * const bech32 = "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr"; * const { filter, relaySet } = filterAndRelaySetFromBech32(bech32, ndk); * // filter => { ids: [...], authors: [...] } */ export function filterAndRelaySetFromBech32(bech32: string, ndk: NDK): { filter: NDKFilter; relaySet?: NDKRelaySet } { const filter = filterFromId(bech32); const relays = relaysFromBech32(bech32, ndk); if (relays.length === 0) return { filter }; return { filter, relaySet: new NDKRelaySet(new Set(relays), ndk), }; } /** * Creates a valid nostr filter from an event id or a NIP-19 bech32. * * @example * const bech32 = "nevent1qgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtcqype6ycavy2e9zpx9mzeuekaahgw96ken0mzkcmgz40ljccwyrn88gxv2ewr" * const filter = filterFromBech32(bech32); * // filter => { ids: [...], authors: [...] } */ export function filterFromId(id: string): NDKFilter { let decoded; if (id.match(NIP33_A_REGEX)) { const [kind, pubkey, identifier] = id.split(":"); const filter: NDKFilter = { authors: [pubkey], kinds: [Number.parseInt(kind)], }; if (identifier) { filter["#d"] = [identifier]; } return filter; } if (id.match(BECH32_REGEX)) { try { decoded = nip19.decode(id); switch (decoded.type) { case "nevent": { const filter: NDKFilter = { ids: [decoded.data.id] }; if (decoded.data.author) filter.authors = [decoded.data.author]; if (decoded.data.kind) filter.kinds = [decoded.data.kind]; return filter; } case "note": return { ids: [decoded.data] }; case "naddr": { const filter: NDKFilter = { authors: [decoded.data.pubkey], kinds: [decoded.data.kind], }; if (decoded.data.identifier) filter["#d"] = [decoded.data.identifier]; return filter; } } } catch (e) { console.error("Error decoding", id, e); // Empty } } return { ids: [id] }; } export function isNip33AValue(value: string): boolean { return value.match(NIP33_A_REGEX) !== null; } /** * Matches an `a` tag of a NIP-33 (kind:pubkey:[identifier]) */ export const NIP33_A_REGEX = /^(\d+):([0-9A-Fa-f]+)(?::(.*))?$/; export const BECH32_REGEX = /^n(event|ote|profile|pub|addr)1[\d\w]+$/; /** * Returns the specified relays from a NIP-19 bech32. * * @param bech32 The NIP-19 bech32. */ export function relaysFromBech32(bech32: string, ndk: NDK): NDKRelay[] { try { const decoded = nip19.decode(bech32); if (["naddr", "nevent"].includes(decoded?.type)) { const data = decoded.data as unknown as EventPointer; if (data?.relays) { return data.relays.map((r: string) => new NDKRelay(r, ndk.relayAuthDefaultPolicy, ndk)); } } } catch (_e) { /* empty */ } return []; }