mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
added v2 note restrictions to enable upgrade path from v2 onward.
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"liveServer.settings.root": "/src"
|
"liveServer.settings.root": "./src"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -67,7 +67,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="components/iframe_forms/iframe-application-form.html"
|
src="./components/iframe_forms/iframe-application-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -56,7 +56,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="/components/iframe_forms/iframe-bug-fix-form.html"
|
src="./components/iframe_forms/iframe-bug-fix-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="/components/iframe_forms/iframe-content-appeals-form.html"
|
src="./components/iframe_forms/iframe-content-appeals-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="/components/iframe_forms/iframe-request-form.html"
|
src="./components/iframe_forms/iframe-request-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -57,7 +57,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="/components/iframe_forms/iframe-feedback-form.html"
|
src="./components/iframe_forms/iframe-feedback-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -633,6 +633,10 @@
|
|||||||
|
|
||||||
<!-- Other Scripts -->
|
<!-- Other Scripts -->
|
||||||
<script src="js/libs/nostr.bundle.js"></script>
|
<script src="js/libs/nostr.bundle.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.8.3";
|
||||||
|
window.NostrTools = { nip19, SimplePool };
|
||||||
|
</script>
|
||||||
<script type="module" src="js/config.js"></script>
|
<script type="module" src="js/config.js"></script>
|
||||||
<script type="module" src="js/lists.js"></script>
|
<script type="module" src="js/lists.js"></script>
|
||||||
<script type="module" src="js/accessControl.js"></script>
|
<script type="module" src="js/accessControl.js"></script>
|
||||||
|
@@ -99,6 +99,13 @@ class bitvidApp {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
|
// Force update of any registered service workers to ensure latest code is used.
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||||
|
registrations.forEach((registration) => registration.update());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Initialize the video modal (components/video-modal.html)
|
// 1. Initialize the video modal (components/video-modal.html)
|
||||||
await this.initModal();
|
await this.initModal();
|
||||||
this.updateModalElements();
|
this.updateModalElements();
|
||||||
@@ -766,7 +773,7 @@ class bitvidApp {
|
|||||||
|
|
||||||
if (!videos || videos.length === 0) {
|
if (!videos || videos.length === 0) {
|
||||||
this.videoList.innerHTML = `
|
this.videoList.innerHTML = `
|
||||||
<p class="text-center text-gray-500">
|
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
|
||||||
No public videos available yet. Be the first to upload one!
|
No public videos available yet. Be the first to upload one!
|
||||||
</p>`;
|
</p>`;
|
||||||
return;
|
return;
|
||||||
@@ -1175,6 +1182,10 @@ class bitvidApp {
|
|||||||
|
|
||||||
// 8) Refresh local UI
|
// 8) Refresh local UI
|
||||||
await this.loadVideos();
|
await this.loadVideos();
|
||||||
|
|
||||||
|
// 8.1) Purge the outdated cache
|
||||||
|
this.videosMap.clear();
|
||||||
|
|
||||||
this.showSuccess("Video updated successfully!");
|
this.showSuccess("Video updated successfully!");
|
||||||
|
|
||||||
// 9) Also refresh all profile caches so any new name/pic changes are reflected
|
// 9) Also refresh all profile caches so any new name/pic changes are reflected
|
||||||
@@ -1315,18 +1326,15 @@ class bitvidApp {
|
|||||||
try {
|
try {
|
||||||
// 1) Check local subscription map
|
// 1) Check local subscription map
|
||||||
let video = this.videosMap.get(eventId);
|
let video = this.videosMap.get(eventId);
|
||||||
|
|
||||||
// 2) If not in local map, attempt fallback fetch from getOldEventById
|
// 2) If not in local map, attempt fallback fetch from getOldEventById
|
||||||
if (!video) {
|
if (!video) {
|
||||||
video = await this.getOldEventById(eventId);
|
video = await this.getOldEventById(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) If still no luck, show error and return
|
// 3) If still no luck, show error and return
|
||||||
if (!video) {
|
if (!video) {
|
||||||
this.showError("Video not found.");
|
this.showError("Video not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Decrypt magnet if private & owned
|
// 4) Decrypt magnet if private & owned
|
||||||
if (
|
if (
|
||||||
video.isPrivate &&
|
video.isPrivate &&
|
||||||
@@ -1337,18 +1345,15 @@ class bitvidApp {
|
|||||||
video.magnet = fakeDecrypt(video.magnet);
|
video.magnet = fakeDecrypt(video.magnet);
|
||||||
video.alreadyDecrypted = true;
|
video.alreadyDecrypted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Show the modal
|
// 5) Show the modal
|
||||||
this.currentVideo = video;
|
this.currentVideo = video;
|
||||||
this.currentMagnetUri = video.magnet;
|
this.currentMagnetUri = video.magnet;
|
||||||
this.showModalWithPoster();
|
this.showModalWithPoster();
|
||||||
|
|
||||||
// 6) Update ?v= param in the URL
|
// 6) Update ?v= param in the URL
|
||||||
const nevent = window.NostrTools.nip19.neventEncode({ id: eventId });
|
const nevent = window.NostrTools.nip19.neventEncode({ id: eventId });
|
||||||
const newUrl =
|
const newUrl =
|
||||||
window.location.pathname + `?v=${encodeURIComponent(nevent)}`;
|
window.location.pathname + `?v=${encodeURIComponent(nevent)}`;
|
||||||
window.history.pushState({}, "", newUrl);
|
window.history.pushState({}, "", newUrl);
|
||||||
|
|
||||||
// 7) Optionally fetch the author profile
|
// 7) Optionally fetch the author profile
|
||||||
let creatorProfile = {
|
let creatorProfile = {
|
||||||
name: "Unknown",
|
name: "Unknown",
|
||||||
@@ -1368,7 +1373,6 @@ class bitvidApp {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log("Error fetching creator profile:", error);
|
this.log("Error fetching creator profile:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8) Render video details in modal
|
// 8) Render video details in modal
|
||||||
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||||
if (this.videoTitle)
|
if (this.videoTitle)
|
||||||
@@ -1393,15 +1397,16 @@ class bitvidApp {
|
|||||||
this.creatorAvatar.src = creatorProfile.picture;
|
this.creatorAvatar.src = creatorProfile.picture;
|
||||||
this.creatorAvatar.alt = creatorProfile.name;
|
this.creatorAvatar.alt = creatorProfile.name;
|
||||||
}
|
}
|
||||||
|
// 9) Clean up any existing torrent instance before starting a new stream.
|
||||||
// 9) Stream torrent
|
await torrentClient.cleanup();
|
||||||
this.log("Starting video stream with:", video.magnet);
|
// 10) Append a cache-busting parameter to the magnet URI.
|
||||||
|
const cacheBustedMagnet = video.magnet + "&ts=" + Date.now();
|
||||||
|
this.log("Starting video stream with:", cacheBustedMagnet);
|
||||||
const realTorrent = await torrentClient.streamVideo(
|
const realTorrent = await torrentClient.streamVideo(
|
||||||
video.magnet,
|
cacheBustedMagnet,
|
||||||
this.modalVideo
|
this.modalVideo
|
||||||
);
|
);
|
||||||
|
// 11) Start intervals to update stats
|
||||||
// 10) Start intervals to update stats
|
|
||||||
const updateInterval = setInterval(() => {
|
const updateInterval = setInterval(() => {
|
||||||
if (!document.body.contains(this.modalVideo)) {
|
if (!document.body.contains(this.modalVideo)) {
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
@@ -1410,7 +1415,6 @@ class bitvidApp {
|
|||||||
this.updateTorrentStatus(realTorrent);
|
this.updateTorrentStatus(realTorrent);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
this.activeIntervals.push(updateInterval);
|
this.activeIntervals.push(updateInterval);
|
||||||
|
|
||||||
// (Optional) Mirror small inline stats into the modal
|
// (Optional) Mirror small inline stats into the modal
|
||||||
const mirrorInterval = setInterval(() => {
|
const mirrorInterval = setInterval(() => {
|
||||||
if (!document.body.contains(this.modalVideo)) {
|
if (!document.body.contains(this.modalVideo)) {
|
||||||
|
@@ -10,6 +10,7 @@ const npubs = [
|
|||||||
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
|
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
|
||||||
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
|
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
|
||||||
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
|
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
|
||||||
|
"npub13qexjtmajssuhz8gdchgx65dwsnr705drse294zz5vt4e78ya2vqzyg8lv", // SatoshiSignal
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
||||||
|
249
src/js/nostr.js
249
src/js/nostr.js
@@ -3,15 +3,18 @@
|
|||||||
import { isDevMode } from "./config.js";
|
import { isDevMode } from "./config.js";
|
||||||
import { accessControl } from "./accessControl.js";
|
import { accessControl } from "./accessControl.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The usual relays
|
||||||
|
*/
|
||||||
const RELAY_URLS = [
|
const RELAY_URLS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://nos.lol",
|
"wss://nos.lol",
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://nostr.wine",
|
"wss://relay.primal.net",
|
||||||
"wss://relay.nostr.band",
|
"wss://relay.nostr.band",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Just a helper to keep error spam in check
|
// To limit error spam
|
||||||
let errorLogCount = 0;
|
let errorLogCount = 0;
|
||||||
const MAX_ERROR_LOGS = 100;
|
const MAX_ERROR_LOGS = 100;
|
||||||
function logErrorOnce(message, eventContent = null) {
|
function logErrorOnce(message, eventContent = null) {
|
||||||
@@ -31,7 +34,7 @@ function logErrorOnce(message, eventContent = null) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Example "encryption" that just reverses strings.
|
* Example "encryption" that just reverses strings.
|
||||||
* In real usage, swap with actual crypto.
|
* In real usage, replace with actual crypto.
|
||||||
*/
|
*/
|
||||||
function fakeEncrypt(magnet) {
|
function fakeEncrypt(magnet) {
|
||||||
return magnet.split("").reverse().join("");
|
return magnet.split("").reverse().join("");
|
||||||
@@ -42,39 +45,61 @@ function fakeDecrypt(encrypted) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a raw Nostr event => your "video" object.
|
* Convert a raw Nostr event => your "video" object.
|
||||||
|
* CHANGED: skip if version <2
|
||||||
*/
|
*/
|
||||||
function convertEventToVideo(event) {
|
function convertEventToVideo(event) {
|
||||||
const content = JSON.parse(event.content || "{}");
|
try {
|
||||||
return {
|
const content = JSON.parse(event.content || "{}");
|
||||||
id: event.id,
|
|
||||||
// If content.videoRootId is missing, use event.id as a fallback
|
// Example checks:
|
||||||
videoRootId: content.videoRootId || event.id,
|
const isSupportedVersion = content.version >= 2;
|
||||||
version: content.version ?? 1,
|
const hasRequiredFields = !!(content.title && content.magnet);
|
||||||
isPrivate: content.isPrivate ?? false,
|
|
||||||
title: content.title || "",
|
if (!isSupportedVersion) {
|
||||||
magnet: content.magnet || "",
|
return {
|
||||||
thumbnail: content.thumbnail || "",
|
id: event.id,
|
||||||
description: content.description || "",
|
invalid: true,
|
||||||
mode: content.mode || "live",
|
reason: "version <2",
|
||||||
deleted: content.deleted === true,
|
};
|
||||||
pubkey: event.pubkey,
|
}
|
||||||
created_at: event.created_at,
|
if (!hasRequiredFields) {
|
||||||
tags: event.tags,
|
return {
|
||||||
};
|
id: event.id,
|
||||||
|
invalid: true,
|
||||||
|
reason: "missing title/magnet",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
videoRootId: content.videoRootId || event.id,
|
||||||
|
version: content.version,
|
||||||
|
isPrivate: content.isPrivate ?? false,
|
||||||
|
title: content.title ?? "",
|
||||||
|
magnet: content.magnet ?? "",
|
||||||
|
thumbnail: content.thumbnail ?? "",
|
||||||
|
description: content.description ?? "",
|
||||||
|
mode: content.mode ?? "live",
|
||||||
|
deleted: content.deleted === true,
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
created_at: event.created_at,
|
||||||
|
tags: event.tags,
|
||||||
|
invalid: false,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// JSON parse error
|
||||||
|
return { id: event.id, invalid: true, reason: "json parse error" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key each "active" video by its root ID => so you only store
|
* If the video has videoRootId => use that as the “group key”.
|
||||||
* the newest version for each root. But for older events w/o videoRootId,
|
* Otherwise fallback to (pubkey + dTag), or if no dTag => “LEGACY:id”
|
||||||
* or w/o 'd' tag, we handle fallback logic below.
|
|
||||||
*/
|
*/
|
||||||
function getActiveKey(video) {
|
function getActiveKey(video) {
|
||||||
// If it has a videoRootId, we use that
|
|
||||||
if (video.videoRootId) {
|
if (video.videoRootId) {
|
||||||
return `ROOT:${video.videoRootId}`;
|
return `ROOT:${video.videoRootId}`;
|
||||||
}
|
}
|
||||||
// Otherwise fallback to (pubkey + dTag) or if no dTag, fallback to event.id
|
|
||||||
// This is a fallback approach so older events appear in the "active map".
|
|
||||||
const dTag = video.tags?.find((t) => t[0] === "d");
|
const dTag = video.tags?.find((t) => t[0] === "d");
|
||||||
if (dTag) {
|
if (dTag) {
|
||||||
return `${video.pubkey}:${dTag[1]}`;
|
return `${video.pubkey}:${dTag[1]}`;
|
||||||
@@ -88,15 +113,15 @@ class NostrClient {
|
|||||||
this.pubkey = null;
|
this.pubkey = null;
|
||||||
this.relays = RELAY_URLS;
|
this.relays = RELAY_URLS;
|
||||||
|
|
||||||
// All events—old or new—so older share links still work
|
// Store all events so older links still work
|
||||||
this.allEvents = new Map();
|
this.allEvents = new Map();
|
||||||
|
|
||||||
// "activeMap" holds only the newest version for each root ID (or fallback).
|
// “activeMap” holds only the newest version for each root
|
||||||
this.activeMap = new Map();
|
this.activeMap = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to all configured relays
|
* Connect to the configured relays
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
if (isDevMode) console.log("Connecting to relays...");
|
if (isDevMode) console.log("Connecting to relays...");
|
||||||
@@ -107,7 +132,9 @@ class NostrClient {
|
|||||||
const successfulRelays = results
|
const successfulRelays = results
|
||||||
.filter((r) => r.success)
|
.filter((r) => r.success)
|
||||||
.map((r) => r.url);
|
.map((r) => r.url);
|
||||||
if (successfulRelays.length === 0) throw new Error("No relays connected");
|
if (successfulRelays.length === 0) {
|
||||||
|
throw new Error("No relays connected");
|
||||||
|
}
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
||||||
}
|
}
|
||||||
@@ -133,7 +160,6 @@ class NostrClient {
|
|||||||
sub.unsub();
|
sub.unsub();
|
||||||
resolve({ url, success: true });
|
resolve({ url, success: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
sub.on("event", succeed);
|
sub.on("event", succeed);
|
||||||
sub.on("eose", succeed);
|
sub.on("eose", succeed);
|
||||||
})
|
})
|
||||||
@@ -142,7 +168,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt Nostr extension login or abort
|
* Attempt login with a Nostr extension
|
||||||
*/
|
*/
|
||||||
async login() {
|
async login() {
|
||||||
try {
|
try {
|
||||||
@@ -152,7 +178,6 @@ class NostrClient {
|
|||||||
"Please install a Nostr extension (Alby, nos2x, etc.)."
|
"Please install a Nostr extension (Alby, nos2x, etc.)."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
const pubkey = await window.nostr.getPublicKey();
|
||||||
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
||||||
|
|
||||||
@@ -162,8 +187,7 @@ class NostrClient {
|
|||||||
console.log("Whitelist:", accessControl.getWhitelist());
|
console.log("Whitelist:", accessControl.getWhitelist());
|
||||||
console.log("Blacklist:", accessControl.getBlacklist());
|
console.log("Blacklist:", accessControl.getBlacklist());
|
||||||
}
|
}
|
||||||
|
// Access control
|
||||||
// Access control check
|
|
||||||
if (!accessControl.canAccess(npub)) {
|
if (!accessControl.canAccess(npub)) {
|
||||||
if (accessControl.isBlacklisted(npub)) {
|
if (accessControl.isBlacklisted(npub)) {
|
||||||
throw new Error("Your account has been blocked on this platform.");
|
throw new Error("Your account has been blocked on this platform.");
|
||||||
@@ -171,15 +195,14 @@ class NostrClient {
|
|||||||
throw new Error("Access restricted to whitelisted users only.");
|
throw new Error("Access restricted to whitelisted users only.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pubkey = pubkey;
|
this.pubkey = pubkey;
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
console.log("Logged in with extension. Pubkey:", this.pubkey);
|
console.log("Logged in with extension. Pubkey:", this.pubkey);
|
||||||
}
|
}
|
||||||
return this.pubkey;
|
return this.pubkey;
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error("Login error:", e);
|
console.error("Login error:", err);
|
||||||
throw e;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,17 +211,9 @@ class NostrClient {
|
|||||||
if (isDevMode) console.log("User logged out.");
|
if (isDevMode) console.log("User logged out.");
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeNsec(nsec) {
|
|
||||||
try {
|
|
||||||
const { data } = window.NostrTools.nip19.decode(nsec);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid NSEC key.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish a *new* video with a brand-new d tag & brand-new videoRootId
|
* Publish a new video
|
||||||
|
* CHANGED: Force version=2 for all new notes
|
||||||
*/
|
*/
|
||||||
async publishVideo(videoData, pubkey) {
|
async publishVideo(videoData, pubkey) {
|
||||||
if (!pubkey) throw new Error("Not logged in to publish video.");
|
if (!pubkey) throw new Error("Not logged in to publish video.");
|
||||||
@@ -212,13 +227,13 @@ class NostrClient {
|
|||||||
finalMagnet = fakeEncrypt(finalMagnet);
|
finalMagnet = fakeEncrypt(finalMagnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// new "videoRootId" ensures all future edits know they're from the same root
|
// brand-new root & d
|
||||||
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
const contentObject = {
|
const contentObject = {
|
||||||
videoRootId,
|
videoRootId,
|
||||||
version: videoData.version ?? 1,
|
version: 2, // forcibly set version=2
|
||||||
deleted: false,
|
deleted: false,
|
||||||
isPrivate: videoData.isPrivate ?? false,
|
isPrivate: videoData.isPrivate ?? false,
|
||||||
title: videoData.title || "",
|
title: videoData.title || "",
|
||||||
@@ -258,7 +273,6 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return signedEvent;
|
return signedEvent;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDevMode) console.error("Failed to sign/publish:", err);
|
if (isDevMode) console.error("Failed to sign/publish:", err);
|
||||||
@@ -267,11 +281,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits a video by creating a *new event* with a brand-new d tag,
|
* Edits a video => old style
|
||||||
* but reuses the same videoRootId as the original.
|
|
||||||
*
|
|
||||||
* => old link remains pinned to the old event, new link is a fresh ID.
|
|
||||||
* => older version is overshadowed if your dedupe logic only shows newest.
|
|
||||||
*/
|
*/
|
||||||
async editVideo(originalEventStub, updatedData, pubkey) {
|
async editVideo(originalEventStub, updatedData, pubkey) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
@@ -281,9 +291,7 @@ class NostrClient {
|
|||||||
throw new Error("You do not own this video (pubkey mismatch).");
|
throw new Error("You do not own this video (pubkey mismatch).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Attempt to get the FULL old event details (especially videoRootId)
|
|
||||||
let baseEvent = originalEventStub;
|
let baseEvent = originalEventStub;
|
||||||
// If the caller didn't pass .videoRootId, fetch from local or relay:
|
|
||||||
if (!baseEvent.videoRootId) {
|
if (!baseEvent.videoRootId) {
|
||||||
const fetched = await this.getEventById(originalEventStub.id);
|
const fetched = await this.getEventById(originalEventStub.id);
|
||||||
if (!fetched) {
|
if (!fetched) {
|
||||||
@@ -292,32 +300,25 @@ class NostrClient {
|
|||||||
baseEvent = fetched;
|
baseEvent = fetched;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) We now have baseEvent.videoRootId if it existed
|
|
||||||
let oldRootId = baseEvent.videoRootId || null;
|
let oldRootId = baseEvent.videoRootId || null;
|
||||||
|
|
||||||
// Decrypt the old magnet if it was private
|
// Decrypt old magnet if private
|
||||||
let oldPlainMagnet = baseEvent.magnet || "";
|
let oldPlainMagnet = baseEvent.magnet || "";
|
||||||
if (baseEvent.isPrivate && oldPlainMagnet) {
|
if (baseEvent.isPrivate && oldPlainMagnet) {
|
||||||
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Decide new privacy
|
|
||||||
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
||||||
|
|
||||||
// 4) Fallback to old magnet if none was provided
|
|
||||||
let finalPlainMagnet = (updatedData.magnet || "").trim();
|
let finalPlainMagnet = (updatedData.magnet || "").trim();
|
||||||
if (!finalPlainMagnet) {
|
if (!finalPlainMagnet) {
|
||||||
finalPlainMagnet = oldPlainMagnet;
|
finalPlainMagnet = oldPlainMagnet;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Re-encrypt if user wants private
|
|
||||||
let finalMagnet = finalPlainMagnet;
|
let finalMagnet = finalPlainMagnet;
|
||||||
if (wantPrivate) {
|
if (wantPrivate) {
|
||||||
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) If there's no root yet (legacy), use the old event's own ID.
|
|
||||||
// Otherwise keep the existing rootId.
|
|
||||||
if (!oldRootId) {
|
if (!oldRootId) {
|
||||||
oldRootId = baseEvent.id;
|
oldRootId = baseEvent.id;
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
@@ -328,10 +329,8 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a brand-new d-tag so it doesn't overshadow the old share link
|
|
||||||
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
// 7) Build updated content
|
|
||||||
const contentObject = {
|
const contentObject = {
|
||||||
videoRootId: oldRootId,
|
videoRootId: oldRootId,
|
||||||
version: updatedData.version ?? baseEvent.version ?? 1,
|
version: updatedData.version ?? baseEvent.version ?? 1,
|
||||||
@@ -350,7 +349,7 @@ class NostrClient {
|
|||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [
|
tags: [
|
||||||
["t", "video"],
|
["t", "video"],
|
||||||
["d", newD], // new share link
|
["d", newD],
|
||||||
],
|
],
|
||||||
content: JSON.stringify(contentObject),
|
content: JSON.stringify(contentObject),
|
||||||
};
|
};
|
||||||
@@ -360,7 +359,6 @@ class NostrClient {
|
|||||||
console.log("Event content:", event.content);
|
console.log("Event content:", event.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8) Sign and publish the new event
|
|
||||||
try {
|
try {
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
const signedEvent = await window.nostr.signEvent(event);
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
@@ -389,7 +387,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Reverting" => we just mark the most recent content as {deleted:true} and blank out magnet/desc
|
* revertVideo => old style
|
||||||
*/
|
*/
|
||||||
async revertVideo(originalEvent, pubkey) {
|
async revertVideo(originalEvent, pubkey) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
@@ -399,7 +397,6 @@ class NostrClient {
|
|||||||
throw new Error("Not your event (pubkey mismatch).");
|
throw new Error("Not your event (pubkey mismatch).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If front-end didn't pass the tags array, load the full event:
|
|
||||||
let baseEvent = originalEvent;
|
let baseEvent = originalEvent;
|
||||||
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
|
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
|
||||||
const fetched = await this.getEventById(originalEvent.id);
|
const fetched = await this.getEventById(originalEvent.id);
|
||||||
@@ -423,7 +420,6 @@ class NostrClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check d-tag
|
|
||||||
const dTag = baseEvent.tags.find((t) => t[0] === "d");
|
const dTag = baseEvent.tags.find((t) => t[0] === "d");
|
||||||
if (!dTag) {
|
if (!dTag) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -435,17 +431,15 @@ class NostrClient {
|
|||||||
const oldContent = JSON.parse(baseEvent.content || "{}");
|
const oldContent = JSON.parse(baseEvent.content || "{}");
|
||||||
const oldVersion = oldContent.version ?? 1;
|
const oldVersion = oldContent.version ?? 1;
|
||||||
|
|
||||||
// If no root, fallback
|
|
||||||
let finalRootId = oldContent.videoRootId || null;
|
let finalRootId = oldContent.videoRootId || null;
|
||||||
if (!finalRootId) {
|
if (!finalRootId) {
|
||||||
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
|
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build “deleted: true” overshadow event => revert current version
|
|
||||||
const contentObject = {
|
const contentObject = {
|
||||||
videoRootId: finalRootId,
|
videoRootId: finalRootId,
|
||||||
version: oldVersion,
|
version: oldVersion,
|
||||||
deleted: true, // mark *this version* as deleted
|
deleted: true,
|
||||||
isPrivate: oldContent.isPrivate ?? false,
|
isPrivate: oldContent.isPrivate ?? false,
|
||||||
title: oldContent.title || "",
|
title: oldContent.title || "",
|
||||||
magnet: "",
|
magnet: "",
|
||||||
@@ -460,7 +454,7 @@ class NostrClient {
|
|||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [
|
tags: [
|
||||||
["t", "video"],
|
["t", "video"],
|
||||||
["d", existingD], // re-use same d => overshadow
|
["d", existingD],
|
||||||
],
|
],
|
||||||
content: JSON.stringify(contentObject),
|
content: JSON.stringify(contentObject),
|
||||||
};
|
};
|
||||||
@@ -471,9 +465,7 @@ class NostrClient {
|
|||||||
try {
|
try {
|
||||||
await this.pool.publish([url], signedEvent);
|
await this.pool.publish([url], signedEvent);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDevMode) {
|
if (isDevMode) console.error(`Failed to revert on ${url}`, err);
|
||||||
console.error(`Failed to revert on ${url}`, err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -482,15 +474,13 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Deleting" => we just mark all content with the same videoRootId as {deleted:true} and blank out magnet/desc
|
* deleteAllVersions => old style
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async deleteAllVersions(videoRootId, pubkey) {
|
async deleteAllVersions(videoRootId, pubkey) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
throw new Error("Not logged in to delete all versions.");
|
throw new Error("Not logged in to delete all versions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Find all events in our local allEvents that share the same root.
|
|
||||||
const matchingEvents = [];
|
const matchingEvents = [];
|
||||||
for (const [id, vid] of this.allEvents.entries()) {
|
for (const [id, vid] of this.allEvents.entries()) {
|
||||||
if (
|
if (
|
||||||
@@ -501,19 +491,13 @@ class NostrClient {
|
|||||||
matchingEvents.push(vid);
|
matchingEvents.push(vid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If you want to re-check the relay for older versions too,
|
|
||||||
// you can do a fallback query, but typically your local cache is enough.
|
|
||||||
|
|
||||||
if (!matchingEvents.length) {
|
if (!matchingEvents.length) {
|
||||||
throw new Error("No existing events found for that root.");
|
throw new Error("No existing events found for that root.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) For each event, create a "deleted: true" overshadow
|
|
||||||
// by re-using the same d-tag so it cannot appear again.
|
|
||||||
for (const vid of matchingEvents) {
|
for (const vid of matchingEvents) {
|
||||||
await this.revertVideo(
|
await this.revertVideo(
|
||||||
{
|
{
|
||||||
// re-using revertVideo logic
|
|
||||||
id: vid.id,
|
id: vid.id,
|
||||||
pubkey: vid.pubkey,
|
pubkey: vid.pubkey,
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
@@ -531,15 +515,11 @@ class NostrClient {
|
|||||||
pubkey
|
pubkey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally return some status
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to *all* video events. We store them in this.allEvents so older
|
* subscribeVideos => old approach
|
||||||
* notes remain accessible by ID, plus we maintain this.activeMap for the newest
|
|
||||||
* version of each root (or fallback).
|
|
||||||
*/
|
*/
|
||||||
subscribeVideos(onVideo) {
|
subscribeVideos(onVideo) {
|
||||||
const filter = {
|
const filter = {
|
||||||
@@ -553,37 +533,28 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sub = this.pool.sub(this.relays, [filter]);
|
const sub = this.pool.sub(this.relays, [filter]);
|
||||||
|
// Accumulate invalid
|
||||||
|
const invalidDuringSub = [];
|
||||||
|
|
||||||
sub.on("event", (event) => {
|
sub.on("event", (event) => {
|
||||||
try {
|
try {
|
||||||
const video = convertEventToVideo(event);
|
const video = convertEventToVideo(event);
|
||||||
this.allEvents.set(event.id, video);
|
if (video.invalid) {
|
||||||
|
invalidDuringSub.push({ id: video.id, reason: video.reason });
|
||||||
// If it’s marked deleted, remove from active map if it’s the active version
|
return;
|
||||||
// NEW CODE
|
}
|
||||||
if (video.deleted) {
|
// normal logic here
|
||||||
const activeKey = getActiveKey(video);
|
this.allEvents.set(event.id, video);
|
||||||
// Don't compare IDs—just remove that key from the active map
|
if (video.deleted) {
|
||||||
this.activeMap.delete(activeKey);
|
const activeKey = getActiveKey(video);
|
||||||
|
this.activeMap.delete(activeKey);
|
||||||
// (Optional) If you want a debug log:
|
|
||||||
// console.log(`[DELETE] Removed activeKey=${activeKey}`);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not deleted => see if it’s the newest
|
|
||||||
const activeKey = getActiveKey(video);
|
const activeKey = getActiveKey(video);
|
||||||
const prevActive = this.activeMap.get(activeKey);
|
const prevActive = this.activeMap.get(activeKey);
|
||||||
if (!prevActive) {
|
if (!prevActive || video.created_at > prevActive.created_at) {
|
||||||
// brand new => set it
|
|
||||||
this.activeMap.set(activeKey, video);
|
this.activeMap.set(activeKey, video);
|
||||||
onVideo(video);
|
onVideo(video);
|
||||||
} else {
|
|
||||||
// compare timestamps
|
|
||||||
if (video.created_at > prevActive.created_at) {
|
|
||||||
this.activeMap.set(activeKey, video);
|
|
||||||
onVideo(video);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
@@ -593,6 +564,12 @@ class NostrClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sub.on("eose", () => {
|
sub.on("eose", () => {
|
||||||
|
if (isDevMode && invalidDuringSub.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[subscribeVideos] found ${invalidDuringSub.length} invalid v2 notes:`,
|
||||||
|
invalidDuringSub
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
console.log("[subscribeVideos] Reached EOSE for all relays");
|
console.log("[subscribeVideos] Reached EOSE for all relays");
|
||||||
}
|
}
|
||||||
@@ -602,7 +579,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk fetch from all relays, store in allEvents, rebuild activeMap
|
* fetchVideos => old approach
|
||||||
*/
|
*/
|
||||||
async fetchVideos() {
|
async fetchVideos() {
|
||||||
const filter = {
|
const filter = {
|
||||||
@@ -613,39 +590,51 @@ class NostrClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const localAll = new Map();
|
const localAll = new Map();
|
||||||
|
// NEW: track invalid
|
||||||
|
const invalidNotes = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Fetch all events from each relay
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.relays.map(async (url) => {
|
this.relays.map(async (url) => {
|
||||||
const events = await this.pool.list([url], [filter]);
|
const events = await this.pool.list([url], [filter]);
|
||||||
for (const evt of events) {
|
for (const evt of events) {
|
||||||
const vid = convertEventToVideo(evt);
|
const vid = convertEventToVideo(evt);
|
||||||
localAll.set(evt.id, vid);
|
if (vid.invalid) {
|
||||||
|
// Accumulate if invalid
|
||||||
|
invalidNotes.push({ id: vid.id, reason: vid.reason });
|
||||||
|
} else {
|
||||||
|
// Only add if good
|
||||||
|
localAll.set(evt.id, vid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2) Merge into this.allEvents
|
// Merge into allEvents
|
||||||
for (const [id, vid] of localAll.entries()) {
|
for (const [id, vid] of localAll.entries()) {
|
||||||
this.allEvents.set(id, vid);
|
this.allEvents.set(id, vid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Rebuild activeMap
|
// Rebuild activeMap
|
||||||
this.activeMap.clear();
|
this.activeMap.clear();
|
||||||
for (const [id, video] of this.allEvents.entries()) {
|
for (const [id, video] of this.allEvents.entries()) {
|
||||||
// Skip if the video is marked deleted
|
|
||||||
if (video.deleted) continue;
|
if (video.deleted) continue;
|
||||||
|
|
||||||
const activeKey = getActiveKey(video);
|
const activeKey = getActiveKey(video);
|
||||||
const existing = this.activeMap.get(activeKey);
|
const existing = this.activeMap.get(activeKey);
|
||||||
|
|
||||||
// If there's no existing entry or this is newer, set/replace
|
|
||||||
if (!existing || video.created_at > existing.created_at) {
|
if (!existing || video.created_at > existing.created_at) {
|
||||||
this.activeMap.set(activeKey, video);
|
this.activeMap.set(activeKey, video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Return newest version for each root in descending order
|
// OPTIONAL: Log invalid stats
|
||||||
|
if (invalidNotes.length > 0 && isDevMode) {
|
||||||
|
console.warn(
|
||||||
|
`Skipped ${invalidNotes.length} invalid v2 notes:\n`,
|
||||||
|
invalidNotes.map((n) => `${n.id.slice(0, 8)}.. => ${n.reason}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const activeVideos = Array.from(this.activeMap.values()).sort(
|
const activeVideos = Array.from(this.activeMap.values()).sort(
|
||||||
(a, b) => b.created_at - a.created_at
|
(a, b) => b.created_at - a.created_at
|
||||||
);
|
);
|
||||||
@@ -657,14 +646,13 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to fetch an event by ID from local cache, then from the relays
|
* getEventById => old approach
|
||||||
*/
|
*/
|
||||||
async getEventById(eventId) {
|
async getEventById(eventId) {
|
||||||
const local = this.allEvents.get(eventId);
|
const local = this.allEvents.get(eventId);
|
||||||
if (local) {
|
if (local) {
|
||||||
return local;
|
return local;
|
||||||
}
|
}
|
||||||
// direct fetch if missing
|
|
||||||
try {
|
try {
|
||||||
for (const url of this.relays) {
|
for (const url of this.relays) {
|
||||||
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
|
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
|
||||||
@@ -679,12 +667,9 @@ class NostrClient {
|
|||||||
console.error("getEventById direct fetch error:", err);
|
console.error("getEventById direct fetch error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // not found
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return newest versions from activeMap if you want to skip older events
|
|
||||||
*/
|
|
||||||
getActiveVideos() {
|
getActiveVideos() {
|
||||||
return Array.from(this.activeMap.values()).sort(
|
return Array.from(this.activeMap.values()).sort(
|
||||||
(a, b) => b.created_at - a.created_at
|
(a, b) => b.created_at - a.created_at
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
// js/webtorrent.js
|
|
||||||
|
|
||||||
import WebTorrent from "./webtorrent.min.js";
|
import WebTorrent from "./webtorrent.min.js";
|
||||||
|
|
||||||
export class TorrentClient {
|
export class TorrentClient {
|
||||||
@@ -41,7 +39,6 @@ export class TorrentClient {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// If it's already active, resolve immediately
|
|
||||||
if (checkActivation()) return;
|
if (checkActivation()) return;
|
||||||
|
|
||||||
registration.addEventListener("activate", () => {
|
registration.addEventListener("activate", () => {
|
||||||
@@ -59,9 +56,6 @@ export class TorrentClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// setupServiceWorker: Registers /sw.min.js at the root with scope "/"
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
async setupServiceWorker() {
|
async setupServiceWorker() {
|
||||||
try {
|
try {
|
||||||
const isBraveBrowser = await this.isBrave();
|
const isBraveBrowser = await this.isBrave();
|
||||||
@@ -73,31 +67,32 @@ export class TorrentClient {
|
|||||||
throw new Error("Service Worker not supported or disabled");
|
throw new Error("Service Worker not supported or disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Optional) Brave config check
|
|
||||||
if (isBraveBrowser) {
|
if (isBraveBrowser) {
|
||||||
this.log("Checking Brave configuration...");
|
this.log("Checking Brave configuration...");
|
||||||
if (!navigator.serviceWorker) {
|
if (!navigator.serviceWorker) {
|
||||||
throw new Error("Please enable Service Workers in Brave Shield settings");
|
throw new Error(
|
||||||
|
"Please enable Service Workers in Brave Shield settings"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
throw new Error("Please enable WebRTC in Brave Shield settings");
|
throw new Error("Please enable WebRTC in Brave Shield settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister any existing service workers
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
for (const reg of registrations) {
|
for (const reg of registrations) {
|
||||||
await reg.unregister();
|
await reg.unregister();
|
||||||
}
|
}
|
||||||
// Short delay to ensure old workers are removed
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register sw.min.js from the root (Netlify serves it at /sw.min.js)
|
|
||||||
this.log("Registering service worker at /sw.min.js...");
|
this.log("Registering service worker at /sw.min.js...");
|
||||||
const registration = await navigator.serviceWorker.register("/sw.min.js", {
|
const registration = await navigator.serviceWorker.register(
|
||||||
scope: "/",
|
"./sw.min.js",
|
||||||
updateViaCache: "none",
|
{
|
||||||
});
|
scope: "./",
|
||||||
|
updateViaCache: "none",
|
||||||
|
}
|
||||||
|
);
|
||||||
this.log("Service worker registered");
|
this.log("Service worker registered");
|
||||||
|
|
||||||
if (registration.installing) {
|
if (registration.installing) {
|
||||||
@@ -123,7 +118,6 @@ export class TorrentClient {
|
|||||||
await this.waitForServiceWorkerActivation(registration);
|
await this.waitForServiceWorkerActivation(registration);
|
||||||
this.log("Service worker activated");
|
this.log("Service worker activated");
|
||||||
|
|
||||||
// Ensure the service worker is fully ready
|
|
||||||
const readyRegistration = await Promise.race([
|
const readyRegistration = await Promise.race([
|
||||||
navigator.serviceWorker.ready,
|
navigator.serviceWorker.ready,
|
||||||
new Promise((_, reject) =>
|
new Promise((_, reject) =>
|
||||||
@@ -138,6 +132,9 @@ export class TorrentClient {
|
|||||||
throw new Error("Service worker not active after ready state");
|
throw new Error("Service worker not active after ready state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force the SW to check for updates
|
||||||
|
registration.update();
|
||||||
|
|
||||||
this.log("Service worker ready");
|
this.log("Service worker ready");
|
||||||
return registration;
|
return registration;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -256,9 +253,11 @@ export class TorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the WebTorrent server with the registered service worker.
|
// Create the WebTorrent server with the registered service worker.
|
||||||
// (If you need to specify a custom URL prefix for torrent streaming,
|
// Force the server to use '/webtorrent' as the URL prefix.
|
||||||
// pass a pathPrefix option here.)
|
this.client.createServer({
|
||||||
this.client.createServer({ controller: registration });
|
controller: registration,
|
||||||
|
pathPrefix: "/webtorrent",
|
||||||
|
});
|
||||||
this.log("WebTorrent server created");
|
this.log("WebTorrent server created");
|
||||||
|
|
||||||
const isFirefoxBrowser = this.isFirefox();
|
const isFirefoxBrowser = this.isFirefox();
|
||||||
|
63
src/sw.min.js
vendored
63
src/sw.min.js
vendored
@@ -3,15 +3,22 @@
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
// Handle skip waiting message
|
// Handle messages from clients
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
}
|
}
|
||||||
|
if (event.data && event.data.type === "CLEAR_CACHES") {
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((cacheNames) =>
|
||||||
|
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Immediately install and activate
|
// Immediately install and skip waiting
|
||||||
self.addEventListener("install", () => {
|
self.addEventListener("install", (event) => {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,23 +28,24 @@
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
clients.claim(),
|
clients.claim(),
|
||||||
self.skipWaiting(),
|
self.skipWaiting(),
|
||||||
caches.keys().then((cacheNames) =>
|
caches
|
||||||
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
|
.keys()
|
||||||
),
|
.then((cacheNames) =>
|
||||||
|
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
|
||||||
|
),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle fetch events
|
// Handle fetch events
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
const responsePromise = (() => {
|
const requestURL = event.request.url;
|
||||||
const requestURL = event.request.url;
|
// Only handle WebTorrent streaming requests; let other requests proceed normally.
|
||||||
// Only handle WebTorrent streaming requests
|
if (!requestURL.includes("/webtorrent/")) {
|
||||||
// Since our SW is registered with scope "/" the expected URL prefix is "/webtorrent/"
|
return;
|
||||||
if (!requestURL.includes("/webtorrent/")) {
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const responsePromise = (async () => {
|
||||||
// Handle keepalive requests
|
// Handle keepalive requests
|
||||||
if (requestURL.includes("/webtorrent/keepalive/")) {
|
if (requestURL.includes("/webtorrent/keepalive/")) {
|
||||||
return new Response();
|
return new Response();
|
||||||
@@ -57,14 +65,10 @@
|
|||||||
// Handle streaming requests
|
// Handle streaming requests
|
||||||
return (async function ({ request }) {
|
return (async function ({ request }) {
|
||||||
const { url, method, headers, destination } = request;
|
const { url, method, headers, destination } = request;
|
||||||
|
|
||||||
// Get all window clients
|
|
||||||
const windowClients = await clients.matchAll({
|
const windowClients = await clients.matchAll({
|
||||||
type: "window",
|
type: "window",
|
||||||
includeUncontrolled: true,
|
includeUncontrolled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a message channel and wait for a response from a client
|
|
||||||
const [clientResponse, port] = await new Promise((resolve) => {
|
const [clientResponse, port] = await new Promise((resolve) => {
|
||||||
for (const client of windowClients) {
|
for (const client of windowClients) {
|
||||||
const channel = new MessageChannel();
|
const channel = new MessageChannel();
|
||||||
@@ -92,13 +96,26 @@
|
|||||||
port.onmessage = null;
|
port.onmessage = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the response is not a streaming request, return a normal response
|
// Clone and update headers to prevent caching.
|
||||||
|
const responseHeaders = new Headers(clientResponse.headers);
|
||||||
|
responseHeaders.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"no-cache, no-store, must-revalidate, max-age=0"
|
||||||
|
);
|
||||||
|
responseHeaders.set("Pragma", "no-cache");
|
||||||
|
responseHeaders.set("Expires", "0");
|
||||||
|
|
||||||
|
// If the response is not a streaming request, return it directly.
|
||||||
if (clientResponse.body !== "STREAM") {
|
if (clientResponse.body !== "STREAM") {
|
||||||
closeChannel();
|
closeChannel();
|
||||||
return new Response(clientResponse.body, clientResponse);
|
return new Response(clientResponse.body, {
|
||||||
|
status: clientResponse.status,
|
||||||
|
statusText: clientResponse.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, handle streaming response using a ReadableStream
|
// Otherwise, stream the response via a ReadableStream.
|
||||||
return new Response(
|
return new Response(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
pull(controller) {
|
pull(controller) {
|
||||||
@@ -128,7 +145,11 @@
|
|||||||
closeChannel();
|
closeChannel();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
clientResponse
|
{
|
||||||
|
status: clientResponse.status,
|
||||||
|
statusText: clientResponse.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
})(event);
|
})(event);
|
||||||
})();
|
})();
|
||||||
|
Reference in New Issue
Block a user