mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +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();
|
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.ok, false);
|
||||||
assert.equal(result.error, "unavailable");
|
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