Files
bitvid/js/services/nip71MirrorSync.js
T
thePR0M3TH3ANandClaude Opus 4.8 685e52656b nip71: account-level "auto-share new public videos" toggle
Adds an opt-in account preference (off by default, per pubkey) that
auto-mirrors newly published eligible public videos to NIP-71
(kinds 34235/34236) so they surface on other Nostr video clients —
no per-video opt-in needed.

- nip71MirrorFlags: isAutoShareEnabled/setAutoShareEnabled (persisted
  per pubkey) + pure resolvePublishSync({featureOn,autoShare,eligible}).
- nip71MirrorSync: syncNip71MirrorAfterPublish + a videos:published
  subscription in initNip71MirrorSync; converts the canonical 30078
  event, checks eligibility (public + hosted URL + ALLOW_NSFW gate),
  publishes the mirror, and records the per-video flag so later
  edits/deletes stay in lockstep. All best-effort.
- My Videos pane: an "Auto-share new public videos to other Nostr apps"
  checkbox wired to setAutoShareEnabled, gated by FEATURE_NIP71_MIRROR.

Tests (mutation-verified): auto-share persistence, resolvePublishSync
(eligible+on=>publish; off/ineligible=>none), and videos:published
subscription wiring.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 14:06:19 -04:00

140 lines
4.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, auto-mirror it when the account opted into
// "auto-share new public videos" and it's eligible. 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),
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, pubkey } = {}) {
if (!updatedData || typeof updatedData !== "object") {
return;
}
const videoRootId =
typeof updatedData.videoRootId === "string" ? updatedData.videoRootId : "";
const authorPubkey = typeof pubkey === "string" ? pubkey : "";
if (!videoRootId || !authorPubkey) {
return;
}
const video = { ...updatedData, 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.
await nip71MirrorService.remove(video);
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, pubkey } = {}) => {
void syncNip71MirrorAfterEdit({ updatedData, 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);
}
}