f9fcfb1c9e
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
Also remove the getMostPopularRelays function and use a hardcoded list of relays Further, if metadata event is not found from relays cache an empty metadata for that pubkey
213 lines
6.7 KiB
TypeScript
213 lines
6.7 KiB
TypeScript
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,
|
|
getUserRelaySet,
|
|
isOlderThanOneDay,
|
|
unixNow
|
|
} from '../utils'
|
|
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
|
|
|
|
export class MetadataController extends EventEmitter {
|
|
private nostrController: NostrController
|
|
private specialMetadataRelay = 'wss://purplepag.es'
|
|
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
|
|
|
|
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<Event | null> {
|
|
// Return the ongoing fetch promise if one exists for the same hexKey
|
|
if (this.pendingFetches.has(hexKey)) {
|
|
return this.pendingFetches.get(hexKey)!
|
|
}
|
|
|
|
// Define the event filter to only include metadata events authored by the given key
|
|
const eventFilter: Filter = {
|
|
kinds: [kinds.Metadata],
|
|
authors: [hexKey]
|
|
}
|
|
|
|
const fetchPromise = relayController
|
|
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
|
|
.catch((err) => {
|
|
console.error(err)
|
|
return null
|
|
})
|
|
.finally(() => {
|
|
this.pendingFetches.delete(hexKey)
|
|
})
|
|
|
|
this.pendingFetches.set(hexKey, fetchPromise)
|
|
|
|
const metadataEvent = await fetchPromise
|
|
|
|
if (
|
|
metadataEvent &&
|
|
validateEvent(metadataEvent) &&
|
|
verifyEvent(metadataEvent)
|
|
) {
|
|
if (
|
|
!currentEvent ||
|
|
metadataEvent.created_at >= currentEvent.created_at
|
|
) {
|
|
this.handleNewMetadataEvent(metadataEvent)
|
|
}
|
|
return metadataEvent
|
|
}
|
|
|
|
// todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST
|
|
// try to query user relay list
|
|
|
|
// if current event is null we should cache empty metadata event for provided hexKey
|
|
if (!currentEvent) {
|
|
const emptyMetadata = this.getEmptyMetadataEvent(hexKey)
|
|
this.handleNewMetadataEvent(emptyMetadata as VerifiedEvent)
|
|
}
|
|
|
|
return currentEvent
|
|
}
|
|
|
|
/**
|
|
* 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<Event | null> => {
|
|
// 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 (isOlderThanOneDay(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<RelaySet> => {
|
|
const relayEvent =
|
|
(await findRelayListInCache(hexKey)) ||
|
|
(await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, 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 = (pubkey?: string): Event => {
|
|
return {
|
|
content: '',
|
|
created_at: new Date().valueOf(),
|
|
id: '',
|
|
kind: 0,
|
|
pubkey: pubkey || '',
|
|
sig: '',
|
|
tags: []
|
|
}
|
|
}
|
|
}
|