// 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); } }