Bpistle/src/services/relay-membership.service.ts
2025-11-09 00:26:16 +08:00

127 lines
3.5 KiB
TypeScript

import { sortEventsDesc } from '@/lib/event'
import { isValidPubkey } from '@/lib/pubkey'
import client from '@/services/client.service'
import DataLoader from 'dataloader'
import { Filter } from 'nostr-tools'
/**
* NIP-43: Relay Access Metadata and Requests
* https://github.com/nostr-protocol/nips/blob/master/43.md
*/
class RelayMembershipService {
private static instance: RelayMembershipService
private membershipListCache: Map<string, Promise<Set<string>>> = new Map()
private membershipListDataLoader = new DataLoader<
{ url: string; pubkey: string },
Set<string>,
string
>(
async (params) => {
return Promise.all(params.map(({ url, pubkey }) => this.fetchMembershipList(url, pubkey)))
},
{ cacheKeyFn: (key) => key.url, cacheMap: this.membershipListCache }
)
public static getInstance(): RelayMembershipService {
if (!RelayMembershipService.instance) {
RelayMembershipService.instance = new RelayMembershipService()
}
return RelayMembershipService.instance
}
/**
* Check if a user is a member of a relay that supports NIP-43
* @param relayUrl The relay URL
* @param userPubkey The user's public key
* @param relayPubkey The relay's public key from NIP-11
* @returns Membership status
*/
async checkMembership(
relayUrl: string,
userPubkey: string,
relayPubkey?: string
): Promise<boolean> {
if (!relayPubkey) {
return false
}
const memberSet = await this.membershipListDataLoader.load({
url: relayUrl,
pubkey: relayPubkey
})
return memberSet.has(userPubkey)
}
private async fetchMembershipList(relayUrl: string, relayPubkey: string): Promise<Set<string>> {
try {
const filter: Filter = {
kinds: [13534],
authors: [relayPubkey],
limit: 1
}
const events = await client.fetchEvents([relayUrl], filter)
if (events.length === 0) {
return new Set()
}
const membershipEvent = sortEventsDesc(events)[0]
const members = membershipEvent.tags
.filter((tag) => tag[0] === 'member' && isValidPubkey(tag[1]))
.map((tag) => tag[1])
return new Set(members)
} catch (error) {
console.error('Error checking relay membership:', error)
return new Set()
}
}
/**
* Request an invite code from a relay (kind 28935)
* @param relayUrl The relay URL
* @param relayPubkey The relay's public key from NIP-11
* @returns Invite code or null
*/
async requestInviteCode(relayUrl: string, relayPubkey: string): Promise<string | null> {
try {
const filter: Filter = {
kinds: [28935],
authors: [relayPubkey],
limit: 1
}
const events = await client.fetchEvents([relayUrl], filter)
if (events.length === 0) {
return null
}
const inviteEvent = events[0]
const claimTag = inviteEvent.tags.find((tag) => tag[0] === 'claim')
return claimTag?.[1] ?? null
} catch (error) {
console.error('Error requesting invite code:', error)
return null
}
}
async addNewMember(relayUrl: string, newMemberPubkey: string) {
const cache = await this.membershipListCache.get(relayUrl)
if (cache) {
cache.add(newMemberPubkey)
}
}
async removeMember(relayUrl: string, memberPubkey: string) {
const cache = await this.membershipListCache.get(relayUrl)
if (cache) {
cache.delete(memberPubkey)
}
}
}
const instance = RelayMembershipService.getInstance()
export default instance