From 13e9b4aa3eefe1053df8d32953c68ce05f7677d2 Mon Sep 17 00:00:00 2001 From: futpib Date: Mon, 24 Apr 2023 00:50:13 +0400 Subject: [PATCH] Add NIP-25 utils --- index.ts | 1 + nip25.test.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++ nip25.ts | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 nip25.test.js create mode 100644 nip25.ts diff --git a/index.ts b/index.ts index c1f8047..2ca54ae 100644 --- a/index.ts +++ b/index.ts @@ -12,6 +12,7 @@ export * as nip10 from './nip10' export * as nip13 from './nip13' export * as nip19 from './nip19' export * as nip21 from './nip21' +export * as nip25 from './nip25' export * as nip26 from './nip26' export * as nip27 from './nip27' export * as nip39 from './nip39' diff --git a/nip25.test.js b/nip25.test.js new file mode 100644 index 0000000..ffb5992 --- /dev/null +++ b/nip25.test.js @@ -0,0 +1,104 @@ +/* eslint-env jest */ + +const {nip25, finishEvent, getPublicKey, Kind} = require('./lib/nostr.cjs') + +describe('finishReactionEvent + getReactedEventPointer', () => { + const privateKey = + 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' + + const publicKey = getPublicKey(privateKey) + + const reactedEvent = finishEvent({ + kind: Kind.Text, + tags: [ + ['e', 'replied event id'], + ['p', 'replied event pubkey'], + ], + content: 'Replied to a post', + created_at: 1617932115 + }, privateKey) + + it('should create a signed event from a minimal template', () => { + const template = { + created_at: 1617932115 + } + + const event = nip25.finishReactionEvent(template, reactedEvent, privateKey) + + expect(event.kind).toEqual(Kind.Reaction) + expect(event.tags).toEqual([ + [ + 'e', + 'replied event id', + ], + [ + 'p', + 'replied event pubkey', + ], + [ + 'e', + '0ecdbd4dba0652afb19e5f638257a41552a37995a4438ef63de658443f8d16b1', + ], + [ + 'p', + '6af0f9de588f2c53cedcba26c5e2402e0d0aa64ec7b47c9f8d97b5bc562bab5f', + ], + ]) + expect(event.content).toEqual('+') + expect(event.created_at).toEqual(template.created_at) + expect(event.pubkey).toEqual(publicKey) + expect(typeof event.id).toEqual('string') + expect(typeof event.sig).toEqual('string') + + const reactedEventPointer = nip25.getReactedEventPointer(event) + + expect(reactedEventPointer.id).toEqual(reactedEvent.id) + expect(reactedEventPointer.author).toEqual(reactedEvent.pubkey) + }) + + it('should create a signed event from a filled template', () => { + const template = { + tags: [ + ['nonstandard', 'tag'], + ], + content: '👍', + created_at: 1617932115 + } + + const event = nip25.finishReactionEvent(template, reactedEvent, privateKey) + + expect(event.kind).toEqual(Kind.Reaction) + expect(event.tags).toEqual([ + [ + 'nonstandard', + 'tag', + ], + [ + 'e', + 'replied event id', + ], + [ + 'p', + 'replied event pubkey', + ], + [ + 'e', + '0ecdbd4dba0652afb19e5f638257a41552a37995a4438ef63de658443f8d16b1', + ], + [ + 'p', + '6af0f9de588f2c53cedcba26c5e2402e0d0aa64ec7b47c9f8d97b5bc562bab5f', + ], + ]) + expect(event.content).toEqual('👍') + expect(event.created_at).toEqual(template.created_at) + expect(event.pubkey).toEqual(publicKey) + expect(typeof event.id).toEqual('string') + expect(typeof event.sig).toEqual('string') + + const reactedEventPointer = nip25.getReactedEventPointer(event) + + expect(reactedEventPointer.id).toEqual(reactedEvent.id) + expect(reactedEventPointer.author).toEqual(reactedEvent.pubkey) + }) +}) diff --git a/nip25.ts b/nip25.ts new file mode 100644 index 0000000..6d8fad8 --- /dev/null +++ b/nip25.ts @@ -0,0 +1,68 @@ +import { Event, finishEvent, Kind } from './event' +import { EventPointer } from './nip19' + +export type ReactionEventTemplate = { + /** + * Pass only non-nip25 tags if you have to. Nip25 tags ('e' and 'p' tags from reacted event) will be added automatically. + */ + tags?: string[][] + + /** + * @default '+' + */ + content?: string + + created_at: number +} + +export function finishReactionEvent( + t: ReactionEventTemplate, + reacted: Event, + privateKey: string, +): Event { + const inheritedTags = reacted.tags.filter( + (tag) => tag.length >= 2 && (tag[0] === 'e' || tag[0] === 'p'), + ) + + return finishEvent({ + ...t, + kind: Kind.Reaction, + tags: [ + ...(t.tags ?? []), + ...inheritedTags, + ['e', reacted.id], + ['p', reacted.pubkey], + ], + content: t.content ?? '+', + }, privateKey) +} + +export function getReactedEventPointer(event: Event): undefined | EventPointer { + if (event.kind !== Kind.Reaction) { + return undefined + } + + let lastETag: undefined | string[] + let lastPTag: undefined | string[] + + for (let i = event.tags.length - 1; i >= 0 && (lastETag === undefined || lastPTag === undefined); i--) { + const tag = event.tags[i] + if (tag.length >= 2) { + if (tag[0] === 'e' && lastETag === undefined) { + lastETag = tag + } else if (tag[0] === 'p' && lastPTag === undefined) { + lastPTag = tag + } + } + } + + if (lastETag === undefined || lastPTag === undefined) { + return undefined + } + + return { + id: lastETag[1], + relays: [ lastETag[2], lastPTag[2] ].filter((x) => x !== undefined), + author: lastPTag[1], + } +}