2024-02-28 16:49:44 +00:00
|
|
|
import {
|
2024-08-15 17:13:39 +00:00
|
|
|
Event,
|
2024-02-28 16:49:44 +00:00
|
|
|
Filter,
|
|
|
|
VerifiedEvent,
|
|
|
|
kinds,
|
|
|
|
validateEvent,
|
2024-08-15 17:13:39 +00:00
|
|
|
verifyEvent
|
2024-02-28 16:49:44 +00:00
|
|
|
} from 'nostr-tools'
|
2024-03-01 10:16:35 +00:00
|
|
|
import { toast } from 'react-toastify'
|
2024-05-30 17:28:40 +00:00
|
|
|
import { EventEmitter } from 'tseep'
|
2024-08-15 17:13:39 +00:00
|
|
|
import { NostrController, relayController } from '.'
|
2024-05-30 17:28:40 +00:00
|
|
|
import { localCache } from '../services'
|
2024-08-16 06:42:28 +00:00
|
|
|
import { ProfileMetadata, RelaySet } from '../types'
|
2024-08-07 12:52:14 +00:00
|
|
|
import {
|
|
|
|
findRelayListAndUpdateCache,
|
|
|
|
findRelayListInCache,
|
|
|
|
getDefaultRelaySet,
|
2024-08-07 15:24:12 +00:00
|
|
|
getUserRelaySet,
|
2024-08-20 19:16:21 +00:00
|
|
|
isOlderThanOneDay,
|
2024-08-15 17:13:39 +00:00
|
|
|
unixNow
|
|
|
|
} from '../utils'
|
2024-08-20 19:16:21 +00:00
|
|
|
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
|
2024-02-28 16:49:44 +00:00
|
|
|
|
2024-05-30 17:28:40 +00:00
|
|
|
export class MetadataController extends EventEmitter {
|
2024-03-01 10:16:35 +00:00
|
|
|
private nostrController: NostrController
|
|
|
|
private specialMetadataRelay = 'wss://purplepag.es'
|
2024-08-20 19:16:21 +00:00
|
|
|
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
|
2024-03-01 10:16:35 +00:00
|
|
|
|
|
|
|
constructor() {
|
2024-05-30 17:28:40 +00:00
|
|
|
super()
|
2024-03-01 10:16:35 +00:00
|
|
|
this.nostrController = NostrController.getInstance()
|
|
|
|
}
|
2024-02-28 16:49:44 +00:00
|
|
|
|
2024-05-30 17:28:40 +00:00
|
|
|
/**
|
|
|
|
* 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> {
|
2024-08-20 19:16:21 +00:00
|
|
|
// Return the ongoing fetch promise if one exists for the same hexKey
|
|
|
|
if (this.pendingFetches.has(hexKey)) {
|
|
|
|
return this.pendingFetches.get(hexKey)!
|
|
|
|
}
|
|
|
|
|
2024-05-30 17:28:40 +00:00
|
|
|
// Define the event filter to only include metadata events authored by the given key
|
2024-02-28 16:49:44 +00:00
|
|
|
const eventFilter: Filter = {
|
2024-08-20 19:16:21 +00:00
|
|
|
kinds: [kinds.Metadata],
|
|
|
|
authors: [hexKey]
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|
|
|
|
|
2024-08-20 19:16:21 +00:00
|
|
|
const fetchPromise = relayController
|
|
|
|
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
|
2024-03-19 09:23:34 +00:00
|
|
|
.catch((err) => {
|
2024-08-20 19:16:21 +00:00
|
|
|
console.error(err)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.pendingFetches.delete(hexKey)
|
2024-03-19 09:23:34 +00:00
|
|
|
})
|
|
|
|
|
2024-08-20 19:16:21 +00:00
|
|
|
this.pendingFetches.set(hexKey, fetchPromise)
|
|
|
|
|
|
|
|
const metadataEvent = await fetchPromise
|
|
|
|
|
2024-03-19 09:23:34 +00:00
|
|
|
if (
|
|
|
|
metadataEvent &&
|
2024-08-20 19:16:21 +00:00
|
|
|
validateEvent(metadataEvent) &&
|
|
|
|
verifyEvent(metadataEvent)
|
2024-03-19 09:23:34 +00:00
|
|
|
) {
|
2024-07-05 08:38:04 +00:00
|
|
|
if (
|
|
|
|
!currentEvent ||
|
|
|
|
metadataEvent.created_at >= currentEvent.created_at
|
|
|
|
) {
|
2024-05-30 17:28:40 +00:00
|
|
|
this.handleNewMetadataEvent(metadataEvent)
|
|
|
|
}
|
2024-07-05 08:38:04 +00:00
|
|
|
return metadataEvent
|
2024-03-19 09:23:34 +00:00
|
|
|
}
|
|
|
|
|
2024-08-20 19:16:21 +00:00
|
|
|
// todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST
|
|
|
|
// try to query user relay list
|
2024-03-19 09:23:34 +00:00
|
|
|
|
2024-08-20 19:16:21 +00:00
|
|
|
// 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)
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|
|
|
|
|
2024-08-20 19:16:21 +00:00
|
|
|
return currentEvent
|
2024-05-30 17:28:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2024-08-21 07:41:59 +00:00
|
|
|
// Check if the cached metadata is older than one day
|
2024-08-20 19:16:21 +00:00
|
|
|
if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) {
|
2024-07-05 08:38:04 +00:00
|
|
|
// If older than one week, find the metadata from relays in background
|
2024-05-30 17:28:40 +00:00
|
|
|
|
|
|
|
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)
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|
|
|
|
|
2024-08-07 12:52:14 +00:00
|
|
|
/**
|
|
|
|
* 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> => {
|
2024-08-07 15:24:12 +00:00
|
|
|
const relayEvent =
|
|
|
|
(await findRelayListInCache(hexKey)) ||
|
2024-08-20 19:16:21 +00:00
|
|
|
(await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey))
|
2024-08-07 15:24:12 +00:00
|
|
|
|
|
|
|
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
|
2024-04-16 06:12:29 +00:00
|
|
|
}
|
|
|
|
|
2024-05-30 17:28:40 +00:00
|
|
|
public extractProfileMetadataContent = (event: Event) => {
|
2024-02-28 16:49:44 +00:00
|
|
|
try {
|
2024-05-17 11:33:01 +00:00
|
|
|
if (!event.content) return {}
|
2024-03-01 10:16:35 +00:00
|
|
|
return JSON.parse(event.content) as ProfileMetadata
|
2024-02-28 16:49:44 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.log('error in parsing metadata event content :>> ', error)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
2024-03-01 10:16:35 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function will not sign provided event if the SIG exists
|
|
|
|
*/
|
|
|
|
public publishMetadataEvent = async (event: Event) => {
|
|
|
|
let signedMetadataEvent = event
|
|
|
|
|
|
|
|
if (event.sig.length < 1) {
|
2024-08-13 09:52:05 +00:00
|
|
|
const timestamp = unixNow()
|
2024-03-01 10:16:35 +00:00
|
|
|
|
2024-03-19 09:23:34 +00:00
|
|
|
// Metadata event to publish to the wss://purplepag.es relay
|
2024-03-01 10:16:35 +00:00
|
|
|
const newMetadataEvent: Event = {
|
|
|
|
...event,
|
|
|
|
created_at: timestamp
|
|
|
|
}
|
|
|
|
|
2024-05-15 08:50:21 +00:00
|
|
|
signedMetadataEvent =
|
|
|
|
await this.nostrController.signEvent(newMetadataEvent)
|
2024-03-01 10:16:35 +00:00
|
|
|
}
|
|
|
|
|
2024-08-15 17:13:39 +00:00
|
|
|
await relayController
|
|
|
|
.publish(signedMetadataEvent, [this.specialMetadataRelay])
|
2024-04-16 06:12:29 +00:00
|
|
|
.then((relays) => {
|
2024-08-15 17:13:39 +00:00
|
|
|
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!')
|
|
|
|
}
|
2024-03-01 10:16:35 +00:00
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
toast.error(err.message)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
|
2024-05-30 17:28:40 +00:00
|
|
|
|
2024-08-20 19:16:21 +00:00
|
|
|
public getEmptyMetadataEvent = (pubkey?: string): Event => {
|
2024-05-30 17:28:40 +00:00
|
|
|
return {
|
|
|
|
content: '',
|
|
|
|
created_at: new Date().valueOf(),
|
|
|
|
id: '',
|
|
|
|
kind: 0,
|
2024-08-20 19:16:21 +00:00
|
|
|
pubkey: pubkey || '',
|
2024-05-30 17:28:40 +00:00
|
|
|
sig: '',
|
|
|
|
tags: []
|
|
|
|
}
|
|
|
|
}
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|