mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2026-07-30 19:26:14 +00:00
nip22: like nip10, but for kind:1111 and adjacent kinds.
This commit is contained in:
@@ -13,6 +13,7 @@ export * as nip17 from './nip17.ts'
|
||||
export * as nip18 from './nip18.ts'
|
||||
export * as nip19 from './nip19.ts'
|
||||
export * as nip21 from './nip21.ts'
|
||||
export * as nip22 from './nip22.ts'
|
||||
export * as nip25 from './nip25.ts'
|
||||
export * as nip27 from './nip27.ts'
|
||||
export * as nip28 from './nip28.ts'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nostr/tools",
|
||||
"version": "2.23.3",
|
||||
"version": "2.23.4",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./core": "./core.ts",
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { parse } from './nip22.ts'
|
||||
|
||||
describe('parse NIP22 comment references', () => {
|
||||
test('top-level comment on addressable event', () => {
|
||||
expect(
|
||||
parse({
|
||||
tags: [
|
||||
['A', '30023:3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289:f9347ca7', 'wss://example.relay'],
|
||||
['K', '30023'],
|
||||
['P', '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289', 'wss://example.relay'],
|
||||
['a', '30023:3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289:f9347ca7', 'wss://example.relay'],
|
||||
['e', '5b4fc7fed15672fefe65d2426f67197b71ccc82aa0cc8a9e94f683eb78e07651', 'wss://example.relay', '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289'],
|
||||
['k', '30023'],
|
||||
['p', '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289', 'wss://example.relay'],
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
root: {
|
||||
kind: 30023,
|
||||
pubkey: '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289',
|
||||
identifier: 'f9347ca7',
|
||||
relays: ['wss://example.relay'],
|
||||
},
|
||||
rootKind: 30023,
|
||||
reply: {
|
||||
kind: 30023,
|
||||
pubkey: '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289',
|
||||
identifier: 'f9347ca7',
|
||||
relays: ['wss://example.relay'],
|
||||
},
|
||||
replyKind: 30023,
|
||||
mentions: [],
|
||||
quotes: [],
|
||||
profiles: [
|
||||
{
|
||||
pubkey: '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289',
|
||||
relays: ['wss://example.relay'],
|
||||
},
|
||||
{
|
||||
pubkey: '3c9849383bdea883b0bd16fece1ed36d37e37cdde3ce43b17ea4e9192ec11289',
|
||||
relays: ['wss://example.relay'],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('reply to comment with quote', () => {
|
||||
expect(
|
||||
parse({
|
||||
tags: [
|
||||
['E', '768ac8720cdeb59227cf95e98b66560ef03d8bc9a90d721779e76e68fb42f5e6', 'wss://example.relay', 'fd913cd6fa9edb8405750cd02a8bbe16e158b8676c0e69fdc27436cc4a54cc9a'],
|
||||
['K', '1063'],
|
||||
['P', 'fd913cd6fa9edb8405750cd02a8bbe16e158b8676c0e69fdc27436cc4a54cc9a'],
|
||||
['e', '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36', 'wss://example.relay', '93ef2ebaaf9554661f33e79949007900bbc535d239a4c801c33a4d67d3e7f546'],
|
||||
['k', '1111'],
|
||||
['p', '93ef2ebaaf9554661f33e79949007900bbc535d239a4c801c33a4d67d3e7f546', 'wss://parent.relay'],
|
||||
['q', '680f14330f3f8b24d9b86137ad9a6ef0c7a955c31cbea02e9f53a25a1fce9d6f', 'wss://quote.relay', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
root: {
|
||||
id: '768ac8720cdeb59227cf95e98b66560ef03d8bc9a90d721779e76e68fb42f5e6',
|
||||
relays: ['wss://example.relay'],
|
||||
author: 'fd913cd6fa9edb8405750cd02a8bbe16e158b8676c0e69fdc27436cc4a54cc9a',
|
||||
},
|
||||
rootKind: 1063,
|
||||
reply: {
|
||||
id: '5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36',
|
||||
relays: ['wss://example.relay', 'wss://parent.relay'],
|
||||
author: '93ef2ebaaf9554661f33e79949007900bbc535d239a4c801c33a4d67d3e7f546',
|
||||
},
|
||||
replyKind: 1111,
|
||||
mentions: [],
|
||||
quotes: [
|
||||
{
|
||||
id: '680f14330f3f8b24d9b86137ad9a6ef0c7a955c31cbea02e9f53a25a1fce9d6f',
|
||||
relays: ['wss://quote.relay'],
|
||||
author: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
},
|
||||
],
|
||||
profiles: [
|
||||
{
|
||||
pubkey: 'fd913cd6fa9edb8405750cd02a8bbe16e158b8676c0e69fdc27436cc4a54cc9a',
|
||||
relays: ['wss://example.relay'],
|
||||
},
|
||||
{
|
||||
pubkey: '93ef2ebaaf9554661f33e79949007900bbc535d239a4c801c33a4d67d3e7f546',
|
||||
relays: ['wss://example.relay', 'wss://parent.relay'],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('comment on external identifier', () => {
|
||||
expect(
|
||||
parse({
|
||||
tags: [
|
||||
['I', 'https://abc.com/articles/1'],
|
||||
['K', 'web'],
|
||||
['i', 'https://abc.com/articles/1'],
|
||||
['k', 'web'],
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
root: {
|
||||
value: 'https://abc.com/articles/1',
|
||||
hint: undefined,
|
||||
},
|
||||
rootKind: 'web',
|
||||
reply: {
|
||||
value: 'https://abc.com/articles/1',
|
||||
hint: undefined,
|
||||
},
|
||||
replyKind: 'web',
|
||||
mentions: [],
|
||||
quotes: [],
|
||||
profiles: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,191 @@
|
||||
import type { Event } from './core.ts'
|
||||
import type { AddressPointer, EventPointer, ProfilePointer } from './nip19.ts'
|
||||
|
||||
export type ExternalPointer = {
|
||||
value: string
|
||||
hint?: string
|
||||
}
|
||||
|
||||
function parseKind(kind: string | undefined): number | string | undefined {
|
||||
if (!kind) return undefined
|
||||
return /^\d+$/.test(kind) ? parseInt(kind, 10) : kind
|
||||
}
|
||||
|
||||
function parseAddressPointer(value: string, relayUrl?: string): AddressPointer | undefined {
|
||||
const idx = value.indexOf(':')
|
||||
const idx2 = value.indexOf(':', idx + 1)
|
||||
if (idx === -1 || idx2 === -1) return undefined
|
||||
|
||||
const kind = parseInt(value.slice(0, idx), 10)
|
||||
if (Number.isNaN(kind)) return undefined
|
||||
|
||||
return {
|
||||
kind,
|
||||
pubkey: value.slice(idx + 1, idx2),
|
||||
identifier: value.slice(idx2 + 1),
|
||||
relays: relayUrl ? [relayUrl] : [],
|
||||
}
|
||||
}
|
||||
|
||||
function parsePointer(tag: string[]): EventPointer | AddressPointer | ExternalPointer | undefined {
|
||||
switch (tag[0]) {
|
||||
case 'E':
|
||||
case 'e':
|
||||
if (!tag[1]) return undefined
|
||||
return {
|
||||
id: tag[1],
|
||||
relays: tag[2] ? [tag[2]] : [],
|
||||
author: tag[3],
|
||||
}
|
||||
case 'A':
|
||||
case 'a':
|
||||
if (!tag[1]) return undefined
|
||||
return parseAddressPointer(tag[1], tag[2])
|
||||
case 'I':
|
||||
case 'i':
|
||||
if (!tag[1]) return undefined
|
||||
return {
|
||||
value: tag[1],
|
||||
hint: tag[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseQuote(tag: string[]): EventPointer | AddressPointer | ExternalPointer | undefined {
|
||||
if (!tag[1]) return undefined
|
||||
|
||||
if (tag[1].includes(':')) {
|
||||
return parseAddressPointer(tag[1], tag[2])
|
||||
}
|
||||
|
||||
return {
|
||||
id: tag[1],
|
||||
relays: tag[2] ? [tag[2]] : [],
|
||||
author: tag[3],
|
||||
}
|
||||
}
|
||||
|
||||
function choosePointer(
|
||||
candidates: Array<{ tagName: string; pointer: EventPointer | AddressPointer | ExternalPointer }>,
|
||||
): EventPointer | AddressPointer | ExternalPointer | undefined {
|
||||
return (
|
||||
candidates.findLast(candidate => candidate.tagName === 'A' || candidate.tagName === 'a')?.pointer ||
|
||||
candidates.findLast(candidate => candidate.tagName === 'I' || candidate.tagName === 'i')?.pointer ||
|
||||
candidates.findLast(candidate => candidate.tagName === 'E' || candidate.tagName === 'e')?.pointer
|
||||
)
|
||||
}
|
||||
|
||||
function inheritRelayHints(
|
||||
pointer: EventPointer | AddressPointer | ExternalPointer | undefined,
|
||||
profiles: ProfilePointer[],
|
||||
) {
|
||||
if (!pointer || !('id' in pointer) || !pointer.author) return
|
||||
|
||||
const author = profiles.find(profile => profile.pubkey === pointer.author)
|
||||
if (!author || !author.relays) return
|
||||
|
||||
if (!pointer.relays) {
|
||||
pointer.relays = []
|
||||
}
|
||||
|
||||
author.relays.forEach(url => {
|
||||
if (pointer.relays!.indexOf(url) === -1) pointer.relays!.push(url)
|
||||
})
|
||||
author.relays = pointer.relays
|
||||
}
|
||||
|
||||
export function parse(event: Pick<Event, 'tags'>): {
|
||||
/**
|
||||
* Pointer to root scope.
|
||||
*/
|
||||
root: EventPointer | AddressPointer | ExternalPointer | undefined
|
||||
|
||||
/**
|
||||
* Kind of root scope from `K` tag.
|
||||
*/
|
||||
rootKind: number | string | undefined
|
||||
|
||||
/**
|
||||
* Pointer to parent item being replied to.
|
||||
*/
|
||||
reply: EventPointer | AddressPointer | ExternalPointer | undefined
|
||||
|
||||
/**
|
||||
* Kind of parent item from `k` tag.
|
||||
*/
|
||||
replyKind: number | string | undefined
|
||||
|
||||
/**
|
||||
* Reserved for extra referenced items.
|
||||
*/
|
||||
mentions: (EventPointer | AddressPointer | ExternalPointer)[]
|
||||
|
||||
/**
|
||||
* Pointers directly quoted with `q` tags.
|
||||
*/
|
||||
quotes: (EventPointer | AddressPointer | ExternalPointer)[]
|
||||
|
||||
/**
|
||||
* Root and parent authors.
|
||||
*/
|
||||
profiles: ProfilePointer[]
|
||||
} {
|
||||
const result: ReturnType<typeof parse> = {
|
||||
root: undefined,
|
||||
rootKind: undefined,
|
||||
reply: undefined,
|
||||
replyKind: undefined,
|
||||
mentions: [],
|
||||
quotes: [],
|
||||
profiles: [],
|
||||
}
|
||||
|
||||
const rootCandidates: Array<{ tagName: string; pointer: EventPointer | AddressPointer | ExternalPointer }> = []
|
||||
const replyCandidates: Array<{ tagName: string; pointer: EventPointer | AddressPointer | ExternalPointer }> = []
|
||||
|
||||
for (const tag of event.tags) {
|
||||
if ((tag[0] === 'E' || tag[0] === 'A' || tag[0] === 'I') && tag[1]) {
|
||||
const pointer = parsePointer(tag)
|
||||
if (pointer) rootCandidates.push({ tagName: tag[0], pointer })
|
||||
continue
|
||||
}
|
||||
|
||||
if ((tag[0] === 'e' || tag[0] === 'a' || tag[0] === 'i') && tag[1]) {
|
||||
const pointer = parsePointer(tag)
|
||||
if (pointer) replyCandidates.push({ tagName: tag[0], pointer })
|
||||
continue
|
||||
}
|
||||
|
||||
if (tag[0] === 'K') {
|
||||
result.rootKind = parseKind(tag[1])
|
||||
continue
|
||||
}
|
||||
|
||||
if (tag[0] === 'k') {
|
||||
result.replyKind = parseKind(tag[1])
|
||||
continue
|
||||
}
|
||||
|
||||
if (tag[0] === 'q') {
|
||||
const pointer = parseQuote(tag)
|
||||
if (pointer) result.quotes.push(pointer)
|
||||
continue
|
||||
}
|
||||
|
||||
if ((tag[0] === 'P' || tag[0] === 'p') && tag[1]) {
|
||||
result.profiles.push({
|
||||
pubkey: tag[1],
|
||||
relays: tag[2] ? [tag[2]] : [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result.root = choosePointer(rootCandidates)
|
||||
result.reply = choosePointer(replyCandidates)
|
||||
|
||||
inheritRelayHints(result.root, result.profiles)
|
||||
inheritRelayHints(result.reply, result.profiles)
|
||||
result.quotes.forEach(pointer => inheritRelayHints(pointer, result.profiles))
|
||||
|
||||
return result
|
||||
}
|
||||
+8
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "nostr-tools",
|
||||
"version": "2.23.3",
|
||||
"version": "2.23.4",
|
||||
"description": "Tools for making a Nostr client.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nbd-wtf/nostr-tools.git"
|
||||
"url": "git+https://github.com/nbd-wtf/nostr-tools.git"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
@@ -144,6 +144,12 @@
|
||||
"require": "./lib/cjs/nip21.js",
|
||||
"types": "./lib/types/nip21.d.ts"
|
||||
},
|
||||
"./nip22": {
|
||||
"source": "./nip22.ts",
|
||||
"import": "./lib/esm/nip22.js",
|
||||
"require": "./lib/cjs/nip22.js",
|
||||
"types": "./lib/types/nip22.d.ts"
|
||||
},
|
||||
"./nip25": {
|
||||
"source": "./nip25.ts",
|
||||
"import": "./lib/esm/nip25.js",
|
||||
|
||||
Reference in New Issue
Block a user