From 5e389ab29b41fc80ae8423a51ca3f31bbe4058aa Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:50:00 -0500 Subject: [PATCH] Document and test share note events --- docs/nostr-event-schemas.md | 16 +++++ js/nostrEventSchemas.js | 6 +- tests/share-event-builder.test.mjs | 93 ++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 tests/share-event-builder.test.mjs diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md index 5a91a84a..7a6cd54a 100644 --- a/docs/nostr-event-schemas.md +++ b/docs/nostr-event-schemas.md @@ -174,6 +174,22 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca | Zap request (`NOTE_TYPES.ZAP_REQUEST`) | `9734` | `['p', ]` plus optional `['e', ]`, `['a', ]`, `['amount', ]`, `['lnurl', ]`, and `['relays', ...]` for receipt publishing | Optional plaintext zap note | | Zap receipt (`NOTE_TYPES.ZAP_RECEIPT`) | `9735` | `['bolt11', ]`, `['description', ]`, `['p', ]` plus optional `['e', ]` and `['a', ]` | Empty content; receipts are published by the recipient's LNURL server | +### Share notes (kind `1`) + +Share notes are plain text `kind:1` events generated by the Share modal. The +`content` field is preserved verbatim (after UTF-8 sanitization) and can contain +any short message or commentary accompanying the share. + +The builder attaches the following tags: + +* `['e', , '', 'mention']` — the shared video’s event id, normalized to + lowercase hex. +* `['p', , '', 'mention']` — the shared video’s author pubkey, + normalized to lowercase hex. +* `['r', , ]` — optional relay hints. Relay URLs are + trimmed, duplicates are removed, and the optional marker is normalized to + `read` or `write` when provided. + Subscription lists therefore match the [NIP-51 follow-set specification](./nips/51.md#sets) by emitting kind `30000` events with the shared `['d','subscriptions']` identifier. Builders continue to diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index 7ec1ee5c..3b582e86 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -1010,14 +1010,12 @@ export function buildShareEvent(params) { const readMarker = schema?.readMarker || "read"; const writeMarker = schema?.writeMarker || "write"; - const normalizedVideoId = - typeof video?.id === "string" ? video.id.trim() : ""; + const normalizedVideoId = normalizePointerIdentifier(video?.id); if (normalizedVideoId) { tags.push(["e", normalizedVideoId, "", "mention"]); } - const normalizedVideoPubkey = - typeof video?.pubkey === "string" ? video.pubkey.trim() : ""; + const normalizedVideoPubkey = normalizePointerIdentifier(video?.pubkey); if (normalizedVideoPubkey) { tags.push(["p", normalizedVideoPubkey, "", "mention"]); } diff --git a/tests/share-event-builder.test.mjs b/tests/share-event-builder.test.mjs new file mode 100644 index 00000000..c95d6cd4 --- /dev/null +++ b/tests/share-event-builder.test.mjs @@ -0,0 +1,93 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const SAMPLE_HEX = "deadbeef".repeat(8); +const SAMPLE_NPUB = "npub1shareeventfixture000000000000000000000"; + +if (typeof globalThis.window === "undefined") { + globalThis.window = {}; +} + +if (!globalThis.window.NostrTools) { + globalThis.window.NostrTools = {}; +} + +const existingNip19 = + typeof globalThis.window.NostrTools.nip19 === "object" + ? globalThis.window.NostrTools.nip19 + : {}; + +if (typeof existingNip19.decode !== "function") { + globalThis.window.NostrTools.nip19 = { + ...existingNip19, + decode: () => ({ type: "npub", data: SAMPLE_HEX }), + }; +} + +const { buildShareEvent } = await import("../js/nostrEventSchemas.js"); + +test("buildShareEvent preserves share content", () => { + const event = buildShareEvent({ + pubkey: "share-author", + created_at: 1700000800, + content: "Check this out!", + video: { id: "a".repeat(64), pubkey: "b".repeat(64) }, + }); + + assert.equal(event.content, "Check this out!"); +}); + +test("buildShareEvent normalizes hex identifiers for e and p tags", () => { + const event = buildShareEvent({ + pubkey: "share-author", + created_at: 1700000801, + content: "Normalize IDs", + video: { + id: "ABCDEF".repeat(10) + "ABCD", + pubkey: SAMPLE_NPUB, + }, + }); + + const eventTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "e"); + const authorTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "p"); + + assert.deepEqual(eventTags, [["e", "abcdef".repeat(10) + "abcd", "", "mention"]]); + assert.deepEqual(authorTags, [["p", SAMPLE_HEX, "", "mention"]]); +}); + +test("buildShareEvent includes sanitized relay hints", () => { + const event = buildShareEvent({ + pubkey: "share-author", + created_at: 1700000802, + content: "Relay hints", + video: { id: "c".repeat(64), pubkey: "d".repeat(64) }, + relays: [ + " wss://relay.one ", + { url: " wss://relay.two ", mode: "READ" }, + ["r", "wss://relay.one", "write"], + { relay: "wss://relay.three", write: true, read: false }, + ["wss://relay.four", "write"], + ], + }); + + const relayTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "r"); + + assert.deepEqual(relayTags, [ + ["r", "wss://relay.one"], + ["r", "wss://relay.two", "read"], + ["r", "wss://relay.one", "write"], + ["r", "wss://relay.three", "write"], + ["r", "wss://relay.four", "write"], + ]); +}); + +test("buildShareEvent tolerates missing optional fields", () => { + const event = buildShareEvent({ + pubkey: "share-author", + created_at: 1700000803, + }); + + assert.equal(event.kind, 1); + assert.equal(event.content, ""); + assert.deepEqual(event.tags, []); +});