mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-13 08:12:31 +00:00
Add file hash support to NIP-94 mirrors
This commit is contained in:
1 parent
36bcbae0e9
commit
6eb8594e28
3 files changed
+231
-30
No files matched your search
+165
-30
@@ -3,6 +3,7 @@
|
|||||||
import { isDevMode } from "../config.js";
|
import { isDevMode } from "../config.js";
|
||||||
import { FEATURE_PUBLISH_NIP71 } from "../constants.js";
|
import { FEATURE_PUBLISH_NIP71 } from "../constants.js";
|
||||||
import { accessControl } from "../accessControl.js";
|
import { accessControl } from "../accessControl.js";
|
||||||
|
import { bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js";
|
||||||
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
||||||
import {
|
import {
|
||||||
buildNip71MetadataTags,
|
buildNip71MetadataTags,
|
||||||
@@ -38,7 +39,6 @@ import {
|
|||||||
} from "./watchHistory.js";
|
} from "./watchHistory.js";
|
||||||
import {
|
import {
|
||||||
buildVideoPostEvent,
|
buildVideoPostEvent,
|
||||||
buildVideoMirrorEvent,
|
|
||||||
buildRepostEvent,
|
buildRepostEvent,
|
||||||
buildWatchHistoryIndexEvent,
|
buildWatchHistoryIndexEvent,
|
||||||
buildWatchHistoryChunkEvent,
|
buildWatchHistoryChunkEvent,
|
||||||
@@ -413,6 +413,85 @@ function extractVideoPublishPayload(rawPayload) {
|
|||||||
return { videoData: normalizedVideoData, nip71Metadata };
|
return { videoData: normalizedVideoData, nip71Metadata };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHexHash(candidate) {
|
||||||
|
if (candidate === undefined || candidate === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const stringValue =
|
||||||
|
typeof candidate === "string" ? candidate : String(candidate);
|
||||||
|
const trimmed = stringValue.trim().toLowerCase();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return HEX64_REGEX.test(trimmed) ? trimmed : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlobConstructor = typeof Blob !== "undefined" ? Blob : null;
|
||||||
|
let sharedTextEncoder = null;
|
||||||
|
|
||||||
|
function getSharedTextEncoder() {
|
||||||
|
if (!sharedTextEncoder && typeof TextEncoder !== "undefined") {
|
||||||
|
sharedTextEncoder = new TextEncoder();
|
||||||
|
}
|
||||||
|
return sharedTextEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function valueToUint8Array(value) {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
BlobConstructor &&
|
||||||
|
value instanceof BlobConstructor &&
|
||||||
|
typeof value.arrayBuffer === "function"
|
||||||
|
) {
|
||||||
|
const buffer = await value.arrayBuffer();
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
devLogger.warn("[nostr] Failed to read Blob while computing hash:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ArrayBuffer !== "undefined") {
|
||||||
|
if (value instanceof ArrayBuffer) {
|
||||||
|
return new Uint8Array(value);
|
||||||
|
}
|
||||||
|
if (ArrayBuffer.isView?.(value)) {
|
||||||
|
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(value)) {
|
||||||
|
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const encoder = getSharedTextEncoder();
|
||||||
|
return encoder ? encoder.encode(value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeSha256HexFromValue(value) {
|
||||||
|
const data = await valueToUint8Array(value);
|
||||||
|
if (!data) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const digest = sha256(data);
|
||||||
|
const hex = typeof digest === "string" ? digest : bytesToHex(digest);
|
||||||
|
return hex ? hex.toLowerCase() : "";
|
||||||
|
} catch (error) {
|
||||||
|
devLogger.warn("[nostr] Failed to compute SHA-256 for mirror payload:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function eventToAddressPointer(event) {
|
function eventToAddressPointer(event) {
|
||||||
if (!event || typeof event !== "object") {
|
if (!event || typeof event !== "object") {
|
||||||
return "";
|
return "";
|
||||||
@@ -4052,49 +4131,105 @@ export class NostrClient {
|
|||||||
"application/octet-stream";
|
"application/octet-stream";
|
||||||
const mimeType = mimeTypeSource.toLowerCase();
|
const mimeType = mimeTypeSource.toLowerCase();
|
||||||
|
|
||||||
const mirrorTags = [
|
const fileHashCandidates = [
|
||||||
["url", finalUrl],
|
videoData.fileSha256,
|
||||||
["m", mimeType],
|
videoData.uploadedFileSha256,
|
||||||
|
videoPayload?.legacyFormData?.fileSha256,
|
||||||
|
videoPayload?.fileSha256,
|
||||||
];
|
];
|
||||||
|
let fileSha256 = "";
|
||||||
if (finalThumbnail) {
|
for (const candidate of fileHashCandidates) {
|
||||||
mirrorTags.push(["thumb", finalThumbnail]);
|
const normalized = normalizeHexHash(candidate);
|
||||||
|
if (normalized) {
|
||||||
|
fileSha256 = normalized;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const altText = finalDescription || finalTitle || "";
|
const originalHashCandidates = [
|
||||||
if (altText) {
|
videoData.originalFileSha256,
|
||||||
mirrorTags.push(["alt", altText]);
|
videoPayload?.legacyFormData?.originalFileSha256,
|
||||||
|
videoPayload?.originalFileSha256,
|
||||||
|
];
|
||||||
|
let originalFileSha256 = "";
|
||||||
|
for (const candidate of originalHashCandidates) {
|
||||||
|
const normalized = normalizeHexHash(candidate);
|
||||||
|
if (normalized) {
|
||||||
|
originalFileSha256 = normalized;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!contentObject.isPrivate && finalMagnet) {
|
const uploadedFile =
|
||||||
mirrorTags.push(["magnet", finalMagnet]);
|
videoData?.uploadedFile ||
|
||||||
|
videoData?.file ||
|
||||||
|
videoPayload?.legacyFormData?.uploadedFile ||
|
||||||
|
videoPayload?.legacyFormData?.file ||
|
||||||
|
videoPayload?.uploadedFile ||
|
||||||
|
videoPayload?.file ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
const originalFile =
|
||||||
|
videoData?.originalFile ||
|
||||||
|
videoPayload?.legacyFormData?.originalFile ||
|
||||||
|
videoPayload?.originalFile ||
|
||||||
|
uploadedFile;
|
||||||
|
|
||||||
|
if (!fileSha256 && uploadedFile) {
|
||||||
|
fileSha256 = await computeSha256HexFromValue(uploadedFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mirrorEvent = buildVideoMirrorEvent({
|
if (!originalFileSha256 && originalFile) {
|
||||||
pubkey: normalizedPubkey,
|
originalFileSha256 = await computeSha256HexFromValue(originalFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!originalFileSha256 && fileSha256) {
|
||||||
|
originalFileSha256 = fileSha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mirrorOptions = {
|
||||||
|
url: finalUrl,
|
||||||
|
magnet: finalMagnet,
|
||||||
|
thumbnail: finalThumbnail,
|
||||||
|
description: finalDescription,
|
||||||
|
title: finalTitle,
|
||||||
|
mimeType,
|
||||||
|
isPrivate: contentObject.isPrivate,
|
||||||
|
actorPubkey: normalizedPubkey,
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
tags: mirrorTags,
|
};
|
||||||
content: altText,
|
|
||||||
});
|
|
||||||
|
|
||||||
devLogger.log("Prepared NIP-94 mirror event:", mirrorEvent);
|
if (fileSha256) {
|
||||||
|
mirrorOptions.fileSha256 = fileSha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalFileSha256) {
|
||||||
|
mirrorOptions.originalFileSha256 = originalFileSha256;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.signAndPublishEvent(mirrorEvent, {
|
const mirrorResult = await this.mirrorVideoEvent(
|
||||||
context: "NIP-94 mirror",
|
signedEvent.id,
|
||||||
logName: "NIP-94 mirror",
|
mirrorOptions,
|
||||||
devLogLabel: "NIP-94 mirror",
|
|
||||||
rejectionLogLevel: "warn",
|
|
||||||
});
|
|
||||||
|
|
||||||
devLogger.log(
|
|
||||||
"NIP-94 mirror dispatched for hosted URL:",
|
|
||||||
finalUrl
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (mirrorResult?.ok) {
|
||||||
|
devLogger.log("Prepared NIP-94 mirror event:", mirrorResult.event);
|
||||||
|
devLogger.log(
|
||||||
|
"NIP-94 mirror dispatched for hosted URL:",
|
||||||
|
finalUrl,
|
||||||
|
);
|
||||||
|
} else if (mirrorResult) {
|
||||||
|
devLogger.warn(
|
||||||
|
"[nostr] NIP-94 mirror rejected:",
|
||||||
|
mirrorResult.error || "mirror-failed",
|
||||||
|
mirrorResult.details || null,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (mirrorError) {
|
} catch (mirrorError) {
|
||||||
devLogger.warn(
|
devLogger.warn(
|
||||||
"[nostr] NIP-94 mirror rejected by all relays:",
|
"[nostr] Failed to publish NIP-94 mirror:",
|
||||||
mirrorError
|
mirrorError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else devLogger.log("Skipping NIP-94 mirror: no hosted URL provided.");
|
} else devLogger.log("Skipping NIP-94 mirror: no hosted URL provided.");
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ const REBROADCAST_GUARD_PREFIX = "bitvid:rebroadcast:v1";
|
|||||||
const rebroadcastAttemptMemory = new Map();
|
const rebroadcastAttemptMemory = new Map();
|
||||||
|
|
||||||
const HEX_PRIVATE_KEY_REGEX = /^[0-9a-f]{64}$/i;
|
const HEX_PRIVATE_KEY_REGEX = /^[0-9a-f]{64}$/i;
|
||||||
|
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||||
|
|
||||||
|
function normalizeSha256TagValue(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const stringValue = typeof value === "string" ? value : String(value);
|
||||||
|
const trimmed = stringValue.trim().toLowerCase();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!HEX64_REGEX.test(trimmed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
export function signEventWithPrivateKey(event, privateKey) {
|
export function signEventWithPrivateKey(event, privateKey) {
|
||||||
const tools = getCachedNostrTools();
|
const tools = getCachedNostrTools();
|
||||||
@@ -818,6 +834,18 @@ export async function mirrorVideoEvent({
|
|||||||
const explicitAlt = sanitize(options.altText);
|
const explicitAlt = sanitize(options.altText);
|
||||||
const altText = explicitAlt || description || title || "";
|
const altText = explicitAlt || description || title || "";
|
||||||
|
|
||||||
|
const fileSha256 = normalizeSha256TagValue(options.fileSha256);
|
||||||
|
if (fileSha256 === null) {
|
||||||
|
return { ok: false, error: "invalid-file-sha" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalFileSha256 = normalizeSha256TagValue(
|
||||||
|
options.originalFileSha256,
|
||||||
|
);
|
||||||
|
if (originalFileSha256 === null) {
|
||||||
|
return { ok: false, error: "invalid-original-file-sha" };
|
||||||
|
}
|
||||||
|
|
||||||
const tags = [];
|
const tags = [];
|
||||||
tags.push(["url", url]);
|
tags.push(["url", url]);
|
||||||
if (mimeType) {
|
if (mimeType) {
|
||||||
@@ -832,6 +860,12 @@ export async function mirrorVideoEvent({
|
|||||||
if (!isPrivate && magnet) {
|
if (!isPrivate && magnet) {
|
||||||
tags.push(["magnet", magnet]);
|
tags.push(["magnet", magnet]);
|
||||||
}
|
}
|
||||||
|
if (fileSha256) {
|
||||||
|
tags.push(["x", fileSha256]);
|
||||||
|
}
|
||||||
|
if (originalFileSha256) {
|
||||||
|
tags.push(["ox", originalFileSha256]);
|
||||||
|
}
|
||||||
|
|
||||||
const additionalTags = Array.isArray(options.additionalTags)
|
const additionalTags = Array.isArray(options.additionalTags)
|
||||||
? options.additionalTags.filter((tag) => Array.isArray(tag) && tag.length >= 2)
|
? options.additionalTags.filter((tag) => Array.isArray(tag) && tag.length >= 2)
|
||||||
|
|||||||
@@ -107,3 +107,35 @@ test("mirrorVideoEvent lowercases inferred MIME types", async () => {
|
|||||||
assert.ok(mTag, "mirror events must include a MIME tag");
|
assert.ok(mTag, "mirror events must include a MIME tag");
|
||||||
assert.equal(mTag[1], "video/webm");
|
assert.equal(mTag[1], "video/webm");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mirrorVideoEvent includes hash tags when provided", async () => {
|
||||||
|
const eventId = "hash-tags";
|
||||||
|
const actorPubkey = "3".repeat(64);
|
||||||
|
const { client } = createPublishClient({ actorPubkey });
|
||||||
|
const fileHash = "a".repeat(64);
|
||||||
|
const originalHash = "b".repeat(64);
|
||||||
|
|
||||||
|
const result = await mirrorVideoEvent({
|
||||||
|
client,
|
||||||
|
eventId,
|
||||||
|
options: {
|
||||||
|
url: "https://videos.example/demo.mp4",
|
||||||
|
fileSha256: fileHash.toUpperCase(),
|
||||||
|
originalFileSha256: originalHash,
|
||||||
|
},
|
||||||
|
resolveActiveSigner: resolveActiveSignerStub,
|
||||||
|
shouldRequestExtensionPermissions: shouldRequestExtensionPermissionsStub,
|
||||||
|
signEventWithPrivateKey: signEventWithPrivateKeyStub,
|
||||||
|
inferMimeTypeFromUrl: () => "video/mp4",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
const xTag = result.event?.tags?.find(
|
||||||
|
(tag) => Array.isArray(tag) && tag[0] === "x",
|
||||||
|
);
|
||||||
|
const oxTag = result.event?.tags?.find(
|
||||||
|
(tag) => Array.isArray(tag) && tag[0] === "ox",
|
||||||
|
);
|
||||||
|
assert.deepEqual(xTag, ["x", fileHash]);
|
||||||
|
assert.deepEqual(oxTag, ["ox", originalHash]);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user