mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
171 lines
5.7 KiB
JavaScript
171 lines
5.7 KiB
JavaScript
// Keeps an opted-in NIP-71 mirror (kind 34235/36) in lockstep with the canonical
|
|
// kind-30078 video on edit/delete — the "no UX fuss" glue so the two events never
|
|
// drift. Extracted from nostrService (which is size-capped); called from its
|
|
// edit/delete handlers. All best-effort: a mirror failure must never break the
|
|
// core edit/delete.
|
|
|
|
import { FEATURE_NIP71_MIRROR } from "../constants.js";
|
|
import { nip71MirrorService } from "./nip71MirrorService.js";
|
|
import { convertEventToVideo } from "../nostr/index.js";
|
|
import {
|
|
isMirrorEnabled,
|
|
setMirrorEnabled,
|
|
isAutoShareEnabled,
|
|
resolveEditSync,
|
|
resolveDeleteSync,
|
|
resolvePublishSync,
|
|
} from "./nip71MirrorFlags.js";
|
|
import { devLogger } from "../utils/logger.js";
|
|
|
|
// On publish of a NEW video, mirror it when the account default permits it and
|
|
// this upload has not explicitly opted out. Sets the per-video flag so later
|
|
// edits/deletes keep it in sync. Best-effort.
|
|
export async function syncNip71MirrorAfterPublish(detail = {}) {
|
|
const pubkey = typeof detail?.pubkey === "string" ? detail.pubkey : "";
|
|
const legacyEvent = detail?.result?.legacy;
|
|
if (!pubkey || !legacyEvent || typeof legacyEvent !== "object") {
|
|
return;
|
|
}
|
|
let video;
|
|
try {
|
|
video = convertEventToVideo(legacyEvent);
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
if (!video || video.invalid || !video.videoRootId) {
|
|
return;
|
|
}
|
|
const withPubkey = { ...video, pubkey };
|
|
const decision = resolvePublishSync({
|
|
featureOn: FEATURE_NIP71_MIRROR,
|
|
autoShare:
|
|
isAutoShareEnabled(pubkey) && detail?.payload?.mirrorNip71 !== false,
|
|
eligible: nip71MirrorService.canMirror(withPubkey).ok,
|
|
});
|
|
if (decision.action !== "publish") {
|
|
return;
|
|
}
|
|
try {
|
|
const result = await nip71MirrorService.publish(withPubkey);
|
|
if (result?.ok) {
|
|
setMirrorEnabled(pubkey, video.videoRootId, true);
|
|
}
|
|
} catch (error) {
|
|
devLogger.warn("[nip71MirrorSync] auto-share publish failed", error);
|
|
}
|
|
}
|
|
|
|
export async function syncNip71MirrorAfterEdit({
|
|
updatedData,
|
|
originalEvent,
|
|
result,
|
|
pubkey,
|
|
} = {}) {
|
|
if (!updatedData || typeof updatedData !== "object") {
|
|
return;
|
|
}
|
|
let publishedVideo = null;
|
|
try {
|
|
if (result && typeof result === "object") {
|
|
const parsed = convertEventToVideo(result);
|
|
if (parsed && !parsed.invalid) {
|
|
publishedVideo = parsed;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
devLogger.warn("[nip71MirrorSync] couldn't parse edited video", error);
|
|
}
|
|
const videoRootId =
|
|
(typeof publishedVideo?.videoRootId === "string" && publishedVideo.videoRootId) ||
|
|
(typeof updatedData.videoRootId === "string" && updatedData.videoRootId) ||
|
|
(typeof originalEvent?.videoRootId === "string" && originalEvent.videoRootId) ||
|
|
"";
|
|
const authorPubkey = typeof pubkey === "string" ? pubkey : "";
|
|
if (!videoRootId || !authorPubkey) {
|
|
return;
|
|
}
|
|
// Prefer the actual signed canonical result: it includes fields preserved by
|
|
// the edit builder (root, dimensions, hashes) that the form does not resend.
|
|
const video = { ...updatedData, ...publishedVideo, pubkey: authorPubkey, videoRootId };
|
|
const decision = resolveEditSync({
|
|
featureOn: FEATURE_NIP71_MIRROR,
|
|
enabled: isMirrorEnabled(authorPubkey, videoRootId),
|
|
eligible: nip71MirrorService.canMirror(video).ok,
|
|
});
|
|
if (decision.action === "none") {
|
|
return;
|
|
}
|
|
try {
|
|
if (decision.action === "publish") {
|
|
await nip71MirrorService.publish(video);
|
|
} else {
|
|
// "unshare": no longer eligible (e.g. flipped private) — pull it down.
|
|
const removal = await nip71MirrorService.remove(video);
|
|
// Keep the flag when the relay operation failed so a later edit can retry
|
|
// rather than silently orphaning a previously public mirror.
|
|
if (removal?.ok) {
|
|
setMirrorEnabled(authorPubkey, videoRootId, false);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
devLogger.warn("[nip71MirrorSync] edit sync failed", error);
|
|
}
|
|
}
|
|
|
|
// Drive the lifecycle off nostrService's existing "videos:edited"/"videos:deleted"
|
|
// events — so the size-capped nostrService needs no changes. Idempotent: a guard
|
|
// prevents double-registration. Returns an unsubscribe fn.
|
|
let registered = false;
|
|
export function initNip71MirrorSync(nostrService) {
|
|
if (!nostrService || typeof nostrService.on !== "function" || registered) {
|
|
return () => {};
|
|
}
|
|
registered = true;
|
|
const offEdit = nostrService.on("videos:edited", ({
|
|
updatedData,
|
|
originalEvent,
|
|
result,
|
|
pubkey,
|
|
} = {}) => {
|
|
void syncNip71MirrorAfterEdit({ updatedData, originalEvent, result, pubkey });
|
|
});
|
|
const offDelete = nostrService.on("videos:deleted", ({ videoRootId, video, pubkey } = {}) => {
|
|
void syncNip71MirrorAfterDelete({ videoRootId, video, pubkey });
|
|
});
|
|
const offPublish = nostrService.on("videos:published", (detail) => {
|
|
void syncNip71MirrorAfterPublish(detail);
|
|
});
|
|
return () => {
|
|
registered = false;
|
|
if (typeof offEdit === "function") offEdit();
|
|
if (typeof offDelete === "function") offDelete();
|
|
if (typeof offPublish === "function") offPublish();
|
|
};
|
|
}
|
|
|
|
export async function syncNip71MirrorAfterDelete({ videoRootId, video, pubkey } = {}) {
|
|
const root = typeof videoRootId === "string" ? videoRootId : "";
|
|
const authorPubkey = typeof pubkey === "string" ? pubkey : "";
|
|
if (!root || !authorPubkey) {
|
|
return;
|
|
}
|
|
const decision = resolveDeleteSync({
|
|
featureOn: FEATURE_NIP71_MIRROR,
|
|
enabled: isMirrorEnabled(authorPubkey, root),
|
|
});
|
|
if (decision.action === "none") {
|
|
return;
|
|
}
|
|
try {
|
|
await nip71MirrorService.remove({
|
|
...(video || {}),
|
|
videoRootId: root,
|
|
pubkey: authorPubkey,
|
|
});
|
|
} catch (error) {
|
|
devLogger.warn("[nip71MirrorSync] delete sync failed", error);
|
|
} finally {
|
|
setMirrorEnabled(authorPubkey, root, false);
|
|
}
|
|
}
|