added v2 note restrictions to enable upgrade path from v2 onward.

This commit is contained in:
Keep Creating Online
2025-02-04 00:23:37 -05:00
parent 139e279e35
commit 80d90387b8
12 changed files with 208 additions and 195 deletions

View File

@@ -1,4 +1,3 @@
{
"liveServer.settings.root": "/src"
}
"liveServer.settings.root": "./src"
}

View File

@@ -67,7 +67,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="components/iframe_forms/iframe-application-form.html"
src="./components/iframe_forms/iframe-application-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -56,7 +56,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="/components/iframe_forms/iframe-bug-fix-form.html"
src="./components/iframe_forms/iframe-bug-fix-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -53,7 +53,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="/components/iframe_forms/iframe-content-appeals-form.html"
src="./components/iframe_forms/iframe-content-appeals-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -53,7 +53,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="/components/iframe_forms/iframe-request-form.html"
src="./components/iframe_forms/iframe-request-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -57,7 +57,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="/components/iframe_forms/iframe-feedback-form.html"
src="./components/iframe_forms/iframe-feedback-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -633,6 +633,10 @@
<!-- Other Scripts -->
<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/lists.js"></script>
<script type="module" src="js/accessControl.js"></script>

View File

@@ -99,6 +99,13 @@ class bitvidApp {
async init() {
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)
await this.initModal();
this.updateModalElements();
@@ -766,7 +773,7 @@ class bitvidApp {
if (!videos || videos.length === 0) {
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!
</p>`;
return;
@@ -1175,6 +1182,10 @@ class bitvidApp {
// 8) Refresh local UI
await this.loadVideos();
// 8.1) Purge the outdated cache
this.videosMap.clear();
this.showSuccess("Video updated successfully!");
// 9) Also refresh all profile caches so any new name/pic changes are reflected
@@ -1315,18 +1326,15 @@ class bitvidApp {
try {
// 1) Check local subscription map
let video = this.videosMap.get(eventId);
// 2) If not in local map, attempt fallback fetch from getOldEventById
if (!video) {
video = await this.getOldEventById(eventId);
}
// 3) If still no luck, show error and return
if (!video) {
this.showError("Video not found.");
return;
}
// 4) Decrypt magnet if private & owned
if (
video.isPrivate &&
@@ -1337,18 +1345,15 @@ class bitvidApp {
video.magnet = fakeDecrypt(video.magnet);
video.alreadyDecrypted = true;
}
// 5) Show the modal
this.currentVideo = video;
this.currentMagnetUri = video.magnet;
this.showModalWithPoster();
// 6) Update ?v= param in the URL
const nevent = window.NostrTools.nip19.neventEncode({ id: eventId });
const newUrl =
window.location.pathname + `?v=${encodeURIComponent(nevent)}`;
window.history.pushState({}, "", newUrl);
// 7) Optionally fetch the author profile
let creatorProfile = {
name: "Unknown",
@@ -1368,7 +1373,6 @@ class bitvidApp {
} catch (error) {
this.log("Error fetching creator profile:", error);
}
// 8) Render video details in modal
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (this.videoTitle)
@@ -1393,15 +1397,16 @@ class bitvidApp {
this.creatorAvatar.src = creatorProfile.picture;
this.creatorAvatar.alt = creatorProfile.name;
}
// 9) Stream torrent
this.log("Starting video stream with:", video.magnet);
// 9) Clean up any existing torrent instance before starting a new stream.
await torrentClient.cleanup();
// 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(
video.magnet,
cacheBustedMagnet,
this.modalVideo
);
// 10) Start intervals to update stats
// 11) Start intervals to update stats
const updateInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(updateInterval);
@@ -1410,7 +1415,6 @@ class bitvidApp {
this.updateTorrentStatus(realTorrent);
}, 1000);
this.activeIntervals.push(updateInterval);
// (Optional) Mirror small inline stats into the modal
const mirrorInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {

View File

@@ -10,6 +10,7 @@ const npubs = [
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
"npub13qexjtmajssuhz8gdchgx65dwsnr705drse294zz5vt4e78ya2vqzyg8lv", // SatoshiSignal
];
console.log("DEBUG: lists.js loaded, npubs:", npubs);

View File

@@ -3,15 +3,18 @@
import { isDevMode } from "./config.js";
import { accessControl } from "./accessControl.js";
/**
* The usual relays
*/
const RELAY_URLS = [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://nostr.wine",
"wss://relay.primal.net",
"wss://relay.nostr.band",
];
// Just a helper to keep error spam in check
// To limit error spam
let errorLogCount = 0;
const MAX_ERROR_LOGS = 100;
function logErrorOnce(message, eventContent = null) {
@@ -31,7 +34,7 @@ function logErrorOnce(message, eventContent = null) {
/**
* Example "encryption" that just reverses strings.
* In real usage, swap with actual crypto.
* In real usage, replace with actual crypto.
*/
function fakeEncrypt(magnet) {
return magnet.split("").reverse().join("");
@@ -42,39 +45,61 @@ function fakeDecrypt(encrypted) {
/**
* Convert a raw Nostr event => your "video" object.
* CHANGED: skip if version <2
*/
function convertEventToVideo(event) {
const content = JSON.parse(event.content || "{}");
return {
id: event.id,
// If content.videoRootId is missing, use event.id as a fallback
videoRootId: content.videoRootId || event.id,
version: content.version ?? 1,
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,
};
try {
const content = JSON.parse(event.content || "{}");
// Example checks:
const isSupportedVersion = content.version >= 2;
const hasRequiredFields = !!(content.title && content.magnet);
if (!isSupportedVersion) {
return {
id: event.id,
invalid: true,
reason: "version <2",
};
}
if (!hasRequiredFields) {
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
* the newest version for each root. But for older events w/o videoRootId,
* or w/o 'd' tag, we handle fallback logic below.
* If the video has videoRootId => use that as the “group key”.
* Otherwise fallback to (pubkey + dTag), or if no dTag => “LEGACY:id”
*/
function getActiveKey(video) {
// If it has a videoRootId, we use that
if (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");
if (dTag) {
return `${video.pubkey}:${dTag[1]}`;
@@ -88,15 +113,15 @@ class NostrClient {
this.pubkey = null;
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();
// "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();
}
/**
* Connect to all configured relays
* Connect to the configured relays
*/
async init() {
if (isDevMode) console.log("Connecting to relays...");
@@ -107,7 +132,9 @@ class NostrClient {
const successfulRelays = results
.filter((r) => r.success)
.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) {
console.log(`Connected to ${successfulRelays.length} relay(s)`);
}
@@ -133,7 +160,6 @@ class NostrClient {
sub.unsub();
resolve({ url, success: true });
};
sub.on("event", 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() {
try {
@@ -152,7 +178,6 @@ class NostrClient {
"Please install a Nostr extension (Alby, nos2x, etc.)."
);
}
const pubkey = await window.nostr.getPublicKey();
const npub = window.NostrTools.nip19.npubEncode(pubkey);
@@ -162,8 +187,7 @@ class NostrClient {
console.log("Whitelist:", accessControl.getWhitelist());
console.log("Blacklist:", accessControl.getBlacklist());
}
// Access control check
// Access control
if (!accessControl.canAccess(npub)) {
if (accessControl.isBlacklisted(npub)) {
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.");
}
}
this.pubkey = pubkey;
if (isDevMode) {
console.log("Logged in with extension. Pubkey:", this.pubkey);
}
return this.pubkey;
} catch (e) {
console.error("Login error:", e);
throw e;
} catch (err) {
console.error("Login error:", err);
throw err;
}
}
@@ -188,17 +211,9 @@ class NostrClient {
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) {
if (!pubkey) throw new Error("Not logged in to publish video.");
@@ -212,13 +227,13 @@ class NostrClient {
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 dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const contentObject = {
videoRootId,
version: videoData.version ?? 1,
version: 2, // forcibly set version=2
deleted: false,
isPrivate: videoData.isPrivate ?? false,
title: videoData.title || "",
@@ -258,7 +273,6 @@ class NostrClient {
}
})
);
return signedEvent;
} catch (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,
* 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.
* Edits a video => old style
*/
async editVideo(originalEventStub, updatedData, pubkey) {
if (!pubkey) {
@@ -281,9 +291,7 @@ class NostrClient {
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;
// If the caller didn't pass .videoRootId, fetch from local or relay:
if (!baseEvent.videoRootId) {
const fetched = await this.getEventById(originalEventStub.id);
if (!fetched) {
@@ -292,32 +300,25 @@ class NostrClient {
baseEvent = fetched;
}
// 2) We now have baseEvent.videoRootId if it existed
let oldRootId = baseEvent.videoRootId || null;
// Decrypt the old magnet if it was private
// Decrypt old magnet if private
let oldPlainMagnet = baseEvent.magnet || "";
if (baseEvent.isPrivate && oldPlainMagnet) {
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
}
// 3) Decide new privacy
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
// 4) Fallback to old magnet if none was provided
let finalPlainMagnet = (updatedData.magnet || "").trim();
if (!finalPlainMagnet) {
finalPlainMagnet = oldPlainMagnet;
}
// 5) Re-encrypt if user wants private
let finalMagnet = finalPlainMagnet;
if (wantPrivate) {
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) {
oldRootId = baseEvent.id;
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)}`;
// 7) Build updated content
const contentObject = {
videoRootId: oldRootId,
version: updatedData.version ?? baseEvent.version ?? 1,
@@ -350,7 +349,7 @@ class NostrClient {
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", newD], // new share link
["d", newD],
],
content: JSON.stringify(contentObject),
};
@@ -360,7 +359,6 @@ class NostrClient {
console.log("Event content:", event.content);
}
// 8) Sign and publish the new event
try {
const signedEvent = await window.nostr.signEvent(event);
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) {
if (!pubkey) {
@@ -399,7 +397,6 @@ class NostrClient {
throw new Error("Not your event (pubkey mismatch).");
}
// If front-end didn't pass the tags array, load the full event:
let baseEvent = originalEvent;
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
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");
if (!dTag) {
throw new Error(
@@ -435,17 +431,15 @@ class NostrClient {
const oldContent = JSON.parse(baseEvent.content || "{}");
const oldVersion = oldContent.version ?? 1;
// If no root, fallback
let finalRootId = oldContent.videoRootId || null;
if (!finalRootId) {
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
}
// Build “deleted: true” overshadow event => revert current version
const contentObject = {
videoRootId: finalRootId,
version: oldVersion,
deleted: true, // mark *this version* as deleted
deleted: true,
isPrivate: oldContent.isPrivate ?? false,
title: oldContent.title || "",
magnet: "",
@@ -460,7 +454,7 @@ class NostrClient {
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", existingD], // re-use same d => overshadow
["d", existingD],
],
content: JSON.stringify(contentObject),
};
@@ -471,9 +465,7 @@ class NostrClient {
try {
await this.pool.publish([url], signedEvent);
} catch (err) {
if (isDevMode) {
console.error(`Failed to revert on ${url}`, err);
}
if (isDevMode) 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) {
if (!pubkey) {
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 = [];
for (const [id, vid] of this.allEvents.entries()) {
if (
@@ -501,19 +491,13 @@ class NostrClient {
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) {
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) {
await this.revertVideo(
{
// re-using revertVideo logic
id: vid.id,
pubkey: vid.pubkey,
content: JSON.stringify({
@@ -531,15 +515,11 @@ class NostrClient {
pubkey
);
}
// Optionally return some status
return true;
}
/**
* Subscribes to *all* video events. We store them in this.allEvents so older
* notes remain accessible by ID, plus we maintain this.activeMap for the newest
* version of each root (or fallback).
* subscribeVideos => old approach
*/
subscribeVideos(onVideo) {
const filter = {
@@ -553,37 +533,28 @@ class NostrClient {
}
const sub = this.pool.sub(this.relays, [filter]);
// Accumulate invalid
const invalidDuringSub = [];
sub.on("event", (event) => {
try {
const video = convertEventToVideo(event);
this.allEvents.set(event.id, video);
// If its marked deleted, remove from active map if its the active version
// NEW CODE
if (video.deleted) {
const activeKey = getActiveKey(video);
// Don't compare IDs—just remove that key from the active map
this.activeMap.delete(activeKey);
// (Optional) If you want a debug log:
// console.log(`[DELETE] Removed activeKey=${activeKey}`);
if (video.invalid) {
invalidDuringSub.push({ id: video.id, reason: video.reason });
return;
}
// normal logic here
this.allEvents.set(event.id, video);
if (video.deleted) {
const activeKey = getActiveKey(video);
this.activeMap.delete(activeKey);
return;
}
// Not deleted => see if its the newest
const activeKey = getActiveKey(video);
const prevActive = this.activeMap.get(activeKey);
if (!prevActive) {
// brand new => set it
if (!prevActive || video.created_at > prevActive.created_at) {
this.activeMap.set(activeKey, video);
onVideo(video);
} else {
// compare timestamps
if (video.created_at > prevActive.created_at) {
this.activeMap.set(activeKey, video);
onVideo(video);
}
}
} catch (err) {
if (isDevMode) {
@@ -593,6 +564,12 @@ class NostrClient {
});
sub.on("eose", () => {
if (isDevMode && invalidDuringSub.length > 0) {
console.warn(
`[subscribeVideos] found ${invalidDuringSub.length} invalid v2 notes:`,
invalidDuringSub
);
}
if (isDevMode) {
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() {
const filter = {
@@ -613,39 +590,51 @@ class NostrClient {
};
const localAll = new Map();
// NEW: track invalid
const invalidNotes = [];
try {
// 1) Fetch all events from each relay
await Promise.all(
this.relays.map(async (url) => {
const events = await this.pool.list([url], [filter]);
for (const evt of events) {
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()) {
this.allEvents.set(id, vid);
}
// 3) Rebuild activeMap
// Rebuild activeMap
this.activeMap.clear();
for (const [id, video] of this.allEvents.entries()) {
// Skip if the video is marked deleted
if (video.deleted) continue;
const activeKey = getActiveKey(video);
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) {
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(
(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) {
const local = this.allEvents.get(eventId);
if (local) {
return local;
}
// direct fetch if missing
try {
for (const url of this.relays) {
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
@@ -679,12 +667,9 @@ class NostrClient {
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() {
return Array.from(this.activeMap.values()).sort(
(a, b) => b.created_at - a.created_at

View File

@@ -1,5 +1,3 @@
// js/webtorrent.js
import WebTorrent from "./webtorrent.min.js";
export class TorrentClient {
@@ -41,7 +39,6 @@ export class TorrentClient {
return false;
};
// If it's already active, resolve immediately
if (checkActivation()) return;
registration.addEventListener("activate", () => {
@@ -59,9 +56,6 @@ export class TorrentClient {
});
}
// ------------------------------------------------------------------
// setupServiceWorker: Registers /sw.min.js at the root with scope "/"
// ------------------------------------------------------------------
async setupServiceWorker() {
try {
const isBraveBrowser = await this.isBrave();
@@ -73,31 +67,32 @@ export class TorrentClient {
throw new Error("Service Worker not supported or disabled");
}
// (Optional) Brave config check
if (isBraveBrowser) {
this.log("Checking Brave configuration...");
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) {
throw new Error("Please enable WebRTC in Brave Shield settings");
}
// Unregister any existing service workers
const registrations = await navigator.serviceWorker.getRegistrations();
for (const reg of registrations) {
await reg.unregister();
}
// Short delay to ensure old workers are removed
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...");
const registration = await navigator.serviceWorker.register("/sw.min.js", {
scope: "/",
updateViaCache: "none",
});
const registration = await navigator.serviceWorker.register(
"./sw.min.js",
{
scope: "./",
updateViaCache: "none",
}
);
this.log("Service worker registered");
if (registration.installing) {
@@ -123,7 +118,6 @@ export class TorrentClient {
await this.waitForServiceWorkerActivation(registration);
this.log("Service worker activated");
// Ensure the service worker is fully ready
const readyRegistration = await Promise.race([
navigator.serviceWorker.ready,
new Promise((_, reject) =>
@@ -138,6 +132,9 @@ export class TorrentClient {
throw new Error("Service worker not active after ready state");
}
// Force the SW to check for updates
registration.update();
this.log("Service worker ready");
return registration;
} catch (error) {
@@ -256,9 +253,11 @@ export class TorrentClient {
}
// Create the WebTorrent server with the registered service worker.
// (If you need to specify a custom URL prefix for torrent streaming,
// pass a pathPrefix option here.)
this.client.createServer({ controller: registration });
// Force the server to use '/webtorrent' as the URL prefix.
this.client.createServer({
controller: registration,
pathPrefix: "/webtorrent",
});
this.log("WebTorrent server created");
const isFirefoxBrowser = this.isFirefox();

63
src/sw.min.js vendored
View File

@@ -3,15 +3,22 @@
let cancelled = false;
// Handle skip waiting message
// Handle messages from clients
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
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
self.addEventListener("install", () => {
// Immediately install and skip waiting
self.addEventListener("install", (event) => {
self.skipWaiting();
});
@@ -21,23 +28,24 @@
Promise.all([
clients.claim(),
self.skipWaiting(),
caches.keys().then((cacheNames) =>
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
),
caches
.keys()
.then((cacheNames) =>
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
),
])
);
});
// Handle fetch events
self.addEventListener("fetch", (event) => {
const responsePromise = (() => {
const requestURL = event.request.url;
// Only handle WebTorrent streaming requests
// Since our SW is registered with scope "/" the expected URL prefix is "/webtorrent/"
if (!requestURL.includes("/webtorrent/")) {
return null;
}
const requestURL = event.request.url;
// Only handle WebTorrent streaming requests; let other requests proceed normally.
if (!requestURL.includes("/webtorrent/")) {
return;
}
const responsePromise = (async () => {
// Handle keepalive requests
if (requestURL.includes("/webtorrent/keepalive/")) {
return new Response();
@@ -57,14 +65,10 @@
// Handle streaming requests
return (async function ({ request }) {
const { url, method, headers, destination } = request;
// Get all window clients
const windowClients = await clients.matchAll({
type: "window",
includeUncontrolled: true,
});
// Create a message channel and wait for a response from a client
const [clientResponse, port] = await new Promise((resolve) => {
for (const client of windowClients) {
const channel = new MessageChannel();
@@ -92,13 +96,26 @@
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") {
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(
new ReadableStream({
pull(controller) {
@@ -128,7 +145,11 @@
closeChannel();
},
}),
clientResponse
{
status: clientResponse.status,
statusText: clientResponse.statusText,
headers: responseHeaders,
}
);
})(event);
})();