mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2026-09-13 22:05:07 +00:00
update nip29 helpers to latest spec.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nostr/tools",
|
||||
"version": "2.24.1",
|
||||
"version": "2.24.2",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./core": "./core.ts",
|
||||
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import type { Event } from './core.ts'
|
||||
import {
|
||||
Group,
|
||||
GroupAdmin,
|
||||
GroupAdminPermission,
|
||||
GroupMember,
|
||||
GroupRole,
|
||||
GroupPinnedEvent,
|
||||
generateEditGroupMetadataEventTemplate,
|
||||
generateGroupAdminsEventTemplate,
|
||||
generateGroupJoinRequestEventTemplate,
|
||||
generateGroupLeaveRequestEventTemplate,
|
||||
generateGroupLivekitParticipantsEventTemplate,
|
||||
generateGroupMembersEventTemplate,
|
||||
generateGroupMetadataEventTemplate,
|
||||
generateGroupPinnedEventsEventTemplate,
|
||||
generateGroupRolesEventTemplate,
|
||||
generateCreateGroupEventTemplate,
|
||||
generateCreateInviteEventTemplate,
|
||||
generateDeleteEventEventTemplate,
|
||||
generateDeleteGroupEventTemplate,
|
||||
generatePutUserEventTemplate,
|
||||
generateRemoveUserEventTemplate,
|
||||
generateUpdatePinListEventTemplate,
|
||||
parseGroupAdminsEvent,
|
||||
parseGroupLivekitParticipantsEvent,
|
||||
parseGroupMembersEvent,
|
||||
parseGroupMetadataEvent,
|
||||
parseGroupPinnedEventsEvent,
|
||||
parseGroupRolesEvent,
|
||||
validateGroupAdminsEvent,
|
||||
validateGroupLivekitParticipantsEvent,
|
||||
validateGroupMembersEvent,
|
||||
validateGroupMetadataEvent,
|
||||
validateGroupPinnedEventsEvent,
|
||||
validateGroupRolesEvent,
|
||||
} from './nip29.ts'
|
||||
|
||||
const group: Group = {
|
||||
relay: 'wss://relay.example.com',
|
||||
reference: {
|
||||
id: 'sample-group-id',
|
||||
host: 'wss://relay.example.com',
|
||||
},
|
||||
metadata: {
|
||||
id: 'sample-group-id',
|
||||
pubkey: 'sample-pubkey',
|
||||
name: 'Pizza Lovers',
|
||||
banner: 'https://pizza.com/banner.png',
|
||||
picture: 'https://pizza.com/pizza.png',
|
||||
about: 'a group for people who love pizza',
|
||||
isPrivate: true,
|
||||
isRestricted: true,
|
||||
isHidden: false,
|
||||
isClosed: false,
|
||||
hasLiveKit: true,
|
||||
supportedKinds: ['9', '11'],
|
||||
},
|
||||
members: [],
|
||||
admins: [],
|
||||
}
|
||||
|
||||
const makeEvent = (template: ReturnType<typeof generateGroupMetadataEventTemplate>): Event => ({
|
||||
id: 'sample-id',
|
||||
pubkey: 'sample-pubkey',
|
||||
created_at: template.created_at,
|
||||
kind: template.kind,
|
||||
tags: template.tags,
|
||||
content: template.content,
|
||||
sig: 'sample-sig',
|
||||
})
|
||||
|
||||
describe('NIP-29 group metadata event', () => {
|
||||
test('generateGroupMetadataEventTemplate emits all metadata fields', () => {
|
||||
const template = generateGroupMetadataEventTemplate(group)
|
||||
expect(template.kind).toBe(39000)
|
||||
expect(template.tags).toEqual([
|
||||
['d', 'sample-group-id'],
|
||||
['name', 'Pizza Lovers'],
|
||||
['picture', 'https://pizza.com/pizza.png'],
|
||||
['banner', 'https://pizza.com/banner.png'],
|
||||
['about', 'a group for people who love pizza'],
|
||||
['private'],
|
||||
['restricted'],
|
||||
['livekit'],
|
||||
['supported_kinds', '9', '11'],
|
||||
])
|
||||
})
|
||||
|
||||
test('parseGroupMetadataEvent round-trips the metadata fields', () => {
|
||||
const metadata = parseGroupMetadataEvent(makeEvent(generateGroupMetadataEventTemplate(group)))
|
||||
expect(metadata).toEqual({
|
||||
id: 'sample-group-id',
|
||||
pubkey: 'sample-pubkey',
|
||||
name: 'Pizza Lovers',
|
||||
banner: 'https://pizza.com/banner.png',
|
||||
picture: 'https://pizza.com/pizza.png',
|
||||
about: 'a group for people who love pizza',
|
||||
isPrivate: true,
|
||||
isRestricted: true,
|
||||
hasLiveKit: true,
|
||||
supportedKinds: ['9', '11'],
|
||||
})
|
||||
})
|
||||
|
||||
test('parseGroupMetadataEvent parses subgroups parent and child tags', () => {
|
||||
const event = makeEvent(generateGroupMetadataEventTemplate(group))
|
||||
event.tags.push(['parent', 'tech'])
|
||||
event.tags.push(['child', 'nostr'])
|
||||
const metadata = parseGroupMetadataEvent(event)
|
||||
expect(metadata.parent).toBe('tech')
|
||||
expect(metadata.children).toEqual(['nostr'])
|
||||
})
|
||||
|
||||
test('validateGroupMetadataEvent rejects events without a d tag', () => {
|
||||
const event = makeEvent(generateGroupMetadataEventTemplate(group))
|
||||
event.tags = []
|
||||
expect(validateGroupMetadataEvent(event)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-29 group roles event', () => {
|
||||
const roles: GroupRole[] = [{ name: 'ceo', description: 'runs the place' }, { name: 'secretary' }]
|
||||
|
||||
test('generateGroupRolesEventTemplate emits role tags', () => {
|
||||
const template = generateGroupRolesEventTemplate(group, roles)
|
||||
expect(template.kind).toBe(39003)
|
||||
expect(template.tags).toEqual([
|
||||
['d', 'sample-group-id'],
|
||||
['role', 'ceo', 'runs the place'],
|
||||
['role', 'secretary'],
|
||||
])
|
||||
})
|
||||
|
||||
test('parseGroupRolesEvent round-trips roles', () => {
|
||||
const event = makeEvent(generateGroupRolesEventTemplate(group, roles))
|
||||
expect(validateGroupRolesEvent(event)).toBe(true)
|
||||
expect(parseGroupRolesEvent(event)).toEqual(roles)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-29 group livekit participants event', () => {
|
||||
test('generateGroupLivekitParticipantsEventTemplate emits participant tags', () => {
|
||||
const template = generateGroupLivekitParticipantsEventTemplate(group, ['abc', 'def'])
|
||||
expect(template.kind).toBe(39004)
|
||||
expect(template.tags).toEqual([
|
||||
['d', 'sample-group-id'],
|
||||
['participant', 'abc'],
|
||||
['participant', 'def'],
|
||||
])
|
||||
})
|
||||
|
||||
test('parseGroupLivekitParticipantsEvent round-trips participants', () => {
|
||||
const event = makeEvent(generateGroupLivekitParticipantsEventTemplate(group, ['abc', 'def']))
|
||||
expect(validateGroupLivekitParticipantsEvent(event)).toBe(true)
|
||||
expect(parseGroupLivekitParticipantsEvent(event)).toEqual(['abc', 'def'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-29 group pinned events event', () => {
|
||||
const pinnedEvents: GroupPinnedEvent[] = [
|
||||
{ type: 'e', value: 'event-id-1' },
|
||||
{ type: 'a', value: '1:pubkey:d-identifier' },
|
||||
{ type: 'e', value: 'event-id-2' },
|
||||
]
|
||||
|
||||
test('generateGroupPinnedEventsEventTemplate emits e and a tags in order', () => {
|
||||
const template = generateGroupPinnedEventsEventTemplate(group, pinnedEvents)
|
||||
expect(template.kind).toBe(39005)
|
||||
expect(template.tags).toEqual([
|
||||
['d', 'sample-group-id'],
|
||||
['e', 'event-id-1'],
|
||||
['a', '1:pubkey:d-identifier'],
|
||||
['e', 'event-id-2'],
|
||||
])
|
||||
})
|
||||
|
||||
test('parseGroupPinnedEventsEvent round-trips pinned events in order', () => {
|
||||
const event = makeEvent(generateGroupPinnedEventsEventTemplate(group, pinnedEvents))
|
||||
expect(validateGroupPinnedEventsEvent(event)).toBe(true)
|
||||
expect(parseGroupPinnedEventsEvent(event)).toEqual(pinnedEvents)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-29 moderation events', () => {
|
||||
const previous = ['eb96c864', '2db75638']
|
||||
|
||||
test('generatePutUserEventTemplate', () => {
|
||||
const template = generatePutUserEventTemplate('sample-group-id', 'pubkey', ['ceo'], 'why', previous)
|
||||
expect(template.kind).toBe(9000)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['p', 'pubkey', 'ceo'],
|
||||
['previous', 'eb96c864', '2db75638'],
|
||||
])
|
||||
})
|
||||
|
||||
test('generateRemoveUserEventTemplate', () => {
|
||||
const template = generateRemoveUserEventTemplate('sample-group-id', 'pubkey', 'bye')
|
||||
expect(template.kind).toBe(9001)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['p', 'pubkey'],
|
||||
])
|
||||
})
|
||||
|
||||
test('generateEditGroupMetadataEventTemplate', () => {
|
||||
const template = generateEditGroupMetadataEventTemplate(group)
|
||||
expect(template.kind).toBe(9002)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['name', 'Pizza Lovers'],
|
||||
['picture', 'https://pizza.com/pizza.png'],
|
||||
['banner', 'https://pizza.com/banner.png'],
|
||||
['about', 'a group for people who love pizza'],
|
||||
['private'],
|
||||
['restricted'],
|
||||
['livekit'],
|
||||
['supported_kinds', '9', '11'],
|
||||
])
|
||||
})
|
||||
|
||||
test('generateDeleteEventEventTemplate', () => {
|
||||
const template = generateDeleteEventEventTemplate('sample-group-id', 'event-id', 'spam')
|
||||
expect(template.kind).toBe(9005)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['e', 'event-id'],
|
||||
])
|
||||
})
|
||||
|
||||
test('generateCreateGroupEventTemplate', () => {
|
||||
const template = generateCreateGroupEventTemplate('sample-group-id')
|
||||
expect(template.kind).toBe(9007)
|
||||
expect(template.tags).toEqual([['h', 'sample-group-id']])
|
||||
})
|
||||
|
||||
test('generateDeleteGroupEventTemplate', () => {
|
||||
const template = generateDeleteGroupEventTemplate('sample-group-id')
|
||||
expect(template.kind).toBe(9008)
|
||||
expect(template.tags).toEqual([['h', 'sample-group-id']])
|
||||
})
|
||||
|
||||
test('generateCreateInviteEventTemplate', () => {
|
||||
const template = generateCreateInviteEventTemplate('sample-group-id', 'invite-code')
|
||||
expect(template.kind).toBe(9009)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['code', 'invite-code'],
|
||||
])
|
||||
})
|
||||
|
||||
test('generateUpdatePinListEventTemplate', () => {
|
||||
const template = generateUpdatePinListEventTemplate('sample-group-id', [
|
||||
{ type: 'e', value: 'event-id-1' },
|
||||
{ type: 'a', value: '1:pubkey:d-identifier' },
|
||||
])
|
||||
expect(template.kind).toBe(9010)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['e', 'event-id-1'],
|
||||
['a', '1:pubkey:d-identifier'],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-29 join and leave requests', () => {
|
||||
test('generateGroupJoinRequestEventTemplate includes optional code tag', () => {
|
||||
const template = generateGroupJoinRequestEventTemplate('sample-group-id', 'invite-code', 'want in')
|
||||
expect(template.kind).toBe(9021)
|
||||
expect(template.tags).toEqual([
|
||||
['h', 'sample-group-id'],
|
||||
['code', 'invite-code'],
|
||||
])
|
||||
expect(template.content).toBe('want in')
|
||||
})
|
||||
|
||||
test('generateGroupLeaveRequestEventTemplate', () => {
|
||||
const template = generateGroupLeaveRequestEventTemplate('sample-group-id', 'leaving')
|
||||
expect(template.kind).toBe(9022)
|
||||
expect(template.tags).toEqual([['h', 'sample-group-id']])
|
||||
expect(template.content).toBe('leaving')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NIP-29 admins and members events', () => {
|
||||
const admins: GroupAdmin[] = [
|
||||
{ pubkey: 'admin-pubkey', label: 'boss', permissions: [GroupAdminPermission.PutUser] },
|
||||
{ pubkey: 'admin-pubkey-2', label: '', permissions: [GroupAdminPermission.EditMetadata] },
|
||||
]
|
||||
const members: GroupMember[] = [{ pubkey: 'member-pubkey', label: 'pizza lover' }]
|
||||
|
||||
test('generateGroupAdminsEventTemplate and parse round-trip', () => {
|
||||
const template = generateGroupAdminsEventTemplate(group, admins)
|
||||
const event = makeEvent(template)
|
||||
expect(validateGroupAdminsEvent(event)).toBe(true)
|
||||
expect(parseGroupAdminsEvent(event)).toEqual(admins)
|
||||
})
|
||||
|
||||
test('generateGroupMembersEventTemplate and parse round-trip', () => {
|
||||
const template = generateGroupMembersEventTemplate(group, members)
|
||||
const event = makeEvent(template)
|
||||
expect(validateGroupMembersEvent(event)).toBe(true)
|
||||
expect(parseGroupMembersEvent(event)).toEqual(members)
|
||||
})
|
||||
})
|
||||
@@ -24,9 +24,16 @@ export type GroupMetadata = {
|
||||
pubkey: string
|
||||
name?: string
|
||||
picture?: string
|
||||
banner?: string
|
||||
about?: string
|
||||
isPublic?: boolean
|
||||
isOpen?: boolean
|
||||
isPrivate?: boolean
|
||||
isRestricted?: boolean
|
||||
isHidden?: boolean
|
||||
isClosed?: boolean
|
||||
hasLiveKit?: boolean
|
||||
supportedKinds?: string[]
|
||||
parent?: string
|
||||
children?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +80,30 @@ export enum GroupAdminPermission {
|
||||
CreateGroup = 'create-group',
|
||||
DeleteGroup = 'delete-group',
|
||||
CreateInvite = 'create-invite',
|
||||
UpdatePinList = 'update-pin-list',
|
||||
}
|
||||
|
||||
function buildGroupMetadataTags(metadata: GroupMetadata): string[][] {
|
||||
const tags: string[][] = []
|
||||
metadata.name && tags.push(['name', metadata.name])
|
||||
metadata.picture && tags.push(['picture', metadata.picture])
|
||||
metadata.banner && tags.push(['banner', metadata.banner])
|
||||
metadata.about && tags.push(['about', metadata.about])
|
||||
metadata.isPrivate && tags.push(['private'])
|
||||
metadata.isRestricted && tags.push(['restricted'])
|
||||
metadata.isHidden && tags.push(['hidden'])
|
||||
metadata.isClosed && tags.push(['closed'])
|
||||
metadata.hasLiveKit && tags.push(['livekit'])
|
||||
metadata.supportedKinds &&
|
||||
metadata.supportedKinds.length > 0 &&
|
||||
tags.push(['supported_kinds', ...metadata.supportedKinds])
|
||||
metadata.parent && tags.push(['parent', metadata.parent])
|
||||
metadata.children &&
|
||||
metadata.children.forEach(child => {
|
||||
tags.push(['child', child])
|
||||
})
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,18 +113,11 @@ export enum GroupAdminPermission {
|
||||
* @returns An event template with the generated group metadata that can be signed later.
|
||||
*/
|
||||
export function generateGroupMetadataEventTemplate(group: Group): EventTemplate {
|
||||
const tags: string[][] = [['d', group.metadata.id]]
|
||||
group.metadata.name && tags.push(['name', group.metadata.name])
|
||||
group.metadata.picture && tags.push(['picture', group.metadata.picture])
|
||||
group.metadata.about && tags.push(['about', group.metadata.about])
|
||||
group.metadata.isPublic && tags.push(['public'])
|
||||
group.metadata.isOpen && tags.push(['open'])
|
||||
|
||||
return {
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 39000,
|
||||
tags,
|
||||
tags: [['d', group.metadata.id], ...buildGroupMetadataTags(group.metadata)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,18 +319,39 @@ export function parseGroupMetadataEvent(event: Event): GroupMetadata {
|
||||
case 'picture':
|
||||
metadata.picture = value
|
||||
break
|
||||
case 'banner':
|
||||
metadata.banner = value
|
||||
break
|
||||
case 'about':
|
||||
metadata.about = value
|
||||
break
|
||||
case 'public':
|
||||
metadata.isPublic = true
|
||||
case 'private':
|
||||
metadata.isPrivate = true
|
||||
break
|
||||
case 'open':
|
||||
metadata.isOpen = true
|
||||
case 'restricted':
|
||||
metadata.isRestricted = true
|
||||
break
|
||||
case 'hidden':
|
||||
metadata.isHidden = true
|
||||
break
|
||||
case 'closed':
|
||||
metadata.isClosed = true
|
||||
break
|
||||
case 'livekit':
|
||||
metadata.hasLiveKit = true
|
||||
break
|
||||
case 'parent':
|
||||
metadata.parent = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const supportedKinds = event.tags.filter(([tag]) => tag === 'supported_kinds').flatMap(([, ...values]) => values)
|
||||
if (supportedKinds.length > 0) metadata.supportedKinds = supportedKinds
|
||||
|
||||
const children = event.tags.filter(([tag]) => tag === 'child').map(([, value]) => value)
|
||||
if (children.length > 0) metadata.children = children
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
@@ -421,6 +466,131 @@ export async function fetchGroupMembersEvent({
|
||||
return groupMembersEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the group roles event from the specified pool.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {AbstractSimplePool} options.pool - The pool object.
|
||||
* @param {GroupReference} options.groupReference - The group reference object.
|
||||
* @param {string} [options.normalizedRelayURL] - The normalized relay URL.
|
||||
* @param {RelayInformation} [options.relayInformation] - The relay information object.
|
||||
* @returns {Promise<Event>} The group roles event that can be parsed later to get the group roles object.
|
||||
* @throws {Error} If the group roles event is not found.
|
||||
*/
|
||||
export async function fetchGroupRolesEvent({
|
||||
pool,
|
||||
groupReference,
|
||||
relayInformation,
|
||||
normalizedRelayURL,
|
||||
}: {
|
||||
pool: AbstractSimplePool
|
||||
groupReference: GroupReference
|
||||
normalizedRelayURL?: string
|
||||
relayInformation?: RelayInformation
|
||||
}): Promise<Event> {
|
||||
if (!normalizedRelayURL) {
|
||||
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
|
||||
}
|
||||
|
||||
if (!relayInformation) {
|
||||
relayInformation = await fetchRelayInformation(normalizedRelayURL)
|
||||
}
|
||||
|
||||
const groupRolesEvent = await pool.get([normalizedRelayURL], {
|
||||
kinds: [39003],
|
||||
authors: [relayInformation.pubkey],
|
||||
'#d': [groupReference.id],
|
||||
})
|
||||
|
||||
if (!groupRolesEvent) throw new Error(`roles for group '${groupReference.id}' not found on ${normalizedRelayURL}`)
|
||||
|
||||
return groupRolesEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the group livekit participants event from the specified pool.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {AbstractSimplePool} options.pool - The pool object.
|
||||
* @param {GroupReference} options.groupReference - The group reference object.
|
||||
* @param {string} [options.normalizedRelayURL] - The normalized relay URL.
|
||||
* @param {RelayInformation} [options.relayInformation] - The relay information object.
|
||||
* @returns {Promise<Event>} The group livekit participants event that can be parsed later to get the participants.
|
||||
* @throws {Error} If the group livekit participants event is not found.
|
||||
*/
|
||||
export async function fetchGroupLivekitParticipantsEvent({
|
||||
pool,
|
||||
groupReference,
|
||||
relayInformation,
|
||||
normalizedRelayURL,
|
||||
}: {
|
||||
pool: AbstractSimplePool
|
||||
groupReference: GroupReference
|
||||
normalizedRelayURL?: string
|
||||
relayInformation?: RelayInformation
|
||||
}): Promise<Event> {
|
||||
if (!normalizedRelayURL) {
|
||||
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
|
||||
}
|
||||
|
||||
if (!relayInformation) {
|
||||
relayInformation = await fetchRelayInformation(normalizedRelayURL)
|
||||
}
|
||||
|
||||
const groupLivekitParticipantsEvent = await pool.get([normalizedRelayURL], {
|
||||
kinds: [39004],
|
||||
authors: [relayInformation.pubkey],
|
||||
'#d': [groupReference.id],
|
||||
})
|
||||
|
||||
if (!groupLivekitParticipantsEvent)
|
||||
throw new Error(`livekit participants for group '${groupReference.id}' not found on ${normalizedRelayURL}`)
|
||||
|
||||
return groupLivekitParticipantsEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the group pinned events event from the specified pool.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {AbstractSimplePool} options.pool - The pool object.
|
||||
* @param {GroupReference} options.groupReference - The group reference object.
|
||||
* @param {string} [options.normalizedRelayURL] - The normalized relay URL.
|
||||
* @param {RelayInformation} [options.relayInformation] - The relay information object.
|
||||
* @returns {Promise<Event>} The group pinned events event that can be parsed later to get the pinned events.
|
||||
* @throws {Error} If the group pinned events event is not found.
|
||||
*/
|
||||
export async function fetchGroupPinnedEventsEvent({
|
||||
pool,
|
||||
groupReference,
|
||||
relayInformation,
|
||||
normalizedRelayURL,
|
||||
}: {
|
||||
pool: AbstractSimplePool
|
||||
groupReference: GroupReference
|
||||
normalizedRelayURL?: string
|
||||
relayInformation?: RelayInformation
|
||||
}): Promise<Event> {
|
||||
if (!normalizedRelayURL) {
|
||||
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference)
|
||||
}
|
||||
|
||||
if (!relayInformation) {
|
||||
relayInformation = await fetchRelayInformation(normalizedRelayURL)
|
||||
}
|
||||
|
||||
const groupPinnedEventsEvent = await pool.get([normalizedRelayURL], {
|
||||
kinds: [39005],
|
||||
authors: [relayInformation.pubkey],
|
||||
'#d': [groupReference.id],
|
||||
})
|
||||
|
||||
if (!groupPinnedEventsEvent)
|
||||
throw new Error(`pinned events for group '${groupReference.id}' not found on ${normalizedRelayURL}`)
|
||||
|
||||
return groupPinnedEventsEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a group members event and returns an array of GroupMember objects.
|
||||
* @param event - The event to parse.
|
||||
@@ -444,6 +614,406 @@ export function parseGroupMembersEvent(event: Event): GroupMember[] {
|
||||
return members
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a NIP29 group role.
|
||||
*/
|
||||
export type GroupRole = {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an event template for the roles supported by a group.
|
||||
*
|
||||
* @param group - The group object.
|
||||
* @param roles - An array of group roles.
|
||||
* @returns The generated event template with the group roles that can be signed later.
|
||||
*/
|
||||
export function generateGroupRolesEventTemplate(group: Group, roles: GroupRole[]): EventTemplate {
|
||||
const tags: string[][] = [['d', group.metadata.id]]
|
||||
for (const role of roles) {
|
||||
const tag = ['role', role.name]
|
||||
role.description && tag.push(role.description)
|
||||
tags.push(tag)
|
||||
}
|
||||
|
||||
return {
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 39003,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a group roles event.
|
||||
*
|
||||
* @param event - The event to validate.
|
||||
* @returns True if the event is a valid group roles event, false otherwise.
|
||||
*/
|
||||
export function validateGroupRolesEvent(event: Event): boolean {
|
||||
if (event.kind !== 39003) return false
|
||||
|
||||
const requiredTags = ['d'] as const
|
||||
for (const tag of requiredTags) {
|
||||
if (!event.tags.find(([t]) => t == tag)) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a group roles event and returns an array of GroupRole objects.
|
||||
*
|
||||
* @param event - The event to parse.
|
||||
* @returns An array of GroupRole objects.
|
||||
* @throws Throws an error if the group roles event is invalid.
|
||||
*/
|
||||
export function parseGroupRolesEvent(event: Event): GroupRole[] {
|
||||
if (!validateGroupRolesEvent(event)) throw new Error('invalid group roles event')
|
||||
|
||||
const roles: GroupRole[] = []
|
||||
|
||||
for (const [tag, name, description] of event.tags) {
|
||||
if (tag !== 'role') continue
|
||||
|
||||
roles.push({ name, description })
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an event template for the livekit participants of a group.
|
||||
*
|
||||
* @param group - The group object.
|
||||
* @param participants - An array of pubkeys currently live in the group's AV rooms.
|
||||
* @returns The generated event template with the livekit participants that can be signed later.
|
||||
*/
|
||||
export function generateGroupLivekitParticipantsEventTemplate(group: Group, participants: string[]): EventTemplate {
|
||||
const tags: string[][] = [['d', group.metadata.id]]
|
||||
participants.forEach(pubkey => {
|
||||
tags.push(['participant', pubkey])
|
||||
})
|
||||
|
||||
return {
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 39004,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a group livekit participants event.
|
||||
*
|
||||
* @param event - The event to validate.
|
||||
* @returns True if the event is a valid group livekit participants event, false otherwise.
|
||||
*/
|
||||
export function validateGroupLivekitParticipantsEvent(event: Event): boolean {
|
||||
if (event.kind !== 39004) return false
|
||||
|
||||
const requiredTags = ['d'] as const
|
||||
for (const tag of requiredTags) {
|
||||
if (!event.tags.find(([t]) => t == tag)) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a group livekit participants event and returns an array of participant pubkeys.
|
||||
*
|
||||
* @param event - The event to parse.
|
||||
* @returns An array of participant pubkeys.
|
||||
* @throws Throws an error if the group livekit participants event is invalid.
|
||||
*/
|
||||
export function parseGroupLivekitParticipantsEvent(event: Event): string[] {
|
||||
if (!validateGroupLivekitParticipantsEvent(event)) throw new Error('invalid group livekit participants event')
|
||||
|
||||
return event.tags.filter(([tag]) => tag === 'participant').map(([, pubkey]) => pubkey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a reference to a pinned event in a NIP29 group.
|
||||
*/
|
||||
export type GroupPinnedEvent = {
|
||||
type: 'e' | 'a'
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an event template for the events pinned in a group.
|
||||
*
|
||||
* @param group - The group object.
|
||||
* @param pinnedEvents - An array of references to pinned events, in display order.
|
||||
* @returns The generated event template with the pinned events that can be signed later.
|
||||
*/
|
||||
export function generateGroupPinnedEventsEventTemplate(group: Group, pinnedEvents: GroupPinnedEvent[]): EventTemplate {
|
||||
const tags: string[][] = [['d', group.metadata.id]]
|
||||
pinnedEvents.forEach(pinnedEvent => {
|
||||
tags.push([pinnedEvent.type, pinnedEvent.value])
|
||||
})
|
||||
|
||||
return {
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 39005,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a group pinned events event.
|
||||
*
|
||||
* @param event - The event to validate.
|
||||
* @returns True if the event is a valid group pinned events event, false otherwise.
|
||||
*/
|
||||
export function validateGroupPinnedEventsEvent(event: Event): boolean {
|
||||
if (event.kind !== 39005) return false
|
||||
|
||||
const requiredTags = ['d'] as const
|
||||
for (const tag of requiredTags) {
|
||||
if (!event.tags.find(([t]) => t == tag)) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a group pinned events event and returns an array of GroupPinnedEvent objects.
|
||||
*
|
||||
* @param event - The event to parse.
|
||||
* @returns An array of GroupPinnedEvent objects.
|
||||
* @throws Throws an error if the group pinned events event is invalid.
|
||||
*/
|
||||
export function parseGroupPinnedEventsEvent(event: Event): GroupPinnedEvent[] {
|
||||
if (!validateGroupPinnedEventsEvent(event)) throw new Error('invalid group pinned events event')
|
||||
|
||||
const pinnedEvents: GroupPinnedEvent[] = []
|
||||
|
||||
for (const [tag, value] of event.tags) {
|
||||
if (tag !== 'e' && tag !== 'a') continue
|
||||
|
||||
pinnedEvents.push({ type: tag, value })
|
||||
}
|
||||
|
||||
return pinnedEvents
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a group moderation event template. These events require the `h` tag and
|
||||
* may optionally carry timeline references in `previous` tags.
|
||||
*/
|
||||
function generateGroupModerationEventTemplate(
|
||||
kind: number,
|
||||
groupId: string,
|
||||
content: string,
|
||||
tags: string[][],
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
const allTags: string[][] = [['h', groupId], ...tags]
|
||||
previous && previous.length > 0 && allTags.push(['previous', ...previous])
|
||||
|
||||
return {
|
||||
content,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind,
|
||||
tags: allTags,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a `put-user` (kind:9000) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param pubkey - The pubkey of the user to add or update.
|
||||
* @param roles - Optional roles to assign to the user.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generatePutUserEventTemplate(
|
||||
groupId: string,
|
||||
pubkey: string,
|
||||
roles?: string[],
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(9000, groupId, reason || '', [['p', pubkey, ...(roles || [])]], previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a `remove-user` (kind:9001) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param pubkey - The pubkey of the user to remove.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateRemoveUserEventTemplate(
|
||||
groupId: string,
|
||||
pubkey: string,
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(9001, groupId, reason || '', [['p', pubkey]], previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an `edit-metadata` (kind:9002) moderation event template carrying
|
||||
* all the metadata fields of the group.
|
||||
*
|
||||
* @param group - The group object with the updated metadata.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateEditGroupMetadataEventTemplate(
|
||||
group: Group,
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(
|
||||
9002,
|
||||
group.metadata.id,
|
||||
reason || '',
|
||||
buildGroupMetadataTags(group.metadata),
|
||||
previous,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a `delete-event` (kind:9005) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param eventId - The id of the event to delete.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateDeleteEventEventTemplate(
|
||||
groupId: string,
|
||||
eventId: string,
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(9005, groupId, reason || '', [['e', eventId]], previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a `create-group` (kind:9007) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group to create.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateCreateGroupEventTemplate(groupId: string, reason?: string, previous?: string[]): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(9007, groupId, reason || '', [], previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a `delete-group` (kind:9008) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group to delete.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateDeleteGroupEventTemplate(groupId: string, reason?: string, previous?: string[]): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(9008, groupId, reason || '', [], previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a `create-invite` (kind:9009) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param code - An arbitrary invite code.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateCreateInviteEventTemplate(
|
||||
groupId: string,
|
||||
code: string,
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
return generateGroupModerationEventTemplate(9009, groupId, reason || '', [['code', code]], previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an `update-pin-list` (kind:9010) moderation event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param pinnedEvents - The full ordered list of pinned events.
|
||||
* @param reason - Optional reason for the action.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateUpdatePinListEventTemplate(
|
||||
groupId: string,
|
||||
pinnedEvents: GroupPinnedEvent[],
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
const tags = pinnedEvents.map(pinnedEvent => [pinnedEvent.type, pinnedEvent.value])
|
||||
return generateGroupModerationEventTemplate(9010, groupId, reason || '', tags, previous)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a group join request (kind:9021) event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param inviteCode - Optional invite code to be preauthorized by the relay.
|
||||
* @param reason - Optional reason for the request.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateGroupJoinRequestEventTemplate(
|
||||
groupId: string,
|
||||
inviteCode?: string,
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
const tags: string[][] = [['h', groupId]]
|
||||
inviteCode && tags.push(['code', inviteCode])
|
||||
previous && previous.length > 0 && tags.push(['previous', ...previous])
|
||||
|
||||
return {
|
||||
content: reason || '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 9021,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a group leave request (kind:9022) event template.
|
||||
*
|
||||
* @param groupId - The id of the group.
|
||||
* @param reason - Optional reason for the request.
|
||||
* @param previous - Optional timeline references.
|
||||
* @returns The generated event template that can be signed later.
|
||||
*/
|
||||
export function generateGroupLeaveRequestEventTemplate(
|
||||
groupId: string,
|
||||
reason?: string,
|
||||
previous?: string[],
|
||||
): EventTemplate {
|
||||
const tags: string[][] = [['h', groupId]]
|
||||
previous && previous.length > 0 && tags.push(['previous', ...previous])
|
||||
|
||||
return {
|
||||
content: reason || '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 9022,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and parses the group metadata event, group admins event, and group members event from the specified pool.
|
||||
* If the normalized relay URL is not provided, it will be obtained using the group reference.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "nostr-tools",
|
||||
"version": "2.24.1",
|
||||
"version": "2.24.2",
|
||||
"description": "Tools for making a Nostr client.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user