clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/npm_and_yarn/npm-f01a459b0f
bindws / src / filter.ts
filter.ts 2.8K
1// NIP-01 filters: parsing, in-memory matching for live subscriptions, and2// NIP-50 search term handling. Mirrors filter.go.3import { isPrivate, type Event } from "./event.ts";45export interface Filter {6 ids?: string[];7 authors?: string[];8 kinds?: number[];9 tags: Record<string, string[]>; // "#e" -> tags.e10 since?: number;11 until?: number;12 limit?: number;13 search?: string;14}1516// parse validates a raw filter object. Returns an error string or the Filter.17export function parseFilter(raw: unknown): Filter | string {18 if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "filter must be an object";19 const f: Filter = { tags: {} };20 const strList = (v: unknown, k: string): string[] | string =>21 Array.isArray(v) && v.every((x) => typeof x === "string") ? (v as string[]) : `${k} must be a list of strings`;22 for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {23 if (k === "ids" || k === "authors") {24 const r = strList(v, k);25 if (typeof r === "string") return r;26 f[k] = r;27 } else if (k === "kinds") {28 if (!Array.isArray(v) || !v.every((x) => Number.isInteger(x))) return "kinds must be a list of integers";29 f.kinds = v as number[];30 } else if (k === "since" || k === "until" || k === "limit") {31 if (!Number.isInteger(v)) return `${k} must be an integer`;32 f[k] = v as number;33 } else if (k === "search") {34 if (typeof v !== "string") return "search must be a string";35 f.search = v;36 } else if (k.length === 2 && k[0] === "#") {37 const r = strList(v, k);38 if (typeof r === "string") return r;39 f.tags[k[1]] = r;40 }41 }42 return f;43}4445export function match(f: Filter, e: Event): boolean {46 if (f.ids && !f.ids.includes(e.id)) return false;47 if (f.authors && !f.authors.includes(e.pubkey)) return false;48 if (f.kinds && !f.kinds.includes(e.kind)) return false;49 if (f.since !== undefined && e.created_at < f.since) return false;50 if (f.until !== undefined && e.created_at > f.until) return false;51 for (const [name, vals] of Object.entries(f.tags)) {52 if (!e.tags.some((t) => t[0] === name && t.length > 1 && vals.includes(t[1]))) return false;53 }54 const terms = searchTerms(f.search);55 if (terms.length > 0) {56 if (isPrivate(e.kind)) return false;57 const content = e.content.toLowerCase();58 for (const term of terms) if (!content.includes(term.toLowerCase())) return false;59 }60 return true;61}6263// searchTerms drops key:value extensions this relay does not implement.64export function searchTerms(q: string | undefined): string[] {65 if (!q) return [];66 return q.split(/\s+/).filter((w) => w && !w.includes(":"));67}6869// ftsQuery quotes every term so user input cannot inject FTS5 syntax.70export function ftsQuery(terms: string[]): string {71 return terms.map((t) => '"' + t.replaceAll('"', '""') + '"').join(" ");72}