mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-07 06:38:40 +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="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)"
|
||||
|
@@ -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)"
|
||||
|
@@ -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)"
|
||||
|
@@ -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)"
|
||||
|
@@ -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)"
|
||||
|
@@ -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>
|
||||
|
@@ -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)) {
|
||||
|
@@ -10,6 +10,7 @@ const npubs = [
|
||||
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
|
||||
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
|
||||
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
|
||||
"npub13qexjtmajssuhz8gdchgx65dwsnr705drse294zz5vt4e78ya2vqzyg8lv", // SatoshiSignal
|
||||
];
|
||||
|
||||
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 { 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 it’s marked deleted, remove from active map if it’s 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 it’s 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
|
||||
|
@@ -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
63
src/sw.min.js
vendored
@@ -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);
|
||||
})();
|
||||
|
Reference in New Issue
Block a user