import { Event, Filter, VerifiedEvent, kinds, validateEvent, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' import { EventEmitter } from 'tseep' import { NostrController, relayController } from '.' import { localCache } from '../services' import { ProfileMetadata, RelaySet } from '../types' import { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelaySet, getMostPopularRelays, getUserRelaySet, isOlderThanOneWeek, unixNow } from '../utils' export class MetadataController extends EventEmitter { private nostrController: NostrController private specialMetadataRelay = 'wss://purplepag.es' constructor() { super() this.nostrController = NostrController.getInstance() } /** * Asynchronously checks for more recent metadata events authored by a specific key. * If a more recent metadata event is found, it is handled and returned. * If no more recent event is found, the current event is returned. * @param hexKey The hexadecimal key of the author to filter metadata events. * @param currentEvent The current metadata event, if any, to compare with newer events. * @returns A promise resolving to the most recent metadata event found, or null if none is found. */ private async checkForMoreRecentMetadata( hexKey: string, currentEvent: Event | null ): Promise { // Define the event filter to only include metadata events authored by the given key const eventFilter: Filter = { kinds: [kinds.Metadata], // Only metadata events authors: [hexKey] // Authored by the specified key } // Try to get the metadata event from a special relay (wss://purplepag.es) const metadataEvent = await relayController .fetchEvent(eventFilter, [this.specialMetadataRelay]) .catch((err) => { console.error(err) // Log any errors return null // Return null if an error occurs }) // If a valid metadata event is found from the special relay if ( metadataEvent && validateEvent(metadataEvent) && // Validate the event verifyEvent(metadataEvent) // Verify the event's authenticity ) { // If there's no current event or the new metadata event is more recent if ( !currentEvent || metadataEvent.created_at >= currentEvent.created_at ) { // Handle the new metadata event this.handleNewMetadataEvent(metadataEvent) } return metadataEvent } // If no valid metadata event is found from the special relay, get the most popular relays const mostPopularRelays = await getMostPopularRelays() // Query the most popular relays for metadata events const events = await relayController .fetchEvents(eventFilter, mostPopularRelays) .catch((err) => { console.error(err) // Log any errors return null // Return null if an error occurs }) // If events are found from the popular relays if (events && events.length) { events.sort((a, b) => b.created_at - a.created_at) // Sort events by creation date (descending) // Iterate through the events for (const event of events) { // If the event is valid, authentic, and more recent than the current event if ( validateEvent(event) && verifyEvent(event) && (!currentEvent || event.created_at > currentEvent.created_at) ) { // Handle the new metadata event this.handleNewMetadataEvent(event) return event } } } return currentEvent // Return the current event if no newer event is found } /** * Handle new metadata events and emit them to subscribers */ private async handleNewMetadataEvent(event: VerifiedEvent) { // update the event in local cache localCache.addUserMetadata(event) // Emit the event to subscribers. this.emit(event.pubkey, event.kind, event) } /** * Finds metadata for a given hexadecimal key. * * @param hexKey - The hexadecimal key to search for metadata. * @returns A promise that resolves to the metadata event. */ public findMetadata = async (hexKey: string): Promise => { // Attempt to retrieve the metadata event from the local cache const cachedMetadataEvent = await localCache.getUserMetadata(hexKey) // If cached metadata is found, check its validity if (cachedMetadataEvent) { // Check if the cached metadata is older than one week if (isOlderThanOneWeek(cachedMetadataEvent.cachedAt)) { // If older than one week, find the metadata from relays in background this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event) } // Return the cached metadata event return cachedMetadataEvent.event } // If no cached metadata is found, retrieve it from relays return this.checkForMoreRecentMetadata(hexKey, null) } /** * Based on the hexKey of the current user, this method attempts to retrieve a relay set. * @func findRelayListInCache first checks if there is already an up-to-date * relay list available in cache; if not - * @func findRelayListAndUpdateCache checks if the relevant relay event is available from * the purple pages relay; * @func findRelayListAndUpdateCache will run again if the previous two calls return null and * check if the relevant relay event can be obtained from 'most popular relays' * If relay event is found, it will be saved in cache for future use * @param hexKey of the current user * @return RelaySet which will contain either relays extracted from the user Relay Event * or a fallback RelaySet with Sigit's Relay */ public findRelayListMetadata = async (hexKey: string): Promise => { const relayEvent = (await findRelayListInCache(hexKey)) || (await findRelayListAndUpdateCache( [this.specialMetadataRelay], hexKey )) || (await findRelayListAndUpdateCache(await getMostPopularRelays(), hexKey)) return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet() } public extractProfileMetadataContent = (event: Event) => { try { if (!event.content) return {} return JSON.parse(event.content) as ProfileMetadata } catch (error) { console.log('error in parsing metadata event content :>> ', error) return null } } /** * Function will not sign provided event if the SIG exists */ public publishMetadataEvent = async (event: Event) => { let signedMetadataEvent = event if (event.sig.length < 1) { const timestamp = unixNow() // Metadata event to publish to the wss://purplepag.es relay const newMetadataEvent: Event = { ...event, created_at: timestamp } signedMetadataEvent = await this.nostrController.signEvent(newMetadataEvent) } await relayController .publish(signedMetadataEvent, [this.specialMetadataRelay]) .then((relays) => { if (relays.length) { toast.success(`Metadata event published on: ${relays.join('\n')}`) this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent) } else { toast.error('Could not publish metadata event to any relay!') } }) .catch((err) => { toast.error(err.message) }) } public validate = (event: Event) => validateEvent(event) && verifyEvent(event) public getEmptyMetadataEvent = (): Event => { return { content: '', created_at: new Date().valueOf(), id: '', kind: 0, pubkey: '', sig: '', tags: [] } } }