import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools' import { RelayList } from 'nostr-tools/kinds' import { getRelayInfo, unixNow } from '.' import { NostrController, relayController } from '../controllers' import { localCache } from '../services' import { RelayMap, RelaySet } from '../types' import { DEFAULT_LOOK_UP_RELAY_LIST, ONE_WEEK_IN_MS, SIGIT_RELAY } from './const' const READ_MARKER = 'read' const WRITE_MARKER = 'write' /** * Attempts to find a relay list from the provided lookUpRelays. * If the relay list is found, it will be added to the user relay list metadata. * @param lookUpRelays * @param hexKey * @return found relay list or null */ const findRelayListAndUpdateCache = async ( lookUpRelays: string[], hexKey: string ): Promise => { try { const eventFilter: Filter = { kinds: [RelayList], authors: [hexKey] } console.count('findRelayListAndUpdateCache') const event = await relayController.fetchEvent(eventFilter, lookUpRelays) if (event) { await localCache.addUserRelayListMetadata(event) } return event } catch (error) { console.error(error) return null } } /** * Attempts to find a relay list in cache. If it is present, it will check that the cached event is not * older than one week. * @param hexKey * @return RelayList event if it's not older than a week; otherwise null */ const findRelayListInCache = async (hexKey: string): Promise => { try { // Attempt to retrieve the metadata event from the local cache const cachedRelayListMetadataEvent = await localCache.getUserRelayListMetadata(hexKey) // Check if the cached event is not older than one week if ( cachedRelayListMetadataEvent && isOlderThanOneWeek(cachedRelayListMetadataEvent.cachedAt) ) { return cachedRelayListMetadataEvent.event } return null } catch (error) { console.error(error) return null } } /** * Transforms a list of relay tags from a Nostr Event to a RelaySet. * @param tags */ const getUserRelaySet = (tags: string[][]): RelaySet => { return tags .filter(isRelayTag) .reduce(toRelaySet, getDefaultRelaySet()) } const getDefaultRelaySet = (): RelaySet => ({ read: [SIGIT_RELAY], write: [SIGIT_RELAY] }) const getDefaultRelayMap = (): RelayMap => ({ [SIGIT_RELAY]: { write: true, read: true } }) const isOlderThanOneWeek = (cachedAt: number) => { return Date.now() - cachedAt < ONE_WEEK_IN_MS } const isOlderThanOneDay = (cachedAt: number) => { return Date.now() - cachedAt < ONE_WEEK_IN_MS } const isRelayTag = (tag: string[]): boolean => tag[0] === 'r' const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => { if (tag.length >= 3) { const marker = tag[2] if (marker === READ_MARKER) { obj.read.push(tag[1]) } else if (marker === WRITE_MARKER) { obj.write.push(tag[1]) } } if (tag.length === 2) { obj.read.push(tag[1]) obj.write.push(tag[1]) } return obj } /** * Provides relay map. * @param npub - user's npub * @returns - promise that resolves into relay map and a timestamp when it has been updated. */ const getRelayMap = async ( npub: string ): Promise<{ map: RelayMap; mapUpdated?: number }> => { // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md const eventFilter: Filter = { kinds: [kinds.RelayList], authors: [npub] } console.count('getRelayMap') const event = await relayController .fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST) .catch((err) => { return Promise.reject(err) }) if (event) { // Handle founded 10002 event const relaysMap: RelayMap = {} // 'r' stands for 'relay' const relayTags = event.tags.filter((tag) => tag[0] === 'r') relayTags.forEach((tag) => { const uri = tag[1] const relayType = tag[2] // if 3rd element of relay tag is undefined, relay is WRITE and READ relaysMap[uri] = { write: relayType ? relayType === 'write' : true, read: relayType ? relayType === 'read' : true } }) Object.keys(relaysMap).forEach((relayUrl) => { console.log('being called from getRelayMap') relayController.connectRelay(relayUrl) }) getRelayInfo(Object.keys(relaysMap)) return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at }) } else { return Promise.resolve({ map: getDefaultRelayMap() }) } } /** * Publishes relay map. * @param relayMap - relay map. * @param npub - user's npub. * @param extraRelaysToPublish - optional relays to publish relay map. * @returns - promise that resolves into a string representing publishing result. */ const publishRelayMap = async ( relayMap: RelayMap, npub: string, extraRelaysToPublish?: string[] ): Promise => { const timestamp = unixNow() const relayURIs = Object.keys(relayMap) // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md const tags: string[][] = relayURIs.map((relayURI) => [ 'r', relayURI, relayMap[relayURI].read && relayMap[relayURI].write ? '' : relayMap[relayURI].write ? 'write' : 'read' ].filter((value) => value !== '') ) const newRelayMapEvent: UnsignedEvent = { kind: kinds.RelayList, tags, content: '', pubkey: npub, created_at: timestamp } const nostrController = NostrController.getInstance() const signedEvent = await nostrController.signEvent(newRelayMapEvent) let relaysToPublish = relayURIs // Add extra relays if provided if (extraRelaysToPublish) { relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish] } // If relay map is empty, use most popular relay URIs if (!relaysToPublish.length) { relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST } const publishResult = await relayController.publish( signedEvent, relaysToPublish ) if (publishResult && publishResult.length) { return Promise.resolve( `Relay Map published on: ${publishResult.join('\n')}` ) } return Promise.reject('Publishing updated relay map was unsuccessful.') } export { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelayMap, getDefaultRelaySet, getRelayMap, getUserRelaySet, isOlderThanOneDay, isOlderThanOneWeek, publishRelayMap }