mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
492 lines
14 KiB
JavaScript
492 lines
14 KiB
JavaScript
import { HEX64_REGEX, normalizeHexHash } from "../utils/hex.js";
|
|
import { infoHashFromMagnet } from "../magnets.js";
|
|
import { resolveStoragePointerValue, getStoragePointerFromTags } from "../utils/storagePointer.js";
|
|
import { buildNip71MetadataTags, extractNip71MetadataFromTags } from "./nip71.js";
|
|
import { buildVideoPostEvent } from "../nostrEventSchemas.js";
|
|
import { computeSha256HexFromValue } from "../utils/cryptoUtils.js";
|
|
import { inferMimeTypeFromUrl } from "../utils/mime.js";
|
|
import { devLogger } from "../utils/logger.js";
|
|
|
|
export function extractVideoPublishPayload(rawPayload) {
|
|
let videoData = rawPayload;
|
|
let nip71Metadata = null;
|
|
|
|
if (rawPayload && typeof rawPayload === "object") {
|
|
if (rawPayload.nip71 && typeof rawPayload.nip71 === "object") {
|
|
nip71Metadata = rawPayload.nip71;
|
|
}
|
|
if (
|
|
rawPayload.legacyFormData &&
|
|
typeof rawPayload.legacyFormData === "object"
|
|
) {
|
|
videoData = rawPayload.legacyFormData;
|
|
} else if (
|
|
Object.prototype.hasOwnProperty.call(rawPayload, "legacyFormData")
|
|
) {
|
|
videoData = rawPayload.legacyFormData || {};
|
|
}
|
|
}
|
|
|
|
if (!videoData || typeof videoData !== "object") {
|
|
videoData = {};
|
|
}
|
|
|
|
const normalizedVideoData = { ...videoData };
|
|
const isNsfw = videoData.isNsfw === true;
|
|
normalizedVideoData.isNsfw = isNsfw;
|
|
normalizedVideoData.isForKids =
|
|
videoData.isForKids === true && !isNsfw;
|
|
|
|
return { videoData: normalizedVideoData, nip71Metadata };
|
|
}
|
|
|
|
export async function prepareVideoPublishPayload(videoPayload, pubkey, { timestamp = Math.floor(Date.now() / 1000) } = {}) {
|
|
const normalizedPubkey = typeof pubkey === "string" ? pubkey.trim() : "";
|
|
if (!normalizedPubkey) {
|
|
throw new Error("Not logged in to publish video.");
|
|
}
|
|
|
|
const { videoData, nip71Metadata } = extractVideoPublishPayload(videoPayload);
|
|
|
|
devLogger.log("Publishing new video with data:", videoData);
|
|
if (nip71Metadata) {
|
|
devLogger.log("Including NIP-71 metadata:", nip71Metadata);
|
|
}
|
|
|
|
const rawMagnet = typeof videoData.magnet === "string" ? videoData.magnet : "";
|
|
const finalMagnet = rawMagnet.trim();
|
|
const finalUrl =
|
|
typeof videoData.url === "string" ? videoData.url.trim() : "";
|
|
const finalThumbnail =
|
|
typeof videoData.thumbnail === "string" ? videoData.thumbnail.trim() : "";
|
|
const finalDescription =
|
|
typeof videoData.description === "string"
|
|
? videoData.description.trim()
|
|
: "";
|
|
const finalTitle =
|
|
typeof videoData.title === "string" ? videoData.title.trim() : "";
|
|
const providedMimeType =
|
|
typeof videoData.mimeType === "string"
|
|
? videoData.mimeType.trim().toLowerCase()
|
|
: "";
|
|
|
|
const createdAt = timestamp;
|
|
|
|
const seriesIdentifierCandidates = [
|
|
videoData.videoRootId,
|
|
videoData.seriesId,
|
|
videoData.seriesIdentifier,
|
|
];
|
|
let seriesIdentifier = "";
|
|
for (const candidate of seriesIdentifierCandidates) {
|
|
const normalized = typeof candidate === "string" ? candidate.trim() : "";
|
|
if (normalized) {
|
|
seriesIdentifier = normalized;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!seriesIdentifier) {
|
|
seriesIdentifier = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
|
|
const videoRootId = seriesIdentifier;
|
|
const dTagValue = seriesIdentifier;
|
|
|
|
const finalEnableComments =
|
|
videoData.enableComments === false ? false : true;
|
|
const finalIsNsfw = videoData.isNsfw === true;
|
|
const finalIsForKids =
|
|
videoData.isForKids === true && !finalIsNsfw;
|
|
const wantPrivate = videoData.isPrivate === true;
|
|
const finalWs =
|
|
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
|
|
const finalXs =
|
|
typeof videoData.xs === "string" ? videoData.xs.trim() : "";
|
|
const infoHashCandidates = [videoData.infoHash];
|
|
let infoHash = "";
|
|
for (const candidate of infoHashCandidates) {
|
|
const normalized = infoHashFromMagnet(candidate);
|
|
if (normalized) {
|
|
infoHash = normalized;
|
|
break;
|
|
}
|
|
}
|
|
if (!infoHash && finalMagnet) {
|
|
infoHash = infoHashFromMagnet(finalMagnet) || "";
|
|
}
|
|
const fileSha256 = normalizeHexHash(videoData.fileSha256);
|
|
const originalFileSha256 = normalizeHexHash(videoData.originalFileSha256);
|
|
const storagePointer = resolveStoragePointerValue({
|
|
storagePointer: videoData.storagePointer,
|
|
url: finalUrl,
|
|
infoHash,
|
|
fallbackId: videoRootId,
|
|
provider: videoData.storageProvider,
|
|
});
|
|
|
|
const contentObject = {
|
|
version: 3,
|
|
title: finalTitle,
|
|
url: finalUrl,
|
|
magnet: finalMagnet,
|
|
thumbnail: finalThumbnail,
|
|
description: finalDescription,
|
|
mode: videoData.mode || "live",
|
|
videoRootId,
|
|
deleted: false,
|
|
isPrivate: videoData.isPrivate ?? false,
|
|
isNsfw: finalIsNsfw,
|
|
isForKids: finalIsForKids,
|
|
enableComments: finalEnableComments,
|
|
};
|
|
|
|
if (infoHash) {
|
|
contentObject.infoHash = infoHash;
|
|
}
|
|
|
|
if (fileSha256) {
|
|
contentObject.fileSha256 = fileSha256;
|
|
}
|
|
|
|
if (originalFileSha256) {
|
|
contentObject.originalFileSha256 = originalFileSha256;
|
|
}
|
|
|
|
if (finalWs) {
|
|
contentObject.ws = finalWs;
|
|
}
|
|
|
|
if (finalXs) {
|
|
contentObject.xs = finalXs;
|
|
}
|
|
|
|
const nip71Tags = buildNip71MetadataTags(
|
|
nip71Metadata && typeof nip71Metadata === "object" ? nip71Metadata : null
|
|
);
|
|
|
|
const additionalTags = storagePointer
|
|
? [["s", storagePointer], ...nip71Tags]
|
|
: nip71Tags;
|
|
|
|
const event = buildVideoPostEvent({
|
|
pubkey: normalizedPubkey,
|
|
created_at: createdAt,
|
|
dTagValue,
|
|
content: contentObject,
|
|
additionalTags,
|
|
});
|
|
|
|
return {
|
|
event,
|
|
videoData,
|
|
nip71Metadata,
|
|
finalUrl,
|
|
finalMagnet,
|
|
finalThumbnail,
|
|
finalDescription,
|
|
finalTitle,
|
|
mimeType: providedMimeType,
|
|
fileSha256,
|
|
originalFileSha256,
|
|
videoRootId,
|
|
dTagValue,
|
|
createdAt,
|
|
contentObject,
|
|
wantPrivate,
|
|
normalizedPubkey,
|
|
videoPayload
|
|
};
|
|
}
|
|
|
|
export async function prepareVideoMirrorOptions(params) {
|
|
const {
|
|
videoData,
|
|
videoPayload,
|
|
finalUrl,
|
|
finalMagnet,
|
|
finalThumbnail,
|
|
finalDescription,
|
|
finalTitle,
|
|
mimeType: providedMimeType,
|
|
fileSha256: initialFileSha256,
|
|
originalFileSha256: initialOriginalFileSha256,
|
|
pubkey,
|
|
createdAt,
|
|
isPrivate
|
|
} = params;
|
|
|
|
const inferredMimeType = inferMimeTypeFromUrl(finalUrl);
|
|
const mimeTypeSource =
|
|
providedMimeType ||
|
|
inferredMimeType ||
|
|
"application/octet-stream";
|
|
const mimeType = mimeTypeSource.toLowerCase();
|
|
|
|
const fileHashCandidates = [
|
|
initialFileSha256,
|
|
videoData.fileSha256,
|
|
videoData.uploadedFileSha256,
|
|
videoPayload?.legacyFormData?.fileSha256,
|
|
videoPayload?.fileSha256,
|
|
];
|
|
let fileSha256 = "";
|
|
for (const candidate of fileHashCandidates) {
|
|
const normalized = normalizeHexHash(candidate);
|
|
if (normalized) {
|
|
fileSha256 = normalized;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const originalHashCandidates = [
|
|
initialOriginalFileSha256,
|
|
videoData.originalFileSha256,
|
|
videoPayload?.legacyFormData?.originalFileSha256,
|
|
videoPayload?.originalFileSha256,
|
|
];
|
|
let originalFileSha256 = "";
|
|
for (const candidate of originalHashCandidates) {
|
|
const normalized = normalizeHexHash(candidate);
|
|
if (normalized) {
|
|
originalFileSha256 = normalized;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const uploadedFile =
|
|
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);
|
|
}
|
|
|
|
if (!originalFileSha256 && originalFile) {
|
|
originalFileSha256 = await computeSha256HexFromValue(originalFile);
|
|
}
|
|
|
|
if (!originalFileSha256 && fileSha256) {
|
|
originalFileSha256 = fileSha256;
|
|
}
|
|
|
|
const mirrorOptions = {
|
|
url: finalUrl,
|
|
magnet: finalMagnet,
|
|
thumbnail: finalThumbnail,
|
|
description: finalDescription,
|
|
title: finalTitle,
|
|
mimeType,
|
|
isPrivate: isPrivate,
|
|
actorPubkey: pubkey,
|
|
created_at: createdAt,
|
|
};
|
|
|
|
if (fileSha256) {
|
|
mirrorOptions.fileSha256 = fileSha256;
|
|
}
|
|
|
|
if (originalFileSha256) {
|
|
mirrorOptions.originalFileSha256 = originalFileSha256;
|
|
}
|
|
|
|
return mirrorOptions;
|
|
}
|
|
|
|
export function prepareVideoEditPayload(params) {
|
|
const {
|
|
baseEvent,
|
|
originalEventStub,
|
|
updatedData,
|
|
userPubkey,
|
|
resolveEventDTag // Injected dependency to resolve D tag
|
|
} = params;
|
|
|
|
const userPubkeyLower = userPubkey.toLowerCase();
|
|
|
|
const nip71Metadata =
|
|
updatedData && typeof updatedData === "object" ? updatedData.nip71 : null;
|
|
|
|
const oldMagnet =
|
|
typeof baseEvent.rawMagnet === "string" && baseEvent.rawMagnet.trim()
|
|
? baseEvent.rawMagnet.trim()
|
|
: typeof baseEvent.magnet === "string"
|
|
? baseEvent.magnet.trim()
|
|
: "";
|
|
const oldUrl = baseEvent.url || "";
|
|
|
|
// Determine if the updated note should be private
|
|
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
|
const wantNsfw = updatedData.isNsfw ?? baseEvent.isNsfw ?? false;
|
|
const wantForKids = updatedData.isForKids ?? baseEvent.isForKids ?? false;
|
|
const finalIsNsfw = wantNsfw === true;
|
|
const finalIsForKids = finalIsNsfw ? false : wantForKids === true;
|
|
|
|
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
|
|
const magnetEdited = updatedData.magnetEdited === true;
|
|
const newMagnetValue =
|
|
typeof updatedData.magnet === "string" ? updatedData.magnet.trim() : "";
|
|
const finalMagnet = magnetEdited ? newMagnetValue : oldMagnet;
|
|
|
|
const urlEdited = updatedData.urlEdited === true;
|
|
const newUrlValue =
|
|
typeof updatedData.url === "string" ? updatedData.url.trim() : "";
|
|
const finalUrl = urlEdited ? newUrlValue : oldUrl;
|
|
|
|
const wsEdited = updatedData.wsEdited === true;
|
|
const xsEdited = updatedData.xsEdited === true;
|
|
const newWsValue =
|
|
typeof updatedData.ws === "string" ? updatedData.ws.trim() : "";
|
|
const newXsValue =
|
|
typeof updatedData.xs === "string" ? updatedData.xs.trim() : "";
|
|
const baseWs =
|
|
typeof baseEvent.ws === "string" ? baseEvent.ws.trim() : "";
|
|
const baseXs =
|
|
typeof baseEvent.xs === "string" ? baseEvent.xs.trim() : "";
|
|
const finalWs = wsEdited ? newWsValue : baseWs;
|
|
const finalXs = xsEdited ? newXsValue : baseXs;
|
|
const finalEnableComments =
|
|
typeof updatedData.enableComments === "boolean"
|
|
? updatedData.enableComments
|
|
: baseEvent.enableComments === false
|
|
? false
|
|
: true;
|
|
|
|
const updatedInfoHash = infoHashFromMagnet(updatedData?.infoHash || "");
|
|
let infoHash =
|
|
updatedInfoHash ||
|
|
infoHashFromMagnet(finalMagnet) ||
|
|
infoHashFromMagnet(baseEvent.infoHash) ||
|
|
"";
|
|
const fileSha256 =
|
|
normalizeHexHash(updatedData?.fileSha256) ||
|
|
normalizeHexHash(baseEvent.fileSha256);
|
|
const originalFileSha256 =
|
|
normalizeHexHash(updatedData?.originalFileSha256) ||
|
|
normalizeHexHash(baseEvent.originalFileSha256);
|
|
|
|
// Use the existing videoRootId (or fall back to the base event's ID)
|
|
const oldRootId = baseEvent.videoRootId || baseEvent.id;
|
|
const storagePointer = resolveStoragePointerValue({
|
|
storagePointer:
|
|
updatedData?.storagePointer ||
|
|
baseEvent.storagePointer ||
|
|
getStoragePointerFromTags(baseEvent.tags),
|
|
url: finalUrl,
|
|
infoHash,
|
|
fallbackId: oldRootId,
|
|
provider: updatedData?.storageProvider || baseEvent.storageProvider,
|
|
});
|
|
|
|
const preservedDTag = resolveEventDTag(baseEvent, originalEventStub);
|
|
const fallbackDTag =
|
|
(typeof baseEvent.id === "string" && baseEvent.id.trim()) ||
|
|
(typeof originalEventStub?.id === "string" && originalEventStub.id.trim()) ||
|
|
"";
|
|
const finalDTagValue =
|
|
(typeof preservedDTag === "string" && preservedDTag.trim()) || fallbackDTag;
|
|
|
|
if (!finalDTagValue) {
|
|
throw new Error("Unable to determine a stable d tag for this edit.");
|
|
}
|
|
|
|
// Build the updated content object
|
|
const contentObject = {
|
|
videoRootId: oldRootId,
|
|
version: updatedData.version ?? baseEvent.version ?? 2,
|
|
deleted: false,
|
|
isPrivate: wantPrivate,
|
|
isNsfw: finalIsNsfw,
|
|
isForKids: finalIsForKids,
|
|
title: updatedData.title ?? baseEvent.title,
|
|
url: finalUrl,
|
|
magnet: finalMagnet,
|
|
thumbnail: updatedData.thumbnail ?? baseEvent.thumbnail,
|
|
description: updatedData.description ?? baseEvent.description,
|
|
mode: updatedData.mode ?? baseEvent.mode ?? "live",
|
|
enableComments: finalEnableComments,
|
|
};
|
|
|
|
if (infoHash) {
|
|
contentObject.infoHash = infoHash;
|
|
}
|
|
|
|
if (fileSha256) {
|
|
contentObject.fileSha256 = fileSha256;
|
|
}
|
|
|
|
if (originalFileSha256) {
|
|
contentObject.originalFileSha256 = originalFileSha256;
|
|
}
|
|
|
|
if (finalWs) {
|
|
contentObject.ws = finalWs;
|
|
}
|
|
|
|
if (finalXs) {
|
|
contentObject.xs = finalXs;
|
|
}
|
|
|
|
let metadataForTags =
|
|
nip71Metadata && typeof nip71Metadata === "object" ? nip71Metadata : null;
|
|
if (!metadataForTags) {
|
|
if (baseEvent?.nip71 && typeof baseEvent.nip71 === "object") {
|
|
metadataForTags = baseEvent.nip71;
|
|
} else {
|
|
const extracted = extractNip71MetadataFromTags(baseEvent);
|
|
if (extracted?.metadata) {
|
|
metadataForTags = extracted.metadata;
|
|
}
|
|
}
|
|
}
|
|
|
|
const nip71Tags = buildNip71MetadataTags(metadataForTags);
|
|
|
|
const additionalTags = storagePointer
|
|
? [["s", storagePointer], ...nip71Tags]
|
|
: nip71Tags;
|
|
|
|
const createdAt = Math.floor(Date.now() / 1000);
|
|
|
|
const event = buildVideoPostEvent({
|
|
pubkey: userPubkeyLower,
|
|
created_at: createdAt,
|
|
dTagValue: finalDTagValue,
|
|
content: contentObject,
|
|
additionalTags,
|
|
});
|
|
|
|
return {
|
|
event,
|
|
contentObject,
|
|
nip71Metadata,
|
|
wantPrivate,
|
|
videoRootId: oldRootId,
|
|
dTagValue: finalDTagValue,
|
|
normalizedPubkey: userPubkeyLower,
|
|
userPubkeyLower,
|
|
createdAt,
|
|
finalUrl,
|
|
finalMagnet,
|
|
finalThumbnail: contentObject.thumbnail,
|
|
finalDescription: contentObject.description,
|
|
finalTitle: contentObject.title,
|
|
mimeType: updatedData.mimeType || "",
|
|
fileSha256,
|
|
originalFileSha256,
|
|
videoData: updatedData,
|
|
videoPayload: updatedData,
|
|
};
|
|
}
|