diff --git a/js/channelProfile.js b/js/channelProfile.js index 21309171..60d5df9b 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -1,15 +1,13 @@ // js/channelProfile.js -import { nostrClient } from "./nostr.js"; +import { + nostrClient, + convertEventToVideo as sharedConvertEventToVideo, +} from "./nostr.js"; import { app } from "./app.js"; import { subscriptions } from "./subscriptions.js"; // <-- NEW import import { initialBlacklist, initialWhitelist } from "./lists.js"; import { isWhitelistEnabled } from "./config.js"; -import { - deriveTitleFromEvent, - parseVideoEventPayload, - findLegacyMagnetInEvent, -} from "./videoEventUtils.js"; /** * Initialize the channel profile view. @@ -204,7 +202,7 @@ async function loadUserVideos(pubkey) { // 3) Convert to "video" objects let videos = []; for (const evt of events) { - const vid = localConvertEventToVideo(evt); + const vid = sharedConvertEventToVideo(evt); if (!vid.invalid && !vid.deleted) { videos.push(vid); } @@ -479,84 +477,6 @@ function dedupeToNewestByRoot(videos) { return Array.from(map.values()); } -/** - * Convert raw event => "video" object. - */ -function localConvertEventToVideo(event) { - const { - parsedContent, - parseError, - title, - url, - magnet, - infoHash, - version, - } = parseVideoEventPayload(event); - - const trimmedUrl = typeof url === "string" ? url.trim() : ""; - const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const legacyMagnet = findLegacyMagnetInEvent(event); - const trimmedLegacyMagnet = - typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; - const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = - trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; - const parsedVersion = - typeof version === "number" - ? version - : typeof version === "string" - ? Number.parseInt(version, 10) - : Number.NaN; - const numericVersion = Number.isFinite(parsedVersion) - ? parsedVersion - : 0; - - const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); - if (!hasPlayableSource) { - return { id: event.id, invalid: true, reason: "missing playable source" }; - } - - const derivedTitle = deriveTitleFromEvent({ - parsedContent, - tags: event.tags, - primaryTitle: title, - }); - - let resolvedTitle = derivedTitle; - if (!resolvedTitle && numericVersion < 2 && playbackMagnet) { - resolvedTitle = trimmedInfoHash - ? `Legacy Video ${trimmedInfoHash.slice(0, 8)}` - : "Legacy BitTorrent Video"; - } - - if (!resolvedTitle) { - const reason = parseError - ? "missing title (json parse error)" - : "missing title"; - return { id: event.id, invalid: true, reason }; - } - - return { - id: event.id, - videoRootId: parsedContent.videoRootId || event.id, - version: numericVersion, - isPrivate: parsedContent.isPrivate ?? false, - title: resolvedTitle, - url: trimmedUrl, - magnet: playbackMagnet, - rawMagnet: trimmedMagnet || trimmedLegacyMagnet, - infoHash: trimmedInfoHash, - thumbnail: parsedContent.thumbnail ?? "", - description: parsedContent.description ?? "", - mode: parsedContent.mode ?? "live", - deleted: parsedContent.deleted === true, - pubkey: event.pubkey, - created_at: event.created_at, - tags: event.tags, - invalid: false, - }; -} - /** * Basic escaping to avoid XSS. */ diff --git a/js/nostr.js b/js/nostr.js index e043cff3..0c378315 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -1,12 +1,9 @@ // js/nostr.js import { isDevMode } from "./config.js"; +import { ACCEPT_LEGACY_V1 } from "./constants.js"; import { accessControl } from "./accessControl.js"; -import { - deriveTitleFromEvent, - parseVideoEventPayload, - findLegacyMagnetInEvent, -} from "./videoEventUtils.js"; +import { deriveTitleFromEvent, magnetFromText } from "./videoEventUtils.js"; /** * The usual relays @@ -91,33 +88,80 @@ function inferMimeTypeFromUrl(url) { } /** - * Convert a raw Nostr event => your "video" object. - * Accepts legacy (<2) payloads when they expose a usable magnet/info hash. + * Convert a raw Nostr event => Bitvid's "video" object. + * Accepts legacy ( (typeof value === "string" ? value.trim() : ""); + const parseInteger = (value) => { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string") { + const parsed = Number.parseInt(value, 10); + if (Number.isFinite(parsed)) { + return parsed; + } + } + return 0; + }; - const trimmedUrl = typeof url === "string" ? url.trim() : ""; - const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const legacyMagnet = findLegacyMagnetInEvent(event); - const trimmedLegacyMagnet = - typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; - const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = - trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; + const rawContent = safeTrim(event.content); + let parsedContent = {}; + let parseError = null; + if (rawContent) { + try { + const parsed = JSON.parse(rawContent); + if (parsed && typeof parsed === "object") { + parsedContent = parsed; + } + } catch (err) { + parseError = err; + parsedContent = {}; + } + } - const numericVersion = Number.isFinite(version) ? version : 0; - const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); + const declaredTitle = safeTrim(parsedContent.title); + const declaredUrl = safeTrim(parsedContent.url); + const declaredMagnet = safeTrim(parsedContent.magnet); - if (!hasPlayableSource) { + const tags = Array.isArray(event.tags) ? event.tags : []; + + let recoveredMagnet = ""; + if (!declaredMagnet && ACCEPT_LEGACY_V1) { + recoveredMagnet = safeTrim(magnetFromText(rawContent)); + if (!recoveredMagnet) { + for (const tag of tags) { + if (!Array.isArray(tag) || tag.length < 2) { + continue; + } + const key = safeTrim(tag[0]).toLowerCase(); + if (key === "magnet" && typeof tag[1] === "string") { + const candidate = safeTrim(tag[1]); + if (candidate) { + recoveredMagnet = candidate; + break; + } + } + for (let i = 1; i < tag.length && !recoveredMagnet; i += 1) { + const candidate = safeTrim(magnetFromText(tag[i])); + if (candidate) { + recoveredMagnet = candidate; + break; + } + } + if (recoveredMagnet) { + break; + } + } + } + } + + const magnet = declaredMagnet || recoveredMagnet; + const url = declaredUrl; + + if (!url && !magnet) { return { id: event.id, invalid: true, @@ -125,16 +169,59 @@ function convertEventToVideo(event) { }; } + const resolveInfoHash = () => { + const declaredHash = safeTrim(parsedContent.infoHash); + if (declaredHash) { + return declaredHash.toLowerCase(); + } + const sourceMagnet = magnet || safeTrim(magnetFromText(rawContent)); + if (sourceMagnet) { + const match = sourceMagnet.match(/xt=urn:btih:([0-9a-z]+)/i); + if (match && match[1]) { + return match[1].toLowerCase(); + } + } + if (ACCEPT_LEGACY_V1) { + const hexMatch = rawContent.match(/\b[0-9a-f]{40}\b/i); + if (hexMatch && hexMatch[0]) { + return hexMatch[0].toLowerCase(); + } + for (const tag of tags) { + if (!Array.isArray(tag)) { + continue; + } + for (const value of tag) { + if (typeof value !== "string") { + continue; + } + const match = value.match(/\b[0-9a-f]{40}\b/i); + if (match && match[0]) { + return match[0].toLowerCase(); + } + } + } + } + return ""; + }; + + const infoHash = resolveInfoHash(); + const version = parseInteger(parsedContent.version); + const derivedTitle = deriveTitleFromEvent({ parsedContent, - tags: event.tags, - primaryTitle: title, + tags, + primaryTitle: declaredTitle, }); let resolvedTitle = derivedTitle; - if (!resolvedTitle && numericVersion < 2 && playbackMagnet) { - resolvedTitle = trimmedInfoHash - ? `Legacy Video ${trimmedInfoHash.slice(0, 8)}` + if ( + !resolvedTitle && + ACCEPT_LEGACY_V1 && + version < 2 && + (magnet || infoHash) + ) { + resolvedTitle = infoHash + ? `Legacy Video ${infoHash.slice(0, 8)}` : "Legacy BitTorrent Video"; } @@ -145,23 +232,25 @@ function convertEventToVideo(event) { return { id: event.id, invalid: true, reason }; } + const normalizeOptional = (value) => safeTrim(value) || ""; + return { id: event.id, - videoRootId: parsedContent.videoRootId || event.id, - version: numericVersion, + videoRootId: safeTrim(parsedContent.videoRootId) || event.id, + version, isPrivate: parsedContent.isPrivate ?? false, title: resolvedTitle, - url: trimmedUrl, - magnet: playbackMagnet, - rawMagnet: trimmedMagnet || trimmedLegacyMagnet, - infoHash: trimmedInfoHash, - thumbnail: parsedContent.thumbnail ?? "", - description: parsedContent.description ?? "", - mode: parsedContent.mode ?? "live", + url, + magnet, + rawMagnet: declaredMagnet || recoveredMagnet, + infoHash, + thumbnail: normalizeOptional(parsedContent.thumbnail), + description: normalizeOptional(parsedContent.description), + mode: safeTrim(parsedContent.mode) || "live", deleted: parsedContent.deleted === true, pubkey: event.pubkey, created_at: event.created_at, - tags: event.tags, + tags, invalid: false, }; } @@ -332,14 +421,14 @@ class NostrClient { const contentObject = { videoRootId, version: 2, // forcibly set version=2 - deleted: false, - isPrivate: videoData.isPrivate ?? false, title: finalTitle, url: finalUrl, magnet: finalMagnet, thumbnail: finalThumbnail, description: finalDescription, mode: videoData.mode || "live", + deleted: false, + isPrivate: videoData.isPrivate ?? false, }; const event = { diff --git a/js/subscriptions.js b/js/subscriptions.js index 4b80ebe6..fff42045 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1,10 +1,8 @@ // js/subscriptions.js -import { nostrClient } from "./nostr.js"; import { - deriveTitleFromEvent, - parseVideoEventPayload, - findLegacyMagnetInEvent, -} from "./videoEventUtils.js"; + nostrClient, + convertEventToVideo as sharedConvertEventToVideo, +} from "./nostr.js"; /** * Manages the user's subscription list (kind=30002) *privately*, @@ -521,86 +519,7 @@ class SubscriptionsManager { } convertEventToVideo(evt) { - const { - parsedContent, - parseError, - title, - url, - magnet, - infoHash, - version, - } = parseVideoEventPayload(evt); - - const trimmedUrl = typeof url === "string" ? url.trim() : ""; - const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const legacyMagnet = findLegacyMagnetInEvent(evt); - const trimmedLegacyMagnet = - typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; - const trimmedInfoHash = - typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = - trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; - const parsedVersion = - typeof version === "number" - ? version - : typeof version === "string" - ? Number.parseInt(version, 10) - : Number.NaN; - const numericVersion = Number.isFinite(parsedVersion) - ? parsedVersion - : 0; - - const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); - if (!hasPlayableSource) { - return { - id: evt.id, - invalid: true, - reason: "missing playable source", - }; - } - - const derivedTitle = deriveTitleFromEvent({ - parsedContent, - tags: evt.tags, - primaryTitle: title, - }); - - let resolvedTitle = derivedTitle; - if (!resolvedTitle && numericVersion < 2 && playbackMagnet) { - resolvedTitle = trimmedInfoHash - ? `Legacy Video ${trimmedInfoHash.slice(0, 8)}` - : "Legacy BitTorrent Video"; - } - - if (!resolvedTitle) { - return { - id: evt.id, - invalid: true, - reason: parseError - ? "missing title (json parse error)" - : "missing title", - }; - } - - return { - id: evt.id, - pubkey: evt.pubkey, - created_at: evt.created_at, - videoRootId: parsedContent.videoRootId || evt.id, - version: numericVersion, - deleted: parsedContent.deleted === true, - isPrivate: parsedContent.isPrivate ?? false, - title: resolvedTitle, - url: trimmedUrl, - magnet: playbackMagnet, - rawMagnet: trimmedMagnet || trimmedLegacyMagnet, - infoHash: trimmedInfoHash, - thumbnail: parsedContent.thumbnail ?? "", - description: parsedContent.description ?? "", - mode: parsedContent.mode ?? "live", - tags: evt.tags || [], - invalid: false, - }; + return sharedConvertEventToVideo(evt); } dedupeToNewestByRoot(videos) { diff --git a/js/videoEventUtils.js b/js/videoEventUtils.js index bfca5107..acf48f63 100644 --- a/js/videoEventUtils.js +++ b/js/videoEventUtils.js @@ -255,7 +255,7 @@ export function parseVideoEventPayload(event = {}) { }; } -function magnetFromText(value) { +export function magnetFromText(value) { if (typeof value !== "string") { return ""; }