Extract video note payload normalization

This commit is contained in:
thePR0M3TH3AN committed 2025-10-26 18:21:32 -04:00
1 parent bf70f7c6e6
commit 700056b5fc
5 files changed
+651 -392

No files matched your search

+9 -352
View File
@@ -13,7 +13,6 @@ import {
} from "./config.js";
import { accessControl } from "./accessControl.js";
import { safeDecodeMagnet } from "./magnetUtils.js";
import { extractMagnetHints, normalizeAndAugmentMagnet } from "./magnet.js";
import { deriveTorrentPlaybackConfig } from "./playbackUtils.js";
import {
URL_FIRST_ENABLED,
@@ -46,6 +45,10 @@ import watchHistoryService from "./watchHistoryService.js";
import r2Service from "./services/r2Service.js";
import PlaybackService from "./services/playbackService.js";
import AuthService from "./services/authService.js";
import {
getVideoNoteErrorMessage,
normalizeVideoNotePayload,
} from "./services/videoNotePayload.js";
import getAuthProvider, {
providers as authProviders,
} from "./services/authProviders/index.js";
@@ -4562,362 +4565,16 @@ class Application {
return false;
}
const normalizeString = (value) => {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number") {
return Number.isFinite(value) ? String(value).trim() : "";
}
return String(value ?? "").trim();
};
const parseNumberOrNull = (value) => {
if (value === null || value === undefined || value === "") {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
const parseUnixSeconds = (value) => {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value === "number" && Number.isFinite(value)) {
const normalized = value > 1e12 ? value / 1000 : value;
return Math.floor(normalized);
}
const trimmed = normalizeString(value);
if (!trimmed) {
return null;
}
const numeric = Number(trimmed);
if (Number.isFinite(numeric)) {
const normalized = numeric > 1e12 ? numeric / 1000 : numeric;
return Math.floor(normalized);
}
const timestamp = Date.parse(trimmed);
if (!Number.isNaN(timestamp)) {
return Math.floor(timestamp / 1000);
}
return null;
};
const normalizeStringArray = (input) => {
if (!Array.isArray(input)) {
return [];
}
return input
.map((entry) => normalizeString(entry))
.filter((entry) => Boolean(entry));
};
const normalizeBooleanFlag = (value, defaultValue = false) => {
if (value === true) {
return true;
}
if (value === false) {
return false;
}
return Boolean(defaultValue);
};
const normalizeImetaVariant = (variant) => {
if (!variant || typeof variant !== "object") {
return null;
}
const normalized = {};
const m = normalizeString(variant.m);
if (m) normalized.m = m;
const dim = normalizeString(variant.dim);
if (dim) normalized.dim = dim;
const url = normalizeString(variant.url);
if (url) normalized.url = url;
const x = normalizeString(variant.x);
if (x) normalized.x = x;
const image = normalizeStringArray(variant.image);
if (image.length) normalized.image = image;
const fallback = normalizeStringArray(variant.fallback);
if (fallback.length) normalized.fallback = fallback;
const service = normalizeStringArray(variant.service);
if (service.length) normalized.service = service;
if (variant.autoGenerated === true) {
normalized.autoGenerated = true;
}
return Object.keys(normalized).length ? normalized : null;
};
const normalizeSegments = (segments) => {
if (!Array.isArray(segments)) {
return [];
}
return segments
.map((segment) => {
if (!segment || typeof segment !== "object") {
return null;
}
const startRaw = segment.start;
const endRaw = segment.end;
const start = parseNumberOrNull(startRaw);
const end = parseNumberOrNull(endRaw);
const normalizedSegment = {};
if (start !== null) {
normalizedSegment.start = start;
} else {
const startString = normalizeString(startRaw);
if (startString) {
normalizedSegment.start = startString;
}
}
if (end !== null) {
normalizedSegment.end = end;
} else {
const endString = normalizeString(endRaw);
if (endString) {
normalizedSegment.end = endString;
}
}
const title = normalizeString(segment.title);
if (title) {
normalizedSegment.title = title;
}
const thumbnail = normalizeString(segment.thumbnail);
if (thumbnail) {
normalizedSegment.thumbnail = thumbnail;
}
return Object.keys(normalizedSegment).length ? normalizedSegment : null;
})
.filter(Boolean);
};
const normalizeTextTracks = (tracks) => {
if (!Array.isArray(tracks)) {
return [];
}
return tracks
.map((track) => {
if (!track || typeof track !== "object") {
return null;
}
const normalizedTrack = {};
const url = normalizeString(track.url);
if (url) normalizedTrack.url = url;
const type = normalizeString(track.type);
if (type) normalizedTrack.type = type;
const language = normalizeString(track.language);
if (language) normalizedTrack.language = language;
return Object.keys(normalizedTrack).length ? normalizedTrack : null;
})
.filter(Boolean);
};
const normalizeParticipants = (participants) => {
if (!Array.isArray(participants)) {
return [];
}
return participants
.map((participant) => {
if (!participant || typeof participant !== "object") {
return null;
}
const normalizedParticipant = {};
const pubkey = normalizeString(participant.pubkey);
if (pubkey) normalizedParticipant.pubkey = pubkey;
const relay = normalizeString(participant.relay);
if (relay) normalizedParticipant.relay = relay;
return Object.keys(normalizedParticipant).length
? normalizedParticipant
: null;
})
.filter(Boolean);
};
const normalizeNip71Metadata = (rawMetadata) => {
if (!rawMetadata || typeof rawMetadata !== "object") {
return null;
}
const normalized = {};
if (rawMetadata.kind !== undefined) {
const kindNumber = parseNumberOrNull(rawMetadata.kind);
if (kindNumber !== null) {
normalized.kind = kindNumber;
} else {
const kindString = normalizeString(rawMetadata.kind);
if (kindString) {
normalized.kind = kindString;
}
}
}
const summary = normalizeString(rawMetadata.summary);
if (summary) {
normalized.summary = summary;
}
const publishedAt = parseUnixSeconds(rawMetadata.publishedAt);
if (publishedAt !== null) {
normalized.publishedAt = publishedAt;
}
const alt = normalizeString(rawMetadata.alt);
if (alt) {
normalized.alt = alt;
}
const duration = parseNumberOrNull(rawMetadata.duration);
if (duration !== null) {
normalized.duration = duration;
}
const contentWarning = normalizeString(rawMetadata.contentWarning);
if (contentWarning) {
normalized.contentWarning = contentWarning;
}
const imeta = Array.isArray(rawMetadata.imeta)
? rawMetadata.imeta
.map((variant) => normalizeImetaVariant(variant))
.filter(Boolean)
: [];
if (imeta.length) {
normalized.imeta = imeta;
}
const textTracks = normalizeTextTracks(rawMetadata.textTracks);
if (textTracks.length) {
normalized.textTracks = textTracks;
}
const segments = normalizeSegments(rawMetadata.segments);
if (segments.length) {
normalized.segments = segments;
}
const hashtags = normalizeStringArray(rawMetadata.hashtags);
if (hashtags.length) {
normalized.hashtags = hashtags;
}
const participants = normalizeParticipants(rawMetadata.participants);
if (participants.length) {
normalized.participants = participants;
}
const references = normalizeStringArray(rawMetadata.references);
if (references.length) {
normalized.references = references;
}
return Object.keys(normalized).length ? normalized : null;
};
const rawPayload = payload && typeof payload === "object" ? payload : {};
const legacyPayload =
rawPayload.legacyFormData &&
typeof rawPayload.legacyFormData === "object"
? rawPayload.legacyFormData
: rawPayload;
const rawNip71 =
(rawPayload.nip71 && typeof rawPayload.nip71 === "object"
? rawPayload.nip71
: null) ||
(legacyPayload.nip71 && typeof legacyPayload.nip71 === "object"
? legacyPayload.nip71
: null);
const title = normalizeString(legacyPayload?.title || "");
const url = normalizeString(legacyPayload?.url || "");
const magnet = normalizeString(legacyPayload?.magnet || "");
const thumbnail = normalizeString(legacyPayload?.thumbnail || "");
const description = normalizeString(legacyPayload?.description || "");
const ws = normalizeString(legacyPayload?.ws || "");
const xs = normalizeString(legacyPayload?.xs || "");
const rawMode = normalizeString(legacyPayload?.mode || "");
const normalizedMode =
rawMode && rawMode.toLowerCase() === "dev" ? "dev" : "live";
const enableComments = normalizeBooleanFlag(
legacyPayload?.enableComments,
true
);
const rawIsNsfw = normalizeBooleanFlag(legacyPayload?.isNsfw, false);
const rawIsForKids = normalizeBooleanFlag(legacyPayload?.isForKids, false);
const isNsfw = rawIsNsfw;
const isForKids = rawIsNsfw ? false : rawIsForKids;
const legacyFormData = {
version: 3,
title,
url,
magnet,
thumbnail,
description,
mode: normalizedMode,
enableComments,
isNsfw,
isForKids,
};
const normalizedNip71 = normalizeNip71Metadata(rawNip71);
const hasLegacySource = Boolean(legacyFormData.url || legacyFormData.magnet);
const hasImetaVariant = Boolean(
normalizedNip71?.imeta?.some((variant) =>
Boolean(
(variant.url && variant.url.length) ||
(variant.m && variant.m.length) ||
(variant.x && variant.x.length) ||
(variant.dim && variant.dim.length) ||
(Array.isArray(variant.image) && variant.image.length > 0) ||
(Array.isArray(variant.fallback) && variant.fallback.length > 0) ||
(Array.isArray(variant.service) && variant.service.length > 0)
)
)
const { payload: publishPayload, errors } = normalizeVideoNotePayload(
payload,
);
if (!legacyFormData.title) {
this.showError("Title is required.");
if (errors.length) {
const message = getVideoNoteErrorMessage(errors[0]);
this.showError(message);
return false;
}
if (!hasLegacySource && !hasImetaVariant) {
this.showError(
"Provide a hosted URL, magnet link, or an imeta variant before publishing."
);
return false;
}
if (legacyFormData.url && !/^https:\/\//i.test(legacyFormData.url)) {
this.showError("Hosted video URLs must use HTTPS.");
return false;
}
if (legacyFormData.magnet) {
const normalizedMagnet = normalizeAndAugmentMagnet(legacyFormData.magnet, {
ws,
xs,
});
legacyFormData.magnet = normalizedMagnet;
const hints = extractMagnetHints(normalizedMagnet);
legacyFormData.ws = hints.ws;
legacyFormData.xs = hints.xs;
} else {
legacyFormData.ws = "";
legacyFormData.xs = "";
}
const publishPayload = {
legacyFormData,
};
if (normalizedNip71) {
publishPayload.nip71 = normalizedNip71;
}
try {
await this.nostrService.publishVideoNote(publishPayload, this.pubkey);
} catch (err) {
+33 -24
View File
@@ -22,6 +22,11 @@ import {
} from "../storage/r2-s3.js";
import { truncateMiddle } from "../utils/formatters.js";
import { userLogger } from "../utils/logger.js";
import {
getVideoNoteErrorMessage,
normalizeVideoNotePayload,
VIDEO_NOTE_ERROR_CODES,
} from "./videoNotePayload.js";
const STATUS_VARIANTS = new Set(["info", "success", "error", "warning"]);
@@ -654,9 +659,17 @@ class R2Service {
return false;
}
const title = String(metadata.title || "").trim();
const rawTitleCandidate =
metadata && typeof metadata === "object" ? metadata.title : metadata;
const title = typeof rawTitleCandidate === "string"
? rawTitleCandidate.trim()
: String(rawTitleCandidate ?? "").trim();
if (!title) {
this.setCloudflareUploadStatus("Title is required.", "error");
this.setCloudflareUploadStatus(
getVideoNoteErrorMessage(VIDEO_NOTE_ERROR_CODES.MISSING_TITLE),
"error",
);
return false;
}
@@ -668,18 +681,6 @@ class R2Service {
return false;
}
const description = String(metadata.description || "").trim();
const thumbnail = String(metadata.thumbnail || "").trim();
const magnet = String(metadata.magnet || "").trim();
const ws = String(metadata.ws || "").trim();
const xs = String(metadata.xs || "").trim();
const enableComments = metadata.enableComments !== false;
const isNsfw = metadata.isNsfw === true;
const isForKids = metadata.isForKids === true && !isNsfw;
metadata.isNsfw = isNsfw;
metadata.isForKids = isForKids;
const accountId = (this.cloudflareSettings?.accountId || "").trim();
const accessKeyId = (this.cloudflareSettings?.accessKeyId || "").trim();
const secretAccessKey = (this.cloudflareSettings?.secretAccessKey || "").trim();
@@ -766,17 +767,17 @@ class R2Service {
);
publishOutcome = false;
} else {
const payload = {
const rawVideoPayload = {
title,
url: publicUrl,
magnet,
thumbnail,
description,
ws,
xs,
enableComments,
isNsfw,
isForKids,
magnet: metadata?.magnet ?? "",
thumbnail: metadata?.thumbnail ?? "",
description: metadata?.description ?? "",
ws: metadata?.ws ?? "",
xs: metadata?.xs ?? "",
enableComments: metadata?.enableComments,
isNsfw: metadata?.isNsfw,
isForKids: metadata?.isForKids,
};
const mergedNip71 = this.buildNip71MetadataForUpload(metadata?.nip71, {
@@ -784,7 +785,15 @@ class R2Service {
file,
});
if (mergedNip71 && Object.keys(mergedNip71).length) {
payload.nip71 = mergedNip71;
rawVideoPayload.nip71 = mergedNip71;
}
const { payload, errors } = normalizeVideoNotePayload(rawVideoPayload);
if (errors.length) {
const message = getVideoNoteErrorMessage(errors[0]);
this.setCloudflareUploadStatus(message, "error");
return false;
}
const published = await publishVideoNote(payload, {
+379
View File
@@ -0,0 +1,379 @@
import { extractMagnetHints, normalizeAndAugmentMagnet } from "../magnet.js";
export const VIDEO_NOTE_ERROR_CODES = {
MISSING_TITLE: "missing_title",
MISSING_SOURCE: "missing_source",
INVALID_URL_PROTOCOL: "invalid_url_protocol",
};
export const VIDEO_NOTE_ERROR_MESSAGES = {
[VIDEO_NOTE_ERROR_CODES.MISSING_TITLE]: "Title is required.",
[VIDEO_NOTE_ERROR_CODES.MISSING_SOURCE]:
"Provide a hosted URL, magnet link, or an imeta variant before publishing.",
[VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL]:
"Hosted video URLs must use HTTPS.",
};
export function getVideoNoteErrorMessage(code) {
return VIDEO_NOTE_ERROR_MESSAGES[code] || "Unable to submit video. Please try again.";
}
function normalizeString(value) {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number") {
return Number.isFinite(value) ? String(value).trim() : "";
}
return String(value ?? "").trim();
}
function parseNumberOrNull(value) {
if (value === null || value === undefined || value === "") {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function parseUnixSeconds(value) {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value === "number" && Number.isFinite(value)) {
const normalized = value > 1e12 ? value / 1000 : value;
return Math.floor(normalized);
}
const trimmed = normalizeString(value);
if (!trimmed) {
return null;
}
const numeric = Number(trimmed);
if (Number.isFinite(numeric)) {
const normalized = numeric > 1e12 ? numeric / 1000 : numeric;
return Math.floor(normalized);
}
const timestamp = Date.parse(trimmed);
if (!Number.isNaN(timestamp)) {
return Math.floor(timestamp / 1000);
}
return null;
}
function normalizeStringArray(input) {
if (!Array.isArray(input)) {
return [];
}
return input
.map((entry) => normalizeString(entry))
.filter((entry) => Boolean(entry));
}
function normalizeBooleanFlag(value, defaultValue = false) {
if (value === true) {
return true;
}
if (value === false) {
return false;
}
return Boolean(defaultValue);
}
function normalizeImetaVariant(variant) {
if (!variant || typeof variant !== "object") {
return null;
}
const normalized = {};
const m = normalizeString(variant.m);
if (m) normalized.m = m;
const dim = normalizeString(variant.dim);
if (dim) normalized.dim = dim;
const url = normalizeString(variant.url);
if (url) normalized.url = url;
const x = normalizeString(variant.x);
if (x) normalized.x = x;
const image = normalizeStringArray(variant.image);
if (image.length) normalized.image = image;
const fallback = normalizeStringArray(variant.fallback);
if (fallback.length) normalized.fallback = fallback;
const service = normalizeStringArray(variant.service);
if (service.length) normalized.service = service;
if (variant.autoGenerated === true) {
normalized.autoGenerated = true;
}
return Object.keys(normalized).length ? normalized : null;
}
function normalizeSegments(segments) {
if (!Array.isArray(segments)) {
return [];
}
return segments
.map((segment) => {
if (!segment || typeof segment !== "object") {
return null;
}
const startRaw = segment.start;
const endRaw = segment.end;
const start = parseNumberOrNull(startRaw);
const end = parseNumberOrNull(endRaw);
const normalizedSegment = {};
if (start !== null) {
normalizedSegment.start = start;
} else {
const startString = normalizeString(startRaw);
if (startString) {
normalizedSegment.start = startString;
}
}
if (end !== null) {
normalizedSegment.end = end;
} else {
const endString = normalizeString(endRaw);
if (endString) {
normalizedSegment.end = endString;
}
}
const title = normalizeString(segment.title);
if (title) {
normalizedSegment.title = title;
}
const thumbnail = normalizeString(segment.thumbnail);
if (thumbnail) {
normalizedSegment.thumbnail = thumbnail;
}
return Object.keys(normalizedSegment).length ? normalizedSegment : null;
})
.filter(Boolean);
}
function normalizeTextTracks(tracks) {
if (!Array.isArray(tracks)) {
return [];
}
return tracks
.map((track) => {
if (!track || typeof track !== "object") {
return null;
}
const normalizedTrack = {};
const url = normalizeString(track.url);
if (url) normalizedTrack.url = url;
const type = normalizeString(track.type);
if (type) normalizedTrack.type = type;
const language = normalizeString(track.language);
if (language) normalizedTrack.language = language;
return Object.keys(normalizedTrack).length ? normalizedTrack : null;
})
.filter(Boolean);
}
function normalizeParticipants(participants) {
if (!Array.isArray(participants)) {
return [];
}
return participants
.map((participant) => {
if (!participant || typeof participant !== "object") {
return null;
}
const normalizedParticipant = {};
const pubkey = normalizeString(participant.pubkey);
if (pubkey) normalizedParticipant.pubkey = pubkey;
const relay = normalizeString(participant.relay);
if (relay) normalizedParticipant.relay = relay;
return Object.keys(normalizedParticipant).length
? normalizedParticipant
: null;
})
.filter(Boolean);
}
function normalizeNip71Metadata(rawMetadata) {
if (!rawMetadata || typeof rawMetadata !== "object") {
return null;
}
const normalized = {};
if (rawMetadata.kind !== undefined) {
const kindNumber = parseNumberOrNull(rawMetadata.kind);
if (kindNumber !== null) {
normalized.kind = kindNumber;
} else {
const kindString = normalizeString(rawMetadata.kind);
if (kindString) {
normalized.kind = kindString;
}
}
}
const summary = normalizeString(rawMetadata.summary);
if (summary) {
normalized.summary = summary;
}
const publishedAt = parseUnixSeconds(rawMetadata.publishedAt);
if (publishedAt !== null) {
normalized.publishedAt = publishedAt;
}
const alt = normalizeString(rawMetadata.alt);
if (alt) {
normalized.alt = alt;
}
const duration = parseNumberOrNull(rawMetadata.duration);
if (duration !== null) {
normalized.duration = duration;
}
const contentWarning = normalizeString(rawMetadata.contentWarning);
if (contentWarning) {
normalized.contentWarning = contentWarning;
}
const imeta = Array.isArray(rawMetadata.imeta)
? rawMetadata.imeta
.map((variant) => normalizeImetaVariant(variant))
.filter(Boolean)
: [];
if (imeta.length) {
normalized.imeta = imeta;
}
const textTracks = normalizeTextTracks(rawMetadata.textTracks);
if (textTracks.length) {
normalized.textTracks = textTracks;
}
const segments = normalizeSegments(rawMetadata.segments);
if (segments.length) {
normalized.segments = segments;
}
const hashtags = normalizeStringArray(rawMetadata.hashtags);
if (hashtags.length) {
normalized.hashtags = hashtags;
}
const participants = normalizeParticipants(rawMetadata.participants);
if (participants.length) {
normalized.participants = participants;
}
const references = normalizeStringArray(rawMetadata.references);
if (references.length) {
normalized.references = references;
}
return Object.keys(normalized).length ? normalized : null;
}
export function normalizeVideoNotePayload(input) {
const rawPayload = input && typeof input === "object" ? input : {};
const legacyPayload =
rawPayload.legacyFormData && typeof rawPayload.legacyFormData === "object"
? rawPayload.legacyFormData
: rawPayload;
const rawNip71 =
(rawPayload.nip71 && typeof rawPayload.nip71 === "object"
? rawPayload.nip71
: null) ||
(legacyPayload.nip71 && typeof legacyPayload.nip71 === "object"
? legacyPayload.nip71
: null);
const title = normalizeString(legacyPayload?.title || "");
const url = normalizeString(legacyPayload?.url || "");
const magnet = normalizeString(legacyPayload?.magnet || "");
const thumbnail = normalizeString(legacyPayload?.thumbnail || "");
const description = normalizeString(legacyPayload?.description || "");
const ws = normalizeString(legacyPayload?.ws || "");
const xs = normalizeString(legacyPayload?.xs || "");
const rawMode = normalizeString(legacyPayload?.mode || "");
const normalizedMode = rawMode && rawMode.toLowerCase() === "dev" ? "dev" : "live";
const enableComments = normalizeBooleanFlag(legacyPayload?.enableComments, true);
const rawIsNsfw = normalizeBooleanFlag(legacyPayload?.isNsfw, false);
const rawIsForKids = normalizeBooleanFlag(legacyPayload?.isForKids, false);
const isNsfw = rawIsNsfw;
const isForKids = rawIsNsfw ? false : rawIsForKids;
const isPrivate = normalizeBooleanFlag(legacyPayload?.isPrivate, false);
const legacyFormData = {
version: 3,
title,
url,
magnet,
thumbnail,
description,
mode: normalizedMode,
enableComments,
isNsfw,
isForKids,
};
if (isPrivate) {
legacyFormData.isPrivate = true;
}
const normalizedNip71 = normalizeNip71Metadata(rawNip71);
const hasLegacySource = Boolean(legacyFormData.url || legacyFormData.magnet);
const hasImetaVariant = Boolean(
normalizedNip71?.imeta?.some((variant) =>
Boolean(
(variant.url && variant.url.length) ||
(variant.m && variant.m.length) ||
(variant.x && variant.x.length) ||
(variant.dim && variant.dim.length) ||
(Array.isArray(variant.image) && variant.image.length > 0) ||
(Array.isArray(variant.fallback) && variant.fallback.length > 0) ||
(Array.isArray(variant.service) && variant.service.length > 0),
),
),
);
const errors = [];
if (!legacyFormData.title) {
errors.push(VIDEO_NOTE_ERROR_CODES.MISSING_TITLE);
}
if (!hasLegacySource && !hasImetaVariant) {
errors.push(VIDEO_NOTE_ERROR_CODES.MISSING_SOURCE);
}
if (legacyFormData.url && !/^https:\/\//i.test(legacyFormData.url)) {
errors.push(VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL);
}
if (legacyFormData.magnet) {
const normalizedMagnet = normalizeAndAugmentMagnet(legacyFormData.magnet, {
ws,
xs,
});
legacyFormData.magnet = normalizedMagnet;
const hints = extractMagnetHints(normalizedMagnet);
legacyFormData.ws = hints.ws || "";
legacyFormData.xs = hints.xs || "";
} else {
legacyFormData.ws = "";
legacyFormData.xs = "";
}
const publishPayload = {
legacyFormData,
};
if (normalizedNip71) {
publishPayload.nip71 = normalizedNip71;
}
return {
payload: publishPayload,
errors,
};
}
+40 -16
View File
@@ -3,6 +3,11 @@
import { createModalAccessibility } from "./modalAccessibility.js";
import { Nip71FormManager } from "./nip71FormManager.js";
import { userLogger } from "../../utils/logger.js";
import {
getVideoNoteErrorMessage,
normalizeVideoNotePayload,
VIDEO_NOTE_ERROR_CODES,
} from "../../services/videoNotePayload.js";
export class UploadModal {
constructor({
@@ -717,14 +722,15 @@ export class UploadModal {
isNsfw: this.readCheckboxValue(this.customFormInputs.isNsfw, false),
isForKids: this.readCheckboxValue(this.customFormInputs.isForKids, false)
});
const payload = {
title: this.customFormInputs.title?.value?.trim() || "",
url: this.customFormInputs.url?.value?.trim() || "",
magnet: this.customFormInputs.magnet?.value?.trim() || "",
ws: this.customFormInputs.ws?.value?.trim() || "",
xs: this.customFormInputs.xs?.value?.trim() || "",
thumbnail: this.customFormInputs.thumbnail?.value?.trim() || "",
description: this.customFormInputs.description?.value?.trim() || "",
const rawPayload = {
title: this.customFormInputs.title?.value ?? "",
url: this.customFormInputs.url?.value ?? "",
magnet: this.customFormInputs.magnet?.value ?? "",
ws: this.customFormInputs.ws?.value ?? "",
xs: this.customFormInputs.xs?.value ?? "",
thumbnail: this.customFormInputs.thumbnail?.value ?? "",
description: this.customFormInputs.description?.value ?? "",
enableComments: this.readCheckboxValue(
this.customFormInputs.enableComments,
true
@@ -733,7 +739,7 @@ export class UploadModal {
};
if (this.customFormInputs.isPrivate) {
payload.isPrivate = this.readCheckboxValue(
rawPayload.isPrivate = this.readCheckboxValue(
this.customFormInputs.isPrivate,
false
);
@@ -741,7 +747,15 @@ export class UploadModal {
const nip71Metadata = this.nip71FormManager.collectSection("custom");
if (nip71Metadata) {
payload.nip71 = nip71Metadata;
rawPayload.nip71 = nip71Metadata;
}
const { payload, errors } = normalizeVideoNotePayload(rawPayload);
if (errors.length) {
const message = getVideoNoteErrorMessage(errors[0]);
this.showError(message);
return;
}
this.emit("upload:submit", { payload });
@@ -1028,12 +1042,12 @@ export class UploadModal {
isForKids: this.readCheckboxValue(this.cloudflareIsForKidsInput, false)
});
const metadata = {
title: this.cloudflareTitleInput?.value?.trim() || "",
description: this.cloudflareDescriptionInput?.value?.trim() || "",
thumbnail: this.cloudflareThumbnailInput?.value?.trim() || "",
magnet: this.cloudflareMagnetInput?.value?.trim() || "",
ws: this.cloudflareWsInput?.value?.trim() || "",
xs: this.cloudflareXsInput?.value?.trim() || "",
title: this.cloudflareTitleInput?.value ?? "",
description: this.cloudflareDescriptionInput?.value ?? "",
thumbnail: this.cloudflareThumbnailInput?.value ?? "",
magnet: this.cloudflareMagnetInput?.value ?? "",
ws: this.cloudflareWsInput?.value ?? "",
xs: this.cloudflareXsInput?.value ?? "",
enableComments: this.readCheckboxValue(
this.cloudflareEnableCommentsInput,
true
@@ -1041,6 +1055,16 @@ export class UploadModal {
...audienceFlags
};
metadata.title = metadata.title?.trim() || "";
if (!metadata.title) {
this.setCloudflareUploadStatus(
getVideoNoteErrorMessage(VIDEO_NOTE_ERROR_CODES.MISSING_TITLE),
"error"
);
return;
}
const nip71Metadata = this.nip71FormManager.collectSection("cloudflare");
if (nip71Metadata) {
const imetaList = Array.isArray(nip71Metadata.imeta)
+190
View File
@@ -0,0 +1,190 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
getVideoNoteErrorMessage,
normalizeVideoNotePayload,
VIDEO_NOTE_ERROR_CODES,
} from "../js/services/videoNotePayload.js";
test("normalizes minimal payload with hosted URL", () => {
const { payload, errors } = normalizeVideoNotePayload({
title: " Example Title ",
url: " https://cdn.example.com/video.mp4 ",
});
assert.deepEqual(errors, []);
assert.ok(payload, "Expected payload result");
assert.equal(payload.legacyFormData.version, 3);
assert.equal(payload.legacyFormData.title, "Example Title");
assert.equal(
payload.legacyFormData.url,
"https://cdn.example.com/video.mp4",
);
assert.equal(payload.legacyFormData.magnet, "");
assert.equal(payload.legacyFormData.ws, "");
assert.equal(payload.legacyFormData.xs, "");
assert.equal(payload.legacyFormData.mode, "live");
assert.equal(payload.legacyFormData.enableComments, true);
assert.equal(payload.legacyFormData.isNsfw, false);
assert.equal(payload.legacyFormData.isForKids, false);
});
test("normalizes mode and boolean flags", () => {
const { payload, errors } = normalizeVideoNotePayload({
title: "Flag Demo",
url: "https://videos.example.com/watch.mp4",
mode: "DEV",
enableComments: false,
isNsfw: true,
isForKids: true,
isPrivate: true,
});
assert.deepEqual(errors, []);
assert.equal(payload.legacyFormData.mode, "dev");
assert.equal(payload.legacyFormData.enableComments, false);
assert.equal(payload.legacyFormData.isNsfw, true);
assert.equal(payload.legacyFormData.isForKids, false);
assert.equal(payload.legacyFormData.isPrivate, true);
});
test("augments magnets with ws/xs hints", () => {
const { payload, errors } = normalizeVideoNotePayload({
title: "Magnet Upload",
magnet: "magnet:?xt=urn:btih:abcdef0123456789abcdef0123456789abcdef01",
ws: "https://cdn.example.com/video.mp4",
xs: "https://cdn.example.com/video.torrent",
});
assert.deepEqual(errors, []);
const { magnet, ws, xs } = payload.legacyFormData;
assert.ok(magnet.startsWith("magnet:?xt=urn:btih:"));
assert.ok(
magnet.includes("ws=https%3A%2F%2Fcdn.example.com%2Fvideo.mp4") ||
magnet.includes("ws=https://cdn.example.com/video.mp4"),
"Normalized magnet should include ws hint",
);
assert.ok(
magnet.includes("xs=https://cdn.example.com/video.torrent") ||
magnet.includes("xs=https%3A%2F%2Fcdn.example.com%2Fvideo.torrent"),
"Normalized magnet should include xs hint",
);
assert.equal(ws, "https://cdn.example.com/video.mp4");
assert.equal(xs, "https://cdn.example.com/video.torrent");
});
test("normalizes nip71 metadata collections", () => {
const { payload, errors } = normalizeVideoNotePayload({
title: "NIP-71 Rich Metadata",
nip71: {
imeta: [
{
url: " https://cdn.example.com/hls/playlist.m3u8 ",
service: [" wss://relay.example.com "],
},
],
segments: [
{
start: " 00:30 ",
end: 90000,
title: " Intro ",
thumbnail: " https://cdn.example.com/thumb.jpg ",
},
{
start: "bad",
end: null,
title: "",
},
],
textTracks: [
{ url: " https://cdn.example.com/captions.vtt ", type: " text/vtt ", language: " en " },
null,
],
participants: [
{ pubkey: " npub123 ", relay: " wss://relay.example.com " },
{},
],
hashtags: [" bitvid ", ""],
references: [" nostr:note1abc "],
duration: "900",
publishedAt: "2023-01-01T00:00:00Z",
summary: " Summary copy ",
},
});
assert.deepEqual(errors, []);
const nip71 = payload.nip71;
assert.ok(nip71, "Expected nip71 payload");
assert.equal(nip71.summary, "Summary copy");
assert.equal(nip71.duration, 900);
assert.equal(nip71.publishedAt, 1672531200);
assert.deepEqual(nip71.hashtags, ["bitvid"]);
assert.deepEqual(nip71.references, ["nostr:note1abc"]);
assert.equal(nip71.segments.length, 2);
assert.deepEqual(nip71.segments[0], {
start: "00:30",
end: 90000,
title: "Intro",
thumbnail: "https://cdn.example.com/thumb.jpg",
});
assert.deepEqual(nip71.segments[1], { start: "bad" });
assert.deepEqual(nip71.textTracks, [
{
url: "https://cdn.example.com/captions.vtt",
type: "text/vtt",
language: "en",
},
]);
assert.deepEqual(nip71.participants, [
{
pubkey: "npub123",
relay: "wss://relay.example.com",
},
]);
assert.equal(nip71.imeta[0].url, "https://cdn.example.com/hls/playlist.m3u8");
assert.deepEqual(nip71.imeta[0].service, ["wss://relay.example.com"]);
});
test("reports validation errors for missing fields", () => {
const { errors } = normalizeVideoNotePayload({
title: " ",
url: "",
});
assert.deepEqual(errors, [
VIDEO_NOTE_ERROR_CODES.MISSING_TITLE,
VIDEO_NOTE_ERROR_CODES.MISSING_SOURCE,
]);
assert.equal(
getVideoNoteErrorMessage(errors[0]),
"Title is required.",
);
});
test("rejects insecure hosted URLs", () => {
const { errors } = normalizeVideoNotePayload({
title: "Sample",
url: "http://example.com/video.mp4",
});
assert.deepEqual(errors, [VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL]);
});
test("allows publishing with only imeta variants", () => {
const { payload, errors } = normalizeVideoNotePayload({
title: "Imeta Only",
nip71: {
imeta: [
{
url: "https://cdn.example.com/video.m3u8",
image: ["https://cdn.example.com/poster.jpg"],
},
],
},
});
assert.deepEqual(errors, []);
assert.ok(payload.nip71);
assert.equal(payload.legacyFormData.url, "");
});