update nip29 helpers to latest spec.

This commit is contained in:
fiatjaf
2026-08-15 19:07:24 -03:00
parent 5352a0187b
commit 5ae314f43e
4 changed files with 893 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nostr/tools",
"version": "2.24.1",
"version": "2.24.2",
"exports": {
".": "./index.ts",
"./core": "./core.ts",
+307
View File
@@ -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)
})
})
+584 -14
View File
@@ -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
View File
@@ -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",