diff --git a/js/nostr.js b/js/nostr.js index e0aca71c..197d4e97 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -1100,12 +1100,15 @@ class NostrClient { } async decryptWatchHistoryEvent(pointerEvent, actorPubkey) { + const fallbackItems = extractPointerItemsFromEvent(pointerEvent); + const fallbackPayload = { version: 0, items: fallbackItems }; + const normalizedActor = typeof actorPubkey === "string" && actorPubkey.trim() ? actorPubkey.trim().toLowerCase() : ""; if (!normalizedActor) { - return { version: 0, items: extractPointerItemsFromEvent(pointerEvent) }; + return fallbackPayload; } const ciphertext = @@ -1114,7 +1117,7 @@ class NostrClient { : ""; if (!ciphertext) { - return { version: 0, items: extractPointerItemsFromEvent(pointerEvent) }; + return fallbackPayload; } const normalizedLogged = @@ -1131,7 +1134,10 @@ class NostrClient { ciphertext ); const parsed = parseWatchHistoryPayload(plaintext); - return parsed; + if (parsed.items.length || fallbackItems.length === 0) { + return parsed; + } + return { ...parsed, items: fallbackItems }; } catch (error) { if (isDevMode) { console.warn( @@ -1164,7 +1170,10 @@ class NostrClient { ciphertext ); const parsed = parseWatchHistoryPayload(plaintext); - return parsed; + if (parsed.items.length || fallbackItems.length === 0) { + return parsed; + } + return { ...parsed, items: fallbackItems }; } catch (error) { if (isDevMode) { console.warn( @@ -1175,7 +1184,7 @@ class NostrClient { } } - return { version: 0, items: extractPointerItemsFromEvent(pointerEvent) }; + return fallbackPayload; } cancelWatchHistoryRepublish(actor) { @@ -1300,9 +1309,18 @@ class NostrClient { return encryptionResult; } + const pointerTags = trimmedItems.map((item) => { + const tag = [item.type, item.value]; + if (item.relay) { + tag.push(item.relay); + } + return tag; + }); + const tags = [ ["d", WATCH_HISTORY_LIST_IDENTIFIER], ["encrypted", "nip04"], + ...pointerTags, ]; const event = { diff --git a/tests/watch-history.test.mjs b/tests/watch-history.test.mjs new file mode 100644 index 00000000..2bff7e48 --- /dev/null +++ b/tests/watch-history.test.mjs @@ -0,0 +1,112 @@ +import "./test-helpers/setup-localstorage.mjs"; +import assert from "node:assert/strict"; + +if (typeof globalThis.window === "undefined") { + globalThis.window = {}; +} + +if (!globalThis.window.NostrTools) { + globalThis.window.NostrTools = {}; +} + +globalThis.window.NostrTools.getEventHash = (event) => { + const payload = `${event.kind}:${event.pubkey}:${event.created_at}`; + return `hash-${Buffer.from(payload).toString("hex")}`; +}; + +globalThis.window.NostrTools.signEvent = (event, privateKey) => + `sig-${privateKey.slice(0, 8)}-${event.created_at}`; + +delete globalThis.window.NostrTools.nip04; +delete globalThis.window.nostr; + +const { nostrClient } = await import("../js/nostr.js"); + +function createPublishingClient(actorPubkey) { + const client = new nostrClient.constructor(); + client.pool = { + publish(urls, event) { + return { + on(eventName, handler) { + if (eventName === "ok" || eventName === "seen") { + setTimeout(handler, 0); + } + }, + }; + }, + }; + client.relays = ["wss://unit.test"]; + client.pubkey = ""; + client.sessionActor = { pubkey: actorPubkey, privateKey: "unit-secret" }; + client.ensureSessionActor = async () => actorPubkey; + client.encryptWatchHistoryPayload = async () => ({ + ok: true, + ciphertext: "ciphertext", + }); + client.persistWatchHistoryEntry = () => {}; + client.cancelWatchHistoryRepublish = () => {}; + client.scheduleWatchHistoryRepublish = () => {}; + return client; +} + +function createDecryptClient(actorPubkey) { + const client = new nostrClient.constructor(); + client.sessionActor = { pubkey: actorPubkey, privateKey: "unit-secret" }; + client.ensureSessionActor = async () => actorPubkey; + return client; +} + +const ACTOR = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const POINTER_A = { + type: "a", + value: `30078:${ACTOR}:watch-history`, + relay: "wss://relay.unit", +}; +const POINTER_E = { type: "e", value: "event-id-123", relay: null }; + +const publishingClient = createPublishingClient(ACTOR); +const publishResult = await publishingClient.publishWatchHistorySnapshot( + ACTOR, + [POINTER_A, POINTER_E] +); + +assert.equal(publishResult.ok, true, "publish should report success"); + +const pointerTags = publishResult.event.tags.filter((tag) => + tag && (tag[0] === "a" || tag[0] === "e") +); +assert.deepEqual(pointerTags, [ + ["a", POINTER_A.value, POINTER_A.relay], + ["e", POINTER_E.value], +]); + +assert.deepEqual( + publishResult.items.map((item) => ({ + type: item.type, + value: item.value, + relay: item.relay || null, + })), + [ + { type: "a", value: POINTER_A.value, relay: POINTER_A.relay }, + { type: "e", value: POINTER_E.value, relay: null }, + ] +); + +delete globalThis.window.nostr; +delete globalThis.window.NostrTools.nip04; + +const decryptClient = createDecryptClient(ACTOR); +const decrypted = await decryptClient.decryptWatchHistoryEvent( + publishResult.event, + ACTOR +); + +assert.deepEqual(decrypted, { + version: 0, + items: [ + { type: "a", value: POINTER_A.value, relay: POINTER_A.relay }, + { type: "e", value: POINTER_E.value, relay: null }, + ], +}); + +console.log("watch history tests passed");