From 3e67f9b014ae7456dd9068f3bc203f5f16b5dec5 Mon Sep 17 00:00:00 2001 From: futpib Date: Sat, 1 Apr 2023 04:08:59 +0400 Subject: [PATCH] Add NIP-10 thread root/reply/mention parsing --- index.ts | 1 + nip10.test.js | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++ nip10.ts | 73 +++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 nip10.test.js create mode 100644 nip10.ts diff --git a/index.ts b/index.ts index 561a818..0295f02 100644 --- a/index.ts +++ b/index.ts @@ -8,6 +8,7 @@ export * from './references' export * as nip04 from './nip04' export * as nip05 from './nip05' export * as nip06 from './nip06' +export * as nip10 from './nip10' export * as nip19 from './nip19' export * as nip26 from './nip26' export * as nip39 from './nip39' diff --git a/nip10.test.js b/nip10.test.js new file mode 100644 index 0000000..c020003 --- /dev/null +++ b/nip10.test.js @@ -0,0 +1,221 @@ +/* eslint-env jest */ + +const {nip10} = require('./lib/nostr.cjs') + +describe('parse NIP10-referenced events', () => { + + test('legacy + a lot of events', () => { + let event = { + tags: [ + [ + 'e', + 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c' + ], + [ + 'e', + 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631' + ], + [ + 'e', + '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64' + ], + [ + 'e', + '49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4' + ], + [ + 'e', + '567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976' + ], + [ + 'e', + '090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051' + ], + [ + 'e', + '89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d' + ], + [ + 'p', + '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7' + ], + [ + 'p', + '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec' + ], + [ + 'p', + '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0' + ] + ], + }; + + expect(nip10.parse(event)).toEqual({ + "mentions": [ + { + "id": "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", + "relays": [], + }, + { + "id": "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", + "relays": [], + }, + { + "id": "49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4", + "relays": [], + }, + { + "id": "567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976", + "relays": [], + }, + { + "id": "090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051", + "relays": [], + }, + ], + "pubkeys": [ + "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", + "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", + "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", + ], + "reply": { + "id": "89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d", + "relays": [], + }, + "root": { + "id": "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", + "relays": [], + }, + }); + }); + + test('legacy + 3 events', () => { + let event = { + tags: [ + [ + 'e', + 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c' + ], + [ + 'e', + 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631' + ], + [ + 'e', + '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64' + ], + [ + 'p', + '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7' + ], + [ + 'p', + '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec' + ], + [ + 'p', + '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0' + ] + ], + }; + + expect(nip10.parse(event)).toEqual({ + "mentions": [ + { + "id": "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", + "relays": [], + }, + ], + "pubkeys": [ + "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", + "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", + "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", + ], + "reply": { + "id": "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", + "relays": [], + }, + "root": { + "id": "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", + "relays": [], + }, + }); + }); + + test('legacy + 2 events', () => { + let event = { + tags: [ + [ + 'e', + 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c' + ], + [ + 'e', + 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631' + ], + [ + 'p', + '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7' + ], + [ + 'p', + '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec' + ], + [ + 'p', + '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0' + ] + ], + }; + + expect(nip10.parse(event)).toEqual({ + "mentions": [], + "pubkeys": [ + "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", + "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", + "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", + ], + "reply": { + "id": "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", + "relays": [], + }, + "root": { + "id": "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", + "relays": [], + }, + }); + }); + + test('legacy + 1 event', () => { + let event = { + tags: [ + [ + 'e', + '9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590' + ], + [ + 'p', + '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec' + ] + ], + }; + + expect(nip10.parse(event)).toEqual({ + "mentions": [], + "pubkeys": [ + "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", + ], + "reply": undefined, + "root": { + "id": "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", + "relays": [], + }, + }); + }); + + // No events with NIP-10 explicit root/reply/mention markers were found in the wild for these tests :( + test.todo('recommended + a lot of events'); + test.todo('recommended + 3 events'); + test.todo('recommended + 2 events'); + test.todo('recommended + 1 event'); +}); diff --git a/nip10.ts b/nip10.ts new file mode 100644 index 0000000..4ac6f68 --- /dev/null +++ b/nip10.ts @@ -0,0 +1,73 @@ +import type { Event } from "./event"; +import { EventPointer } from "./nip19"; + +export type NIP10Result = { + /** + * Pointer to the root of the thread. + */ + root: EventPointer | undefined; + + /** + * Pointer to a "parent" event that parsed event replies to (responded to). + */ + reply: EventPointer | undefined; + + /** + * Pointers to events which may or may not be in the reply chain. + */ + mentions: EventPointer[]; + + /** + * List of pubkeys that are involved in the thread in no particular order. + */ + pubkeys: string[]; +}; + +export function parse(event: Pick): NIP10Result { + const result: NIP10Result = { + reply: undefined, + root: undefined, + mentions: [], + pubkeys: [], + }; + + const eTags: string[][] = []; + + for (const tag of event.tags) { + if (tag[0] === "e" && tag[1]) { + eTags.push(tag); + } + + if (tag[0] === "p" && tag[1]) { + result.pubkeys.push(tag[1]); + } + } + + for (let eTagIndex = 0; eTagIndex < eTags.length; eTagIndex++) { + const eTag = eTags[eTagIndex]; + + const [ _, eTagEventId, eTagRelayUrl, eTagMarker ] = eTag as [string, string, undefined | string, undefined | string]; + + const eventPointer: EventPointer = { + id: eTagEventId, + relays: eTagRelayUrl ? [eTagRelayUrl] : [], + }; + + const isFirstETag = eTagIndex === 0; + const isLastETag = eTagIndex === eTags.length - 1; + + if (eTagMarker === 'root' || isFirstETag) { + result.root = eventPointer; + continue; + } + + if (eTagMarker === 'reply' || isLastETag) { + result.reply = eventPointer; + continue; + } + + result.mentions.push(eventPointer); + } + + return result; +}