import NDK, { getRelayListForUser, NDKEvent, NDKFilter, NDKKind, NDKRelaySet, NDKSubscriptionCacheUsage, NDKUser } from '@nostr-dev-kit/ndk' import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie' import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts' import { UserRelaysType } from 'controllers' import { Dexie } from 'dexie' import { createContext, ReactNode, useEffect, useMemo } from 'react' import { toast } from 'react-toastify' import { ModDetails, UserProfile } from 'types' import { constructModListFromEvents, hexToNpub, log, LogType, npubToHex, orderEventsChronologically } from 'utils' type FetchModsOptions = { source?: string until?: number since?: number limit?: number } interface NDKContextType { ndk: NDK fetchMods: (opts: FetchModsOptions) => Promise fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise fetchEvent: ( filter: NDKFilter, relayUrls?: string[] ) => Promise fetchEventsFromUserRelays: ( filter: NDKFilter, hexKey: string, userRelaysType: UserRelaysType ) => Promise fetchEventFromUserRelays: ( filter: NDKFilter, hexKey: string, userRelaysType: UserRelaysType ) => Promise findMetadata: (pubkey: string) => Promise } // Create the context with an initial value of `null` export const NDKContext = createContext(null) // Create a provider component to wrap around parts of your app export const NDKContextProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { window.onunhandledrejection = async (event: PromiseRejectionEvent) => { event.preventDefault() console.log(event.reason) if (event.reason?.name === Dexie.errnames.DatabaseClosed) { console.log( 'Could not open Dexie DB, probably version change. Deleting old DB and reloading...' ) await Dexie.delete('degmod-db') // Must reload to open a brand new DB window.location.reload() } } }, []) const ndk = useMemo(() => { localStorage.setItem('debug', '*') const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' }) dexieAdapter.locking = true const ndk = new NDK({ enableOutboxModel: true, autoConnectUserRelays: true, autoFetchUserMutelist: true, explicitRelayUrls: [ 'wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.damus.io/', import.meta.env.VITE_APP_RELAY ], cacheAdapter: dexieAdapter }) ndk.connect() return ndk }, []) /** * Fetches a list of mods based on the provided source. * * @param source - The source URL to filter the mods. If it matches the current window location, * it adds a filter condition to the request. * @param until - Optional timestamp to filter events until this time. * @param since - Optional timestamp to filter events from this time. * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error, * it logs the error and shows a notification, then returns an empty array. */ const fetchMods = async ({ source, until, since, limit }: FetchModsOptions): Promise => { const relays = new Set() relays.add(import.meta.env.VITE_APP_RELAY) const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') const promises = adminNpubs.map((npub) => { const hexKey = npubToHex(npub) if (!hexKey) return null return getRelayListForUser(hexKey, ndk) .then((ndkRelayList) => { if (ndkRelayList) { ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url)) } }) .catch((err) => { log( true, LogType.Error, `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`, err ) }) }) await Promise.allSettled(promises) // Define the filter criteria for fetching mods const filter: NDKFilter = { kinds: [NDKKind.Classified], // Specify the kind of events to fetch limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 '#t': [T_TAG_VALUE], until, // Optional filter to fetch events until this timestamp since // Optional filter to fetch events from this timestamp } // If the source matches the current window location, add a filter condition if (source === window.location.host) { filter['#r'] = [window.location.host] // Add a tag filter for the current host } return ndk .fetchEvents( filter, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) ) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) orderEventsChronologically(ndkEvents) // Convert the fetched events into a list of mods const modList = constructModListFromEvents(ndkEvents) return modList // Return the list of mods }) .catch((err) => { // Log the error and show a notification if fetching fails log( true, LogType.Error, 'An error occurred in fetching mods from relays', err ) toast.error('An error occurred in fetching mods from relays') // Show error notification return [] // Return an empty array in case of an error }) } /** * Asynchronously retrieves multiple event from a set of relays based on a provided filter. * If no relays are specified, it defaults to using connected relays. * * @param filter - The filter criteria to find the event. * @param relays - An optional array of relay URLs to search for the event. * @returns Returns a promise that resolves to the found event or null if not found. */ const fetchEvents = async ( filter: NDKFilter, relayUrls: string[] = [] ): Promise => { const relays = new Set() // add all the relays passed to relay set relayUrls.forEach((relayUrl) => { relays.add(relayUrl) }) relays.add(import.meta.env.VITE_APP_RELAY) const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') const promises = adminNpubs.map((npub) => { const hexKey = npubToHex(npub) if (!hexKey) return null return getRelayListForUser(hexKey, ndk) .then((ndkRelayList) => { if (ndkRelayList) { ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url)) } }) .catch((err) => { log( true, LogType.Error, `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`, err ) }) }) await Promise.allSettled(promises) return ndk .fetchEvents( filter, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true) ) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) return orderEventsChronologically(ndkEvents) }) .catch((err) => { // Log the error and show a notification if fetching fails log(true, LogType.Error, 'An error occurred in fetching events', err) toast.error('An error occurred in fetching events') // Show error notification return [] // Return an empty array in case of an error }) } /** * Asynchronously retrieves an event from a set of relays based on a provided filter. * If no relays are specified, it defaults to using connected relays. * * @param filter - The filter criteria to find the event. * @param relaysUrls - An optional array of relay URLs to search for the event. * @returns Returns a promise that resolves to the found event or null if not found. */ const fetchEvent = async (filter: NDKFilter, relayUrls: string[] = []) => { const events = await fetchEvents(filter, relayUrls) if (events.length === 0) return null return events[0] } /** * Asynchronously retrieves multiple events from the user's relays based on a specified filter. * The function first retrieves the user's relays, and then fetches the events using the provided filter. * * @param filter - The event filter to use when fetching the event (e.g., kinds, authors). * @param hexKey - The hexadecimal representation of the user's public key. * @param userRelaysType - The type of relays to search (e.g., write, read). * @returns A promise that resolves with an array of events. */ const fetchEventsFromUserRelays = async ( filter: NDKFilter, hexKey: string, userRelaysType: UserRelaysType ) => { // Find the user's relays. const relayUrls = await getRelayListForUser(hexKey, ndk) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList[userRelaysType] return [] // Return an empty array if ndkRelayList is undefined }) .catch((err) => { log( true, LogType.Error, `An error occurred in fetching user's (${hexKey}) ${userRelaysType}`, err ) return [] as string[] }) // Fetch the event from the user's relays using the provided filter and relay URLs return fetchEvents(filter, relayUrls) } /** * Fetches an event from the user's relays based on a specified filter. * The function first retrieves the user's relays, and then fetches the event using the provided filter. * * @param filter - The event filter to use when fetching the event (e.g., kinds, authors). * @param hexKey - The hexadecimal representation of the user's public key. * @param userRelaysType - The type of relays to search (e.g., write, read). * @returns A promise that resolves to the fetched event or null if the operation fails. */ const fetchEventFromUserRelays = async ( filter: NDKFilter, hexKey: string, userRelaysType: UserRelaysType ) => { const events = await fetchEventsFromUserRelays( filter, hexKey, userRelaysType ) if (events.length === 0) return null return events[0] } /** * Finds metadata for a given pubkey. * * @param hexKey - The pubkey to search for metadata. * @returns A promise that resolves to the metadata event. */ const findMetadata = async (pubkey: string): Promise => { const npub = hexToNpub(pubkey) const user = new NDKUser({ npub }) user.ndk = ndk const userProfile = await user.fetchProfile() return userProfile } return ( {children} ) }