Files
bitvid/js/nostr/reactionEvents.js
T

361 lines
9.6 KiB
JavaScript

import { buildReactionEvent, sanitizeAdditionalTags } from "../nostrEventSchemas.js";
import { publishEventToRelay } from "../nostrPublish.js";
import { RELAY_URLS } from "./toolkit.js";
import { normalizePointerInput } from "./watchHistory.js";
import { devLogger, userLogger } from "../utils/logger.js";
function sanitizeRelayList(primary, fallback) {
if (Array.isArray(primary) && primary.length) {
return primary;
}
if (Array.isArray(fallback) && fallback.length) {
return fallback;
}
return RELAY_URLS;
}
function normalizeString(value) {
return typeof value === "string" ? value.trim() : "";
}
function extractEventIdFromPointerInput(pointerInput) {
if (!pointerInput) {
return "";
}
if (Array.isArray(pointerInput)) {
if (
pointerInput.length >= 2 &&
pointerInput[0] === "e" &&
typeof pointerInput[1] === "string"
) {
const candidate = pointerInput[1].trim();
if (candidate) {
return candidate;
}
}
return "";
}
if (typeof pointerInput !== "object") {
return "";
}
const candidateKeys = ["eventId", "pointerEventId", "id"];
for (const key of candidateKeys) {
const candidate = normalizeString(pointerInput[key]);
if (candidate) {
return candidate;
}
}
const normalizedType = normalizeString(pointerInput.type);
if (normalizedType === "e") {
const candidate = normalizeString(pointerInput.value);
if (candidate) {
return candidate;
}
}
if (pointerInput.pointer) {
const nested = extractEventIdFromPointerInput(pointerInput.pointer);
if (nested) {
return nested;
}
}
if (pointerInput.tag) {
const nested = extractEventIdFromPointerInput(pointerInput.tag);
if (nested) {
return nested;
}
}
return "";
}
function extractEventRelayFromPointerInput(pointerInput) {
if (!pointerInput) {
return "";
}
if (Array.isArray(pointerInput)) {
if (
pointerInput.length >= 3 &&
pointerInput[0] === "e" &&
typeof pointerInput[2] === "string"
) {
const candidate = pointerInput[2].trim();
if (candidate) {
return candidate;
}
}
return "";
}
if (typeof pointerInput !== "object") {
return "";
}
const candidateRelay = normalizeString(pointerInput.eventRelay);
if (candidateRelay) {
return candidateRelay;
}
const normalizedType = normalizeString(pointerInput.type);
if (normalizedType === "e") {
const relayCandidate = normalizeString(pointerInput.relay);
if (relayCandidate) {
return relayCandidate;
}
}
if (pointerInput.pointer) {
const nested = extractEventRelayFromPointerInput(pointerInput.pointer);
if (nested) {
return nested;
}
}
if (pointerInput.tag) {
const nested = extractEventRelayFromPointerInput(pointerInput.tag);
if (nested) {
return nested;
}
}
return "";
}
export async function publishVideoReaction(
client,
pointerInput,
options = {},
{
resolveActiveSigner,
shouldRequestExtensionPermissions,
signEventWithPrivateKey,
DEFAULT_NIP07_PERMISSION_METHODS,
}
) {
if (!client?.pool) {
return { ok: false, error: "nostr-uninitialized" };
}
const pointer = normalizePointerInput(pointerInput);
if (!pointer) {
return { ok: false, error: "invalid-pointer" };
}
const actorPubkey = await client.ensureSessionActor();
if (!actorPubkey) {
return { ok: false, error: "missing-actor" };
}
const createdAt =
typeof options.created_at === "number" && options.created_at > 0
? Math.floor(options.created_at)
: Math.floor(Date.now() / 1000);
const additionalTags = sanitizeAdditionalTags(options.additionalTags);
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;
let pointerEventId = normalizeString(pointer.type === "e" ? pointer.value : "");
if (!pointerEventId) {
pointerEventId = extractEventIdFromPointerInput(pointerInput);
}
if (!pointerEventId) {
pointerEventId = normalizeString(options.pointerEventId);
}
if (!pointerEventId && options.video) {
pointerEventId =
extractEventIdFromPointerInput(options.video.pointerInfo) ||
normalizeString(options.video.pointerEventId) ||
normalizeString(options.video.eventId) ||
normalizeString(options.video.id);
}
let pointerEventRelay = normalizeString(
pointer.type === "e" ? pointer.relay : ""
);
if (!pointerEventRelay) {
pointerEventRelay = extractEventRelayFromPointerInput(pointerInput);
}
if (!pointerEventRelay) {
pointerEventRelay = normalizeString(options.pointerEventRelay);
}
if (!pointerEventRelay && options.video) {
pointerEventRelay =
normalizeString(options.video.pointerInfo?.eventRelay) ||
extractEventRelayFromPointerInput(options.video.pointerInfo) ||
normalizeString(options.video.eventRelay) ||
normalizeString(options.video.relay);
}
if (!pointerEventRelay && pointerRelay) {
pointerEventRelay = pointerRelay;
}
if (!pointerEventId) {
const errorDetails = pointer.type === "a"
? "Unable to resolve event id for address reaction target."
: "Unable to resolve event id for reaction target.";
devLogger.warn("[nostr] " + errorDetails, pointerInput);
return { ok: false, error: "missing-pointer-event-id", details: errorDetails };
}
const pointerTags = [];
let pointerTag = null;
if (pointer.type === "a") {
const addressTag = pointerRelay
? ["a", pointer.value, pointerRelay]
: ["a", pointer.value];
pointerTags.push(addressTag);
pointerTag = addressTag;
}
const eventTag = pointerEventRelay
? ["e", pointerEventId, pointerEventRelay]
: ["e", pointerEventId];
pointerTags.push(eventTag);
if (!pointerTag) {
pointerTag = eventTag;
}
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;
} else if (
options.content &&
typeof options.content === "object" &&
!Array.isArray(options.content)
) {
try {
content = JSON.stringify(options.content);
} catch (error) {
devLogger.warn("[nostr] Failed to serialize custom reaction content:", error);
content = "+";
}
} else if (options.content !== undefined && options.content !== null) {
content = String(options.content);
}
const event = buildReactionEvent({
pubkey: actorPubkey,
created_at: createdAt,
pointerValue: pointer.value,
pointerTag,
pointerTags,
targetPointer: normalizedPointer,
targetAuthorPubkey: explicitTargetAuthor,
additionalTags,
content,
});
let signedEvent = null;
const normalizedActor =
typeof actorPubkey === "string" ? actorPubkey.toLowerCase() : "";
const normalizedLogged =
typeof client.pubkey === "string" ? client.pubkey.toLowerCase() : "";
const signer = resolveActiveSigner(actorPubkey);
const canUseActiveSigner =
normalizedActor &&
normalizedActor === normalizedLogged &&
signer &&
typeof signer.signEvent === "function";
if (canUseActiveSigner) {
let permissionResult = { ok: true };
if (shouldRequestExtensionPermissions(signer)) {
permissionResult = await client.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS
);
}
if (permissionResult.ok) {
try {
signedEvent = await signer.signEvent(event);
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign reaction event with active signer:",
error,
);
return { ok: false, error: "signing-failed", details: error };
}
} else {
userLogger.warn(
"[nostr] Active signer permissions missing; signing reaction with session key.",
permissionResult.error,
);
}
}
if (!signedEvent) {
try {
if (!client.sessionActor || client.sessionActor.pubkey !== actorPubkey) {
await client.ensureSessionActor(true);
}
if (!client.sessionActor || client.sessionActor.pubkey !== actorPubkey) {
throw new Error("session-actor-mismatch");
}
const privateKey = client.sessionActor.privateKey;
signedEvent = signEventWithPrivateKey(event, privateKey);
} catch (error) {
userLogger.warn("[nostr] Failed to sign reaction event with session key:", error);
return { ok: false, error: "signing-failed", details: error };
}
}
const relayList = sanitizeRelayList(options.relays, client.relays);
const publishResults = await Promise.all(
relayList.map((url) => publishEventToRelay(client.pool, url, signedEvent))
);
const acceptedRelays = publishResults
.filter((result) => result.success)
.map((result) => result.url)
.filter((url) => typeof url === "string" && url);
const success = acceptedRelays.length > 0;
if (success) {
devLogger.info(
`[nostr] Reaction event accepted by ${acceptedRelays.length} relay(s):`,
acceptedRelays.join(", ")
);
} else {
userLogger.warn("[nostr] Reaction event rejected by relays:", publishResults);
}
return {
ok: success,
event: signedEvent,
results: publishResults,
acceptedRelays,
};
}