From 3099211f8adba7c167c94a7ee9a47ce27e09a638 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:17:36 -0400 Subject: [PATCH] Propagate reaction target author metadata --- js/nostr/reactionEvents.js | 33 ++++++++-- js/nostrEventSchemas.js | 89 +++++++++++++++++++++++++++ js/reactionCounter.js | 81 +++++++++++++++++++++++- tests/reaction-event-builder.test.mjs | 39 ++++++++++++ 4 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 tests/reaction-event-builder.test.mjs diff --git a/js/nostr/reactionEvents.js b/js/nostr/reactionEvents.js index f59d1aaa..475dc92a 100644 --- a/js/nostr/reactionEvents.js +++ b/js/nostr/reactionEvents.js @@ -50,15 +50,38 @@ export async function publishVideoReaction( ) : []; + const optionRelay = + typeof options.pointerRelay === "string" && options.pointerRelay.trim() + ? options.pointerRelay.trim() + : ""; + + const pointerRelay = + typeof pointer.relay === "string" && pointer.relay.trim() + ? pointer.relay.trim() + : optionRelay; + const pointerTag = pointer.type === "a" - ? pointer.relay - ? ["a", pointer.value, pointer.relay] + ? pointerRelay + ? ["a", pointer.value, pointerRelay] : ["a", pointer.value] - : pointer.relay - ? ["e", pointer.value, pointer.relay] + : pointerRelay + ? ["e", pointer.value, pointerRelay] : ["e", pointer.value]; + const normalizedPointer = { + type: pointer.type, + value: pointer.value, + relay: pointerRelay || null, + }; + + const explicitTargetAuthor = + typeof options.targetAuthorPubkey === "string" && options.targetAuthorPubkey.trim() + ? options.targetAuthorPubkey.trim() + : typeof options.authorPubkey === "string" && options.authorPubkey.trim() + ? options.authorPubkey.trim() + : ""; + let content = "+"; if (typeof options.content === "string") { content = options.content; @@ -82,6 +105,8 @@ export async function publishVideoReaction( created_at: createdAt, pointerValue: pointer.value, pointerTag, + targetPointer: normalizedPointer, + targetAuthorPubkey: explicitTargetAuthor, additionalTags, content, }); diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index f54d2959..34ff19f7 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -689,6 +689,8 @@ export function buildReactionEvent({ pointerValue, pointerTag, pointerTags = [], + targetPointer = null, + targetAuthorPubkey = "", additionalTags = [], content = "", }) { @@ -704,6 +706,93 @@ export function buildReactionEvent({ tags.push(tag); }); + const resolvePointerDetails = (pointerCandidate) => { + if (!pointerCandidate) { + return null; + } + + if (Array.isArray(pointerCandidate) && pointerCandidate.length >= 2) { + const [type, value, relay] = pointerCandidate; + const normalizedType = type === "a" ? "a" : type === "e" ? "e" : ""; + const normalizedValue = typeof value === "string" ? value.trim() : ""; + if (!normalizedType || !normalizedValue) { + return null; + } + const normalizedRelay = + typeof relay === "string" && relay.trim() ? relay.trim() : ""; + return { type: normalizedType, value: normalizedValue, relay: normalizedRelay }; + } + + if (pointerCandidate && typeof pointerCandidate === "object") { + const { type, value, relay } = pointerCandidate; + const normalizedType = type === "a" ? "a" : type === "e" ? "e" : ""; + const normalizedValue = typeof value === "string" ? value.trim() : ""; + if (!normalizedType || !normalizedValue) { + return null; + } + const normalizedRelay = + typeof relay === "string" && relay.trim() ? relay.trim() : ""; + return { type: normalizedType, value: normalizedValue, relay: normalizedRelay }; + } + + return null; + }; + + const pointerDetails = (() => { + const explicitPointer = resolvePointerDetails(targetPointer); + if (explicitPointer) { + return explicitPointer; + } + + const pointerTagEntry = normalizedPointerTags.find( + (tag) => Array.isArray(tag) && tag.length >= 2 && (tag[0] === "a" || tag[0] === "e") + ); + if (!pointerTagEntry) { + return null; + } + + return resolvePointerDetails(pointerTagEntry); + })(); + + const resolvedRelay = pointerDetails?.relay || ""; + + const normalizedAuthorPubkey = (() => { + if (typeof targetAuthorPubkey === "string" && targetAuthorPubkey.trim()) { + return targetAuthorPubkey.trim(); + } + + if (pointerDetails?.type === "a" && pointerDetails.value) { + const segments = pointerDetails.value.split(":"); + if (segments.length >= 2) { + const candidate = segments[1]?.trim(); + if (candidate) { + return candidate; + } + } + } + + return ""; + })(); + + if (normalizedAuthorPubkey) { + const existingAuthorTag = tags.find( + (tag) => Array.isArray(tag) && tag[0] === "p" && tag[1] === normalizedAuthorPubkey + ); + if (!existingAuthorTag) { + if (resolvedRelay) { + tags.push(["p", normalizedAuthorPubkey, resolvedRelay]); + } else { + tags.push(["p", normalizedAuthorPubkey]); + } + } else if (resolvedRelay) { + if (existingAuthorTag.length < 3) { + existingAuthorTag.push(resolvedRelay); + } else if (existingAuthorTag[2] !== resolvedRelay) { + existingAuthorTag[2] = resolvedRelay; + } + } + } + if (Array.isArray(additionalTags)) { additionalTags.forEach((tag) => { if (Array.isArray(tag) && tag.length >= 2) { diff --git a/js/reactionCounter.js b/js/reactionCounter.js index 0335d55f..0e2b9a51 100644 --- a/js/reactionCounter.js +++ b/js/reactionCounter.js @@ -4,7 +4,86 @@ import { devLogger, userLogger } from "./utils/logger.js"; export const reactionCounter = { async publish(pointer, options = {}) { try { - const result = await publishVideoReaction(pointer, options); + const publishOptions = { ...options }; + + const pointerRelayFromPointer = (() => { + if (Array.isArray(pointer) && pointer.length >= 3) { + const relay = pointer[2]; + if (typeof relay === "string" && relay.trim()) { + return relay.trim(); + } + } + if (pointer && typeof pointer === "object") { + const relay = pointer.relay; + if (typeof relay === "string" && relay.trim()) { + return relay.trim(); + } + } + return ""; + })(); + + const pointerRelay = + typeof publishOptions.pointerRelay === "string" && publishOptions.pointerRelay.trim() + ? publishOptions.pointerRelay.trim() + : pointerRelayFromPointer; + + if (pointerRelay && !publishOptions.pointerRelay) { + publishOptions.pointerRelay = pointerRelay; + } + + const enrichedPointer = (() => { + if (!pointerRelay) { + return pointer; + } + + if (Array.isArray(pointer) && pointer.length >= 2) { + const [type, value] = pointer; + return [type, value, pointerRelay]; + } + + if (pointer && typeof pointer === "object") { + return { ...pointer, relay: pointerRelay }; + } + + return pointer; + })(); + + const explicitAuthor = + typeof publishOptions.targetAuthorPubkey === "string" && + publishOptions.targetAuthorPubkey.trim() + ? publishOptions.targetAuthorPubkey.trim() + : ""; + + if (!explicitAuthor) { + const fallbackAuthor = (() => { + if ( + typeof publishOptions.authorPubkey === "string" && + publishOptions.authorPubkey.trim() + ) { + return publishOptions.authorPubkey.trim(); + } + if ( + typeof publishOptions.currentVideoPubkey === "string" && + publishOptions.currentVideoPubkey.trim() + ) { + return publishOptions.currentVideoPubkey.trim(); + } + if ( + publishOptions.video && + typeof publishOptions.video.pubkey === "string" && + publishOptions.video.pubkey.trim() + ) { + return publishOptions.video.pubkey.trim(); + } + return ""; + })(); + + if (fallbackAuthor) { + publishOptions.targetAuthorPubkey = fallbackAuthor; + } + } + + const result = await publishVideoReaction(enrichedPointer, publishOptions); if (!result || !result.ok) { userLogger.warn( "[reactionCounter] Reaction publish rejected by relays:", diff --git a/tests/reaction-event-builder.test.mjs b/tests/reaction-event-builder.test.mjs new file mode 100644 index 00000000..1d3100da --- /dev/null +++ b/tests/reaction-event-builder.test.mjs @@ -0,0 +1,39 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildReactionEvent } from "../js/nostrEventSchemas.js"; + +test("buildReactionEvent includes pointer and author tags when pubkey provided", () => { + const event = buildReactionEvent({ + pubkey: "viewer", + created_at: 1700000000, + pointerValue: "event-id", + pointerTag: ["e", "event-id", "wss://relay.example"], + targetPointer: { type: "e", value: "event-id", relay: "wss://relay.example" }, + targetAuthorPubkey: "author-pubkey", + content: "+", + }); + + const pointerTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "e"); + assert.deepEqual(pointerTags, [["e", "event-id", "wss://relay.example"]]); + + const authorTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "p"); + assert.deepEqual(authorTags, [["p", "author-pubkey", "wss://relay.example"]]); +}); + +test("buildReactionEvent derives author pubkey from address pointer", () => { + const event = buildReactionEvent({ + pubkey: "viewer", + created_at: 1700000001, + pointerValue: "30078:deadbeefcafebabe:clip-1", + pointerTag: ["a", "30078:deadbeefcafebabe:clip-1"], + targetPointer: { type: "a", value: "30078:deadbeefcafebabe:clip-1" }, + content: "+", + }); + + const addressTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "a"); + assert.deepEqual(addressTags, [["a", "30078:deadbeefcafebabe:clip-1"]]); + + const authorTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "p"); + assert.deepEqual(authorTags, [["p", "deadbeefcafebabe"]]); +});