mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(nip71): Phase 1b — mirror teardown (NIP-09 delete + empty-replace tombstone)
Adds remove(video, options) to the mirror service for delete / toggle-off: - NIP-09 kind-5 delete referencing BOTH addressable kinds (34235:pub:root + 34236:pub:root, with k tags) so it's robust to a since-changed orientation. - empty-replace tombstone (same d-tag, title, NO imeta) so clients that cache addressable events but ignore NIP-09 still see the entry cleared; uses the short kind for portrait videos. Published to the write/outbox relay set; unavailable without a signer. tests/nip71-mirror-service.test.mjs: remove publishes delete (both kinds) + tombstone; portrait => 34236 tombstone; unavailable without signer. Mutation-verified (tombstone emission + both-kind a-tags load-bearing). Note: full unit suite green apart from the documented order-dependent flakes (uploadModal-reset; a transient login-modal clipboard global-state flake) — both pass in isolation and are unrelated to this change. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
e20191d6bc
commit
58f1909570
2 files changed
+113
-1
No files matched your search
@@ -114,7 +114,79 @@ export function createNip71MirrorService({
|
||||
};
|
||||
}
|
||||
|
||||
return { isAvailable, canMirror, publish };
|
||||
// Tear down the mirror (on delete, or when the user toggles sync off). Belt-and-
|
||||
// suspenders per the plan: a NIP-09 delete referencing BOTH addressable kinds
|
||||
// (robust to a since-changed orientation), AND an empty-replace tombstone so
|
||||
// clients that cache addressable events but ignore NIP-09 still see it cleared.
|
||||
async function remove(video, options = {}) {
|
||||
if (!isAvailable()) {
|
||||
return { ok: false, error: "unavailable" };
|
||||
}
|
||||
const pubkey = str(video?.pubkey) || getActivePubkey();
|
||||
const root = str(video?.videoRootId);
|
||||
if (!pubkey || !root) {
|
||||
return { ok: false, error: "invalid" };
|
||||
}
|
||||
|
||||
const createdAt = Math.floor(now() / 1000);
|
||||
const reason = str(options.reason) || "Removed NIP-71 mirror";
|
||||
|
||||
const deleteEvent = {
|
||||
kind: 5,
|
||||
pubkey,
|
||||
created_at: createdAt,
|
||||
content: reason,
|
||||
tags: [
|
||||
["a", `34235:${pubkey}:${root}`],
|
||||
["a", `34236:${pubkey}:${root}`],
|
||||
["k", "34235"],
|
||||
["k", "34236"],
|
||||
],
|
||||
};
|
||||
|
||||
const width = Number(video?.width);
|
||||
const height = Number(video?.height);
|
||||
const portrait =
|
||||
Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 && height > width;
|
||||
const tombstoneKind =
|
||||
options.short === true || (options.short !== false && portrait) ? 34236 : 34235;
|
||||
const tombstone = {
|
||||
kind: tombstoneKind,
|
||||
pubkey,
|
||||
created_at: createdAt,
|
||||
// No imeta → nothing plays; title kept so the entry reads as removed.
|
||||
tags: [["d", root], ["title", str(video?.title) || "[removed]"], ["client", "bitvid"]],
|
||||
content: "",
|
||||
};
|
||||
|
||||
const relays = getWriteRelays() || [];
|
||||
const pool = getPool();
|
||||
if (!pool || !relays.length) {
|
||||
return { ok: false, error: "no-relays" };
|
||||
}
|
||||
|
||||
let publishedOk = 0;
|
||||
let total = 0;
|
||||
for (const template of [deleteEvent, tombstone]) {
|
||||
total += 1;
|
||||
let signed;
|
||||
try {
|
||||
signed = await signEvent(template);
|
||||
} catch (error) {
|
||||
userLogger.warn("[nip71Mirror] Failed to sign teardown event:", error);
|
||||
continue;
|
||||
}
|
||||
const results = await publishEventToRelays(pool, relays, signed);
|
||||
const { accepted } = summarizePublishResults(results);
|
||||
if (accepted.length > 0) {
|
||||
publishedOk += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: publishedOk > 0, published: publishedOk, total };
|
||||
}
|
||||
|
||||
return { isAvailable, canMirror, publish, remove };
|
||||
}
|
||||
|
||||
export const nip71MirrorService = createNip71MirrorService();
|
||||
|
||||
@@ -105,3 +105,43 @@ test("publish is unavailable without a signer / pubkey", async () => {
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error, "unavailable");
|
||||
});
|
||||
|
||||
test("remove() publishes a NIP-09 delete (both addressable kinds) AND a tombstone", async () => {
|
||||
const { service, relay } = makeService();
|
||||
const result = await service.remove(baseVideo());
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(relay.events.length, 2, "delete + empty-replace tombstone");
|
||||
|
||||
const del = relay.events.find((e) => e.kind === 5);
|
||||
assert.ok(del, "must publish a NIP-09 kind-5 delete");
|
||||
const aTags = del.tags.filter((t) => t[0] === "a").map((t) => t[1]).sort();
|
||||
assert.deepEqual(aTags, [
|
||||
`34235:${PUBKEY}:root-1`,
|
||||
`34236:${PUBKEY}:root-1`,
|
||||
], "delete references both addressable kinds (orientation-robust)");
|
||||
const kTags = del.tags.filter((t) => t[0] === "k").map((t) => t[1]).sort();
|
||||
assert.deepEqual(kTags, ["34235", "34236"]);
|
||||
|
||||
const tomb = relay.events.find((e) => e.kind === 34235 || e.kind === 34236);
|
||||
assert.ok(tomb, "must publish an empty-replace tombstone");
|
||||
assert.equal(tomb.tags.find((t) => t[0] === "d")[1], "root-1", "same d-tag");
|
||||
assert.equal(
|
||||
tomb.tags.some((t) => t[0] === "imeta"),
|
||||
false,
|
||||
"tombstone has no playable imeta",
|
||||
);
|
||||
});
|
||||
|
||||
test("remove() tombstone uses the short kind for portrait videos", async () => {
|
||||
const { service, relay } = makeService();
|
||||
await service.remove(baseVideo({ width: 1080, height: 1920 }));
|
||||
const tomb = relay.events.find((e) => e.kind === 34235 || e.kind === 34236);
|
||||
assert.equal(tomb.kind, 34236, "portrait => 34236 tombstone");
|
||||
});
|
||||
|
||||
test("remove() is unavailable without a signer", async () => {
|
||||
const { service } = makeService({ available: false });
|
||||
const result = await service.remove(baseVideo());
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error, "unavailable");
|
||||
});
|
||||
Reference in new issue
Block a user