mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #821 from PR0M3TH3AN/codex/add-video-reaction-support
Add helpers for publishing video reactions
This commit is contained in:
6 files changed
+320
-20
No files matched your search
+17
-1
@@ -4,7 +4,23 @@
|
||||
// facades. Prefer importing from the facades directly in new modules so the
|
||||
// wiring between the default client and helper layers stays explicit.
|
||||
|
||||
export { nostrClient, requestDefaultExtensionPermissions } from "./nostrClientFacade.js";
|
||||
import {
|
||||
nostrClient as defaultNostrClient,
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostrClientFacade.js";
|
||||
|
||||
export { requestDefaultExtensionPermissions };
|
||||
export { defaultNostrClient as nostrClient };
|
||||
|
||||
export const publishVideoReaction = (...args) => {
|
||||
if (
|
||||
!defaultNostrClient ||
|
||||
typeof defaultNostrClient.publishVideoReaction !== "function"
|
||||
) {
|
||||
throw new Error("Video reaction publishing is unavailable in this build.");
|
||||
}
|
||||
return defaultNostrClient.publishVideoReaction(...args);
|
||||
};
|
||||
|
||||
export {
|
||||
__testExports,
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
publishViewEvent as publishViewEventForClient,
|
||||
recordVideoView as recordVideoViewForClient,
|
||||
} from "./viewEvents.js";
|
||||
import { publishVideoReaction as publishVideoReactionForClient } from "./reactionEvents.js";
|
||||
import {
|
||||
publishEventToRelay,
|
||||
publishEventToRelays,
|
||||
@@ -2747,6 +2748,15 @@ export class NostrClient {
|
||||
});
|
||||
}
|
||||
|
||||
async publishVideoReaction(pointer, options = {}) {
|
||||
return publishVideoReactionForClient(this, pointer, options, {
|
||||
resolveActiveSigner,
|
||||
shouldRequestExtensionPermissions,
|
||||
signEventWithPrivateKey,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
});
|
||||
}
|
||||
|
||||
async recordVideoView(videoPointer, options = {}) {
|
||||
return recordVideoViewForClient(this, videoPointer, options, {
|
||||
resolveActiveSigner,
|
||||
|
||||
@@ -55,3 +55,5 @@ export {
|
||||
publishViewEvent,
|
||||
recordVideoView,
|
||||
} from "./viewEvents.js";
|
||||
|
||||
export { publishVideoReaction } from "./reactionEvents.js";
|
||||
@@ -0,0 +1,170 @@
|
||||
import { buildReactionEvent } 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;
|
||||
}
|
||||
|
||||
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 = Array.isArray(options.additionalTags)
|
||||
? options.additionalTags.filter(
|
||||
(tag) => Array.isArray(tag) && typeof tag[0] === "string"
|
||||
)
|
||||
: [];
|
||||
|
||||
const pointerTag =
|
||||
pointer.type === "a"
|
||||
? pointer.relay
|
||||
? ["a", pointer.value, pointer.relay]
|
||||
: ["a", pointer.value]
|
||||
: pointer.relay
|
||||
? ["e", pointer.value, pointer.relay]
|
||||
: ["e", pointer.value];
|
||||
|
||||
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,
|
||||
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,
|
||||
};
|
||||
}
|
||||
+94
-19
@@ -16,6 +16,7 @@ export const NOTE_TYPES = Object.freeze({
|
||||
REPOST: "repost",
|
||||
RELAY_LIST: "relayList",
|
||||
VIEW_EVENT: "viewEvent",
|
||||
VIDEO_REACTION: "videoReaction",
|
||||
WATCH_HISTORY_INDEX: "watchHistoryIndex",
|
||||
WATCH_HISTORY_CHUNK: "watchHistoryChunk",
|
||||
SUBSCRIPTION_LIST: "subscriptionList",
|
||||
@@ -194,6 +195,17 @@ const BASE_SCHEMAS = {
|
||||
description: "Optional plaintext content used for diagnostics.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.VIDEO_REACTION]: {
|
||||
type: NOTE_TYPES.VIDEO_REACTION,
|
||||
label: "Reaction event",
|
||||
kind: 7,
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description:
|
||||
"Reaction payload for the referenced event (e.g., '+', '-', or emoji).",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.WATCH_HISTORY_INDEX]: {
|
||||
type: NOTE_TYPES.WATCH_HISTORY_INDEX,
|
||||
label: "Watch history index",
|
||||
@@ -384,6 +396,41 @@ function appendSchemaTags(tags, schema) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
function collectPointerTags(schema, {
|
||||
pointerValue,
|
||||
pointerTag,
|
||||
pointerTags = [],
|
||||
} = {}) {
|
||||
const normalized = [];
|
||||
|
||||
const pointerTagName = schema?.pointerTagName;
|
||||
if (pointerValue && pointerTagName) {
|
||||
const normalizedValue =
|
||||
typeof pointerValue === "string" ? pointerValue : String(pointerValue);
|
||||
if (normalizedValue) {
|
||||
normalized.push([pointerTagName, normalizedValue]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(pointerTag) && pointerTag.length >= 2) {
|
||||
normalized.push(
|
||||
pointerTag.map((value) => (typeof value === "string" ? value : String(value)))
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(pointerTags)) {
|
||||
pointerTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
normalized.push(
|
||||
tag.map((value) => (typeof value === "string" ? value : String(value)))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function buildVideoPostEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
@@ -590,25 +637,11 @@ export function buildViewEvent({
|
||||
tags.push([schema.topicTag.name, schema.topicTag.value]);
|
||||
}
|
||||
|
||||
const pointerTagName = schema?.pointerTagName;
|
||||
if (pointerValue && pointerTagName) {
|
||||
tags.push([pointerTagName, pointerValue]);
|
||||
}
|
||||
const normalizedPointerTags = [];
|
||||
if (Array.isArray(pointerTag) && pointerTag.length >= 2) {
|
||||
normalizedPointerTags.push(
|
||||
pointerTag.map((value) => (typeof value === "string" ? value : String(value)))
|
||||
);
|
||||
}
|
||||
if (Array.isArray(pointerTags)) {
|
||||
pointerTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
normalizedPointerTags.push(
|
||||
tag.map((value) => (typeof value === "string" ? value : String(value)))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
const normalizedPointerTags = collectPointerTags(schema, {
|
||||
pointerValue,
|
||||
pointerTag,
|
||||
pointerTags,
|
||||
});
|
||||
normalizedPointerTags.forEach((tag) => {
|
||||
tags.push(tag);
|
||||
});
|
||||
@@ -650,6 +683,48 @@ export function buildViewEvent({
|
||||
};
|
||||
}
|
||||
|
||||
export function buildReactionEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerValue,
|
||||
pointerTag,
|
||||
pointerTags = [],
|
||||
additionalTags = [],
|
||||
content = "",
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_REACTION);
|
||||
const tags = [];
|
||||
|
||||
const normalizedPointerTags = collectPointerTags(schema, {
|
||||
pointerValue,
|
||||
pointerTag,
|
||||
pointerTags,
|
||||
});
|
||||
normalizedPointerTags.forEach((tag) => {
|
||||
tags.push(tag);
|
||||
});
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? 7,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWatchHistoryIndexEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { publishVideoReaction } from "./nostr.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
|
||||
export const reactionCounter = {
|
||||
async publish(pointer, options = {}) {
|
||||
try {
|
||||
const result = await publishVideoReaction(pointer, options);
|
||||
if (!result || !result.ok) {
|
||||
userLogger.warn(
|
||||
"[reactionCounter] Reaction publish rejected by relays:",
|
||||
result?.results || result
|
||||
);
|
||||
} else if (Array.isArray(result.acceptedRelays)) {
|
||||
devLogger.info(
|
||||
`[reactionCounter] Reaction accepted by ${result.acceptedRelays.length} relay(s):`,
|
||||
result.acceptedRelays.join(", ")
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
userLogger.warn("[reactionCounter] Failed to publish reaction event:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default reactionCounter;
|
||||
Reference in new issue
Block a user