sigit.io/src/controllers/MetadataController.ts

357 lines
11 KiB
TypeScript
Raw Normal View History

import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import {
Event,
EventTemplate,
Filter,
VerifiedEvent,
kinds,
nip19,
validateEvent,
verifyEvent
} from 'nostr-tools'
2024-03-01 10:16:35 +00:00
import { toast } from 'react-toastify'
import { EventEmitter } from 'tseep'
import { NostrController, relayController } from '.'
import { localCache } from '../services'
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
import {
findRelayListAndUpdateCache,
findRelayListInCache,
getDefaultRelaySet,
getMostPopularRelays,
2024-08-07 15:24:12 +00:00
getUserRelaySet,
isOlderThanOneWeek,
queryNip05,
unixNow
} from '../utils'
export class MetadataController extends EventEmitter {
2024-03-01 10:16:35 +00:00
private nostrController: NostrController
private specialMetadataRelay = 'wss://purplepag.es'
constructor() {
super()
2024-03-01 10:16:35 +00:00
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> {
// 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
2024-07-05 08:38:04 +00:00
if (
!currentEvent ||
metadataEvent.created_at >= currentEvent.created_at
) {
// Handle the new metadata event
this.handleNewMetadataEvent(metadataEvent)
}
2024-07-05 08:38:04 +00:00
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<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-07-05 08:38:04 +00:00
// Check if the cached metadata is older than one week
2024-08-07 15:24:12 +00:00
if (isOlderThanOneWeek(cachedMetadataEvent.cachedAt)) {
2024-07-05 08:38:04 +00:00
// 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> => {
2024-08-07 15:24:12 +00:00
const relayEvent =
(await findRelayListInCache(hexKey)) ||
(await findRelayListAndUpdateCache(
[this.specialMetadataRelay],
hexKey
)) ||
(await findRelayListAndUpdateCache(await getMostPopularRelays(), hexKey))
2024-08-07 15:24:12 +00:00
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
}
public extractProfileMetadataContent = (event: Event) => {
try {
if (!event.content) return {}
2024-03-01 10:16:35 +00:00
return JSON.parse(event.content) as ProfileMetadata
} 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) {
const timestamp = unixNow()
2024-03-01 10:16:35 +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
}
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!')
}
2024-03-01 10:16:35 +00:00
})
.catch((err) => {
toast.error(err.message)
})
}
public getNostrJoiningBlockNumber = async (
hexKey: string
): Promise<NostrJoiningBlock | null> => {
const relaySet = await this.findRelayListMetadata(hexKey)
const userRelays: string[] = []
2024-05-10 10:27:34 +00:00
// find user's relays
if (relaySet.write.length > 0) {
userRelays.push(...relaySet.write)
} else {
const metadata = await this.findMetadata(hexKey)
if (!metadata) return null
const metadataContent = this.extractProfileMetadataContent(metadata)
if (metadataContent?.nip05) {
const nip05Profile = await queryNip05(metadataContent.nip05)
if (nip05Profile && nip05Profile.pubkey === hexKey) {
userRelays.push(...nip05Profile.relays)
}
}
}
if (userRelays.length === 0) return null
// filter for finding user's first kind 0 event
const eventFilter: Filter = {
kinds: [kinds.Metadata],
authors: [hexKey]
}
// find user's kind 0 event published on user's relays
const event = await relayController.fetchEvent(eventFilter, userRelays)
2024-05-10 10:27:34 +00:00
if (event) {
const { created_at } = event
2024-05-10 10:27:34 +00:00
// initialize job request
const jobEventTemplate: EventTemplate = {
content: '',
created_at: unixNow(),
kind: 68001,
tags: [
['i', `${created_at * 1000}`],
['j', 'blockChain-block-number']
]
}
2024-05-10 10:27:34 +00:00
// sign job request event
2024-05-15 08:50:21 +00:00
const jobSignedEvent =
await this.nostrController.signEvent(jobEventTemplate)
const relays = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relayable.org'
]
await relayController.publish(jobSignedEvent, relays).catch((err) => {
console.error(
'Error occurred in publish blockChain-block-number DVM job',
err
)
})
const subscribeWithTimeout = (
subscription: NDKSubscription,
timeoutMs: number
): Promise<string> => {
return new Promise((resolve, reject) => {
const eventHandler = (event: NDKEvent) => {
subscription.stop()
resolve(event.content)
}
subscription.on('event', eventHandler)
// Set up a timeout to stop the subscription after a specified time
const timeout = setTimeout(() => {
subscription.stop() // Stop the subscription
reject(new Error('Subscription timed out')) // Reject the promise with a timeout error
}, timeoutMs)
// Handle subscription close event
subscription.on('close', () => clearTimeout(timeout))
})
}
const dvmNDK = new NDK({
explicitRelayUrls: relays
})
await dvmNDK.connect(2000)
2024-05-10 10:27:34 +00:00
// filter for getting DVM job's result
const sub = dvmNDK.subscribe({
kinds: [68002 as number],
'#e': [jobSignedEvent.id],
'#p': [jobSignedEvent.pubkey]
})
// asynchronously get block number from dvm job with 20 seconds timeout
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
const encodedEventPointer = nip19.neventEncode({
id: event.id,
relays: userRelays,
author: event.pubkey,
kind: event.kind
})
return {
block: parseInt(dvmJobResult),
encodedEventPointer
}
}
return null
}
2024-03-01 10:16:35 +00:00
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
public getEmptyMetadataEvent = (): Event => {
return {
content: '',
created_at: new Date().valueOf(),
id: '',
kind: 0,
pubkey: '',
sig: '',
tags: []
}
}
}