diff --git a/jsr.json b/jsr.json index 249c2ca..11bdc2f 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@nostr/tools", - "version": "2.10.1", + "version": "2.10.2", "exports": { ".": "./index.ts", "./core": "./core.ts", diff --git a/nip10.test.ts b/nip10.test.ts index 7d7ff6d..ca38a2c 100644 --- a/nip10.test.ts +++ b/nip10.test.ts @@ -5,20 +5,21 @@ 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'], + ['p', '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec'], + ['p', '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7'], + ['e', '89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d'], + ['e', '090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051'], + ['e', '567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976'], + ['e', '49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4'], + ['e', '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64'], + ['e', 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631'], + ['e', 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c'], ], } expect(parse(event)).toEqual({ + quotes: [], mentions: [ { id: 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631', @@ -55,41 +56,37 @@ describe('parse NIP10-referenced events', () => { relays: [], }, ], - reply: { + root: { id: '89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d', relays: [], }, - root: { + reply: { id: 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c', relays: [], }, }) }) - test('legacy + 3 events', () => { + test('modern', () => { let event = { tags: [ - ['e', 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c'], - ['e', 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631'], - ['e', '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64'], - ['p', '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7'], - ['p', '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec'], ['p', '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0'], + ['p', '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec'], + ['e', '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7'], + ['e', 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631', '', 'root'], + ['e', 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c', '', 'reply'], ], } expect(parse(event)).toEqual({ + quotes: [], mentions: [ { - id: 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631', + id: '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7', relays: [], }, ], profiles: [ - { - pubkey: '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7', - relays: [], - }, { pubkey: '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec', relays: [], @@ -99,30 +96,43 @@ describe('parse NIP10-referenced events', () => { relays: [], }, ], - reply: { - id: '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64', + root: { + id: 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631', relays: [], }, - root: { + reply: { id: 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c', relays: [], }, }) }) - test('legacy + 2 events', () => { + test('modern, inverted, author hint', () => { let event = { tags: [ - ['e', 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c'], - ['e', 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631'], - ['p', '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7'], + ['p', '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0', 'wss://goiaba.com'], ['p', '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec'], - ['p', '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0'], + ['p', '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7'], + ['e', '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64', '', 'reply'], + [ + 'e', + 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631', + 'wss://banana.com', + 'root', + '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0', + ], + ['e', 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c'], ], } expect(parse(event)).toEqual({ - mentions: [], + quotes: [], + mentions: [ + { + id: 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c', + relays: [], + }, + ], profiles: [ { pubkey: '77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7', @@ -134,60 +144,80 @@ describe('parse NIP10-referenced events', () => { }, { pubkey: '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0', - relays: [], + relays: ['wss://goiaba.com'], }, ], - reply: { + root: { id: 'bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631', - relays: [], + relays: ['wss://banana.com', 'wss://goiaba.com'], + author: '4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0', }, - root: { - id: 'b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c', + reply: { + id: '5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64', relays: [], }, }) }) - test('legacy + 1 event', () => { + test('1 event, relay hint from author', () => { let event = { tags: [ - ['e', '9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590'], - ['p', '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec'], + ['p', '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec', 'wss://banana.com'], + [ + 'e', + '9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590', + '', + 'root', + '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec', + ], ], } expect(parse(event)).toEqual({ + quotes: [], mentions: [], profiles: [ { pubkey: '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec', - relays: [], + relays: ['wss://banana.com'], }, ], - reply: undefined, + reply: { + author: '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec', + id: '9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590', + relays: ['wss://banana.com'], + }, root: { + author: '534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec', id: '9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590', - relays: [], + relays: ['wss://banana.com'], }, }) }) - test('recommended + 1 event', () => { + test('many p 1 reply', () => { let event = { tags: [ - ['p', 'a8c21fcd8aa1f4befba14d72fc7a012397732d30d8b3131af912642f3c726f52', 'wss://relay.mostr.pub'], - ['p', '003d7fd21fd09ff7f6f63a75daf194dd99feefbe6919cc376b7359d5090aa9a6', 'wss://relay.mostr.pub'], - ['p', '2f6fbe452edd3987d3c67f3b034c03ec5bcf4d054c521c3a954686f89f03212e', 'wss://relay.mostr.pub'], - ['p', '44c7c74668ff222b0e0b30579c49fc6e22dafcdeaad091036c947f9856590f1e', 'wss://relay.mostr.pub'], - ['p', 'c5cf39149caebda4cdd61771c51f6ba91ef5645919004e5c4998a4ea69f00512', 'wss://relay.mostr.pub'], - ['p', '094d44bb1e812696c57f57ad1c0c707812dedbe72c07e538b80639032c236a9e', 'wss://relay.mostr.pub'], ['p', 'a1ba0ac9b6ec098f726a3c11ec654df4a32cbb84b5377e8788395e9c27d9ecda', 'wss://relay.mostr.pub'], - ['e', 'f9472913904ab7e9da008dcb2d85fd4af2d2993ada483d00c646d0c4481d031d', 'wss://relay.mostr.pub', 'reply'], + ['p', '094d44bb1e812696c57f57ad1c0c707812dedbe72c07e538b80639032c236a9e', 'wss://relay.mostr.pub'], + ['p', 'c5cf39149caebda4cdd61771c51f6ba91ef5645919004e5c4998a4ea69f00512', 'wss://relay.mostr.pub'], + ['p', '44c7c74668ff222b0e0b30579c49fc6e22dafcdeaad091036c947f9856590f1e', 'wss://relay.mostr.pub'], + ['p', '2f6fbe452edd3987d3c67f3b034c03ec5bcf4d054c521c3a954686f89f03212e', 'wss://relay.mostr.pub'], + ['p', '003d7fd21fd09ff7f6f63a75daf194dd99feefbe6919cc376b7359d5090aa9a6', 'wss://relay.mostr.pub'], + ['p', 'a8c21fcd8aa1f4befba14d72fc7a012397732d30d8b3131af912642f3c726f52', 'wss://relay.mostr.pub'], + [ + 'e', + 'f9472913904ab7e9da008dcb2d85fd4af2d2993ada483d00c646d0c4481d031d', + 'wss://relay.mostr.pub', + 'reply', + 'c5cf39149caebda4cdd61771c51f6ba91ef5645919004e5c4998a4ea69f00512', + ], ['mostr', 'https://poa.st/objects/dc50684b-6364-4264-ab16-49f4622f05ea'], ], } expect(parse(event)).toEqual({ + quotes: [], mentions: [], profiles: [ { @@ -222,8 +252,13 @@ describe('parse NIP10-referenced events', () => { reply: { id: 'f9472913904ab7e9da008dcb2d85fd4af2d2993ada483d00c646d0c4481d031d', relays: ['wss://relay.mostr.pub'], + author: 'c5cf39149caebda4cdd61771c51f6ba91ef5645919004e5c4998a4ea69f00512', + }, + root: { + id: 'f9472913904ab7e9da008dcb2d85fd4af2d2993ada483d00c646d0c4481d031d', + relays: ['wss://relay.mostr.pub'], + author: 'c5cf39149caebda4cdd61771c51f6ba91ef5645919004e5c4998a4ea69f00512', }, - root: undefined, }) }) }) diff --git a/nip10.ts b/nip10.ts index 4f0c1e8..9869866 100644 --- a/nip10.ts +++ b/nip10.ts @@ -1,7 +1,7 @@ import type { Event } from './core.ts' import type { EventPointer, ProfilePointer } from './nip19.ts' -export type NIP10Result = { +export function parse(event: Pick): { /** * Pointer to the root of the thread. */ @@ -13,29 +13,80 @@ export type NIP10Result = { reply: EventPointer | undefined /** - * Pointers to events which may or may not be in the reply chain. + * Pointers to events that may or may not be in the reply chain. */ mentions: EventPointer[] + /** + * Pointers to events that were directly quoted. + */ + quotes: EventPointer[] + /** * List of pubkeys that are involved in the thread in no particular order. */ profiles: ProfilePointer[] -} - -export function parse(event: Pick): NIP10Result { +} { const result: NIP10Result = { reply: undefined, root: undefined, mentions: [], profiles: [], + quotes: [], } - const eTags: string[][] = [] + let maybeParent: EventPointer | undefined + let maybeRoot: EventPointer | undefined + + for (let i = event.tags.length - 1; i >= 0; i--) { + const tag = event.tags[i] - for (const tag of event.tags) { if (tag[0] === 'e' && tag[1]) { - eTags.push(tag) + const [_, eTagEventId, eTagRelayUrl, eTagMarker, eTagAuthor] = tag as [ + string, + string, + undefined | string, + undefined | string, + undefined | string, + ] + + const eventPointer: EventPointer = { + id: eTagEventId, + relays: eTagRelayUrl ? [eTagRelayUrl] : [], + author: eTagAuthor, + } + + if (eTagMarker === 'root') { + result.root = eventPointer + continue + } + + if (eTagMarker === 'reply') { + result.reply = eventPointer + continue + } + + if (eTagMarker === 'mention') { + result.mentions.push(eventPointer) + continue + } + + if (!maybeParent) { + maybeParent = eventPointer + } else { + maybeRoot = eventPointer + } + + result.mentions.push(eventPointer) + continue + } + + if (tag[0] === 'q' && tag[1]) { + const [_, eTagEventId, eTagRelayUrl] = tag as [string, string, undefined | string] + result.quotes.push({ + id: eTagEventId, + relays: eTagRelayUrl ? [eTagRelayUrl] : [], + }) } if (tag[0] === 'p' && tag[1]) { @@ -43,49 +94,49 @@ export function parse(event: Pick): NIP10Result { pubkey: tag[1], relays: tag[2] ? [tag[2]] : [], }) - } - } - - 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') { - result.root = eventPointer continue } + } - if (eTagMarker === 'reply') { - result.reply = eventPointer - continue - } + // get legacy (positional) markers, set reply to root and vice-versa if one of them is missing + if (!result.root) { + result.root = maybeRoot || maybeParent || result.reply + } + if (!result.reply) { + result.reply = maybeParent || result.root + } - if (eTagMarker === 'mention') { - result.mentions.push(eventPointer) - continue + // remove root and reply from mentions, inherit relay hints from authors if any + ;[result.reply, result.root].forEach(ref => { + let idx = result.mentions.indexOf(ref!) + if (idx !== -1) { + result.mentions.splice(idx, 1) } - - if (isFirstETag) { - result.root = eventPointer - continue + if (ref!.author) { + let author = result.profiles.find(p => p.pubkey === ref!.author) + if (author && author.relays) { + if (!ref!.relays) { + ref!.relays = [] + } + author.relays.forEach(url => { + if (ref?.relays!?.indexOf(url) === -1) ref!.relays!.push(url) + }) + } } - - if (isLastETag) { - result.reply = eventPointer - continue + }) + result.mentions.forEach(ref => { + if (ref!.author) { + let author = result.profiles.find(p => p.pubkey === ref!.author) + if (author && author.relays) { + if (!ref!.relays) { + ref!.relays = [] + } + author.relays.forEach(url => { + if (ref?.relays!?.indexOf(url) === -1) ref!.relays!.push(url) + }) + } } - - result.mentions.push(eventPointer) - } + }) return result } diff --git a/package.json b/package.json index 8e2e347..33a2605 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "nostr-tools", - "version": "2.10.1", + "version": "2.10.2", "description": "Tools for making a Nostr client.", "repository": { "type": "git",