import type {Event} from 'nostr-tools' import normalizeUrl from "normalize-url" import {verifyEvent, getEventHash, matchFilter as nostrToolsMatchFilter} from 'nostr-tools' import {cached} from "./LRUCache" import {now} from './misc' // =========================================================================== // Relays export const stripProto = (url: string) => url.replace(/.*:\/\//, "") export const isShareableRelay = (url: string) => Boolean( typeof url === 'string' && // Is it actually a websocket url and has a dot url.match(/^wss?:\/\/.+\..+/) && // Sometimes bugs cause multiple relays to get concatenated url.match(/:\/\//g)?.length === 1 && // It shouldn't have any whitespace !url.match(/\s/) && // It shouldn't have any url-encoded whitespace !url.match(/%/) && // Is it secure url.match(/^wss:\/\/.+/) && // Don't match stuff with a port number !url.slice(6).match(/:\d+/) && // Don't match raw ip addresses !url.slice(6).match(/\d+\.\d+\.\d+\.\d+/) && // Skip nostr.wine's virtual relays !url.slice(6).match(/\/npub/) ) export const normalizeRelayUrl = (url: string) => { // Use our library to normalize url = normalizeUrl(url, {stripHash: true, stripAuthentication: false}) // Strip the protocol since only wss works url = stripProto(url) // Urls without pathnames are supposed to have a trailing slash if (!url.includes("/")) { url += "/" } return "wss://" + url } // =========================================================================== // Nostr URIs export const fromNostrURI = (s: string) => s.replace(/^[\w+]+:\/?\/?/, "") export const toNostrURI = (s: string) => `nostr:${s}` // =========================================================================== // Events export type CreateEventOpts = { content?: string tags?: string[][] created_at?: number } export const createEvent = (kind: number, {content = "", tags = [], created_at = now()}: CreateEventOpts) => ({kind, content, tags, created_at}) export const hasValidSignature = cached({ maxSize: 10000, getKey: ([e]: [Event]) => { try { return [getEventHash(e), e.sig].join(":") } catch (err) { return 'invalid' } }, getValue: ([e]: [Event]) => { try { return verifyEvent(e) } catch (err) { return false } }, }) // =========================================================================== // Filters export type Filter = { ids?: string[] kinds?: number[] authors?: string[] since?: number until?: number limit?: number search?: string [key: `#${string}`]: string[] } export const matchFilter = (filter: Filter, event: Event) => { if (!nostrToolsMatchFilter(filter, event)) { return false } if (filter.search) { const content = event.content.toLowerCase() const terms = filter.search.toLowerCase().split(/\s+/g) for (const term of terms) { if (content.includes(term)) { return true } return false } } return true } export const matchFilters = (filters: Filter[], event: Event) => { for (const filter of filters) { if (matchFilter(filter, event)) { return true } } return false }