nostr-tools/nip29.ts

167 lines
4.2 KiB
TypeScript

import { AbstractSimplePool } from './abstract-pool'
import { Subscription } from './abstract-relay'
import { decode } from './nip19'
import type { Event } from './core'
import { fetchRelayInformation } from './nip11'
import { normalizeURL } from './utils'
import { AddressPointer } from './nip19'
export function subscribeRelayGroups(
pool: AbstractSimplePool,
url: string,
params: {
ongroups: (_: Group[]) => void
onerror: (_: Error) => void
onconnect?: () => void
},
): () => void {
let normalized = normalizeURL(url)
let sub: Subscription
let groups: Group[] = []
fetchRelayInformation(normalized)
.then(async info => {
let rl = await pool.ensureRelay(normalized)
params.onconnect?.()
sub = rl.prepareSubscription(
[
{
kinds: [39000],
limit: 50,
authors: [info.pubkey],
},
],
{
onevent(event: Event) {
groups.push(parseGroup(event, normalized))
},
oneose() {
params.ongroups(groups)
sub.onevent = (event: Event) => {
groups.push(parseGroup(event, normalized))
params.ongroups(groups)
}
},
},
)
sub.fire()
})
.catch(params.onerror)
return () => sub.close()
}
export async function loadGroup(pool: AbstractSimplePool, gr: GroupReference): Promise<Group> {
let normalized = normalizeURL(gr.host)
let info = await fetchRelayInformation(normalized)
let event = await pool.get([normalized], {
kinds: [39000],
authors: [info.pubkey],
'#d': [gr.id],
})
if (!event) throw new Error(`group '${gr.id}' not found on ${gr.host}`)
return parseGroup(event, normalized)
}
export async function loadGroupFromCode(pool: AbstractSimplePool, code: string): Promise<Group> {
let gr = parseGroupCode(code)
if (!gr) throw new Error(`code "${code}" does not identify a group`)
return loadGroup(pool, gr)
}
export type GroupReference = {
id: string
host: string
}
export function parseGroupCode(code: string): null | GroupReference {
if (code.startsWith('naddr1')) {
try {
let { data } = decode(code)
let { relays, identifier } = data as AddressPointer
if (!relays || relays.length === 0) return null
let host = relays![0]
if (host.startsWith('wss://')) {
host = host.slice(6)
}
return { host, id: identifier }
} catch (err) {
return null
}
} else if (code.split("'").length === 2) {
let spl = code.split("'")
return { host: spl[0], id: spl[1] }
}
return null
}
export function encodeGroupReference(gr: GroupReference): string {
if (gr.host.startsWith('https://')) gr.host = gr.host.slice(8)
if (gr.host.startsWith('wss://')) gr.host = gr.host.slice(6)
return `${gr.host}'${gr.id}`
}
export type Group = {
id: string
relay: string
pubkey: string
name?: string
picture?: string
about?: string
public?: boolean
open?: boolean
}
export function parseGroup(event: Event, relay: string): Group {
const group: Partial<Group> = { relay, pubkey: event.pubkey }
for (let i = 0; i < event.tags.length; i++) {
const tag = event.tags[i]
switch (tag[0]) {
case 'd':
group.id = tag[1] || ''
break
case 'name':
group.name = tag[1] || ''
break
case 'about':
group.about = tag[1] || ''
break
case 'picture':
group.picture = tag[1] || ''
break
case 'open':
group.open = true
break
case 'public':
group.public = true
break
}
}
return group as Group
}
export type Member = {
pubkey: string
label?: string
permissions: string[]
}
export function parseMembers(event: Event): Member[] {
const members = []
for (let i = 0; i < event.tags.length; i++) {
const tag = event.tags[i]
if (tag.length < 2) continue
if (tag[0] !== 'p') continue
if (!tag[1].match(/^[0-9a-f]{64}$/)) continue
const member: Member = { pubkey: tag[1], permissions: [] }
if (tag.length > 2) member.label = tag[2]
if (tag.length > 3) member.permissions = tag.slice(3)
members.push(member)
}
return members
}