mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
added better black list and event blocking, better and more reliable edit and delete functions
This commit is contained in:
113
src/js/app.js
113
src/js/app.js
@@ -5,6 +5,7 @@ import { nostrClient } from "./nostr.js";
|
||||
import { torrentClient } from "./webtorrent.js";
|
||||
import { isDevMode } from "./config.js";
|
||||
import { disclaimerModal } from "./disclaimer.js";
|
||||
import { initialBlacklist, initialEventBlacklist } from "./lists.js";
|
||||
|
||||
/**
|
||||
* Simple "decryption" placeholder for private videos.
|
||||
@@ -82,6 +83,27 @@ class bitvidApp {
|
||||
// NEW: reference to the login modal's close button
|
||||
this.closeLoginModalBtn =
|
||||
document.getElementById("closeLoginModal") || null;
|
||||
|
||||
// Build a set of blacklisted event IDs (hex) from nevent strings, skipping empties
|
||||
this.blacklistedEventIds = new Set();
|
||||
for (const neventStr of initialEventBlacklist) {
|
||||
// Skip any empty or obviously invalid strings
|
||||
if (!neventStr || neventStr.trim().length < 8) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const decoded = window.NostrTools.nip19.decode(neventStr);
|
||||
if (decoded.type === "nevent" && decoded.data.id) {
|
||||
this.blacklistedEventIds.add(decoded.data.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[bitvidApp] Invalid nevent in blacklist:",
|
||||
neventStr,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forceRefreshAllProfiles() {
|
||||
@@ -700,53 +722,58 @@ class bitvidApp {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to new videos & render them.
|
||||
* Subscribe to videos (older + new) and render them as they come in.
|
||||
*/
|
||||
// js/app.js
|
||||
|
||||
async loadVideos() {
|
||||
console.log("Starting loadVideos...");
|
||||
|
||||
// If we already have a subscription, don’t unsubscribe/resubscribe—
|
||||
// just update the UI from local cache.
|
||||
// We do NOT decode initialEventBlacklist here.
|
||||
// That happens once in the constructor, creating this.blacklistedEventIds.
|
||||
|
||||
if (!this.videoSubscription) {
|
||||
// First-time load: show “Loading...” message
|
||||
if (this.videoList) {
|
||||
this.videoList.innerHTML = `
|
||||
<p class="text-center text-gray-500">
|
||||
Loading videos...
|
||||
Loading videos as they arrive...
|
||||
</p>`;
|
||||
}
|
||||
|
||||
// 1) Do a bulk fetch once
|
||||
try {
|
||||
await nostrClient.fetchVideos();
|
||||
} catch (err) {
|
||||
console.error("Could not load videos initially:", err);
|
||||
this.showError("Could not load videos from relays.");
|
||||
if (this.videoList) {
|
||||
this.videoList.innerHTML = `
|
||||
<p class="text-center text-gray-500">
|
||||
No videos available at this time.
|
||||
</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Render the newest set after the fetch
|
||||
const newestActive = nostrClient.getActiveVideos();
|
||||
this.renderVideoList(newestActive);
|
||||
|
||||
// 3) Create a single subscription that updates our UI
|
||||
// Create a single subscription
|
||||
this.videoSubscription = nostrClient.subscribeVideos(() => {
|
||||
// Each time a new/updated event arrives, we just re-render from local
|
||||
const updatedAll = nostrClient.getActiveVideos();
|
||||
this.renderVideoList(updatedAll);
|
||||
|
||||
// Filter out blacklisted authors & blacklisted event IDs
|
||||
const filteredVideos = updatedAll.filter((video) => {
|
||||
// 1) If the event ID is in our blacklisted set, skip
|
||||
if (this.blacklistedEventIds.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) Check author (if you’re also blacklisting authors by npub)
|
||||
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||
if (initialBlacklist.includes(authorNpub)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.renderVideoList(filteredVideos);
|
||||
});
|
||||
} else {
|
||||
// If we’ve already subscribed before, just update from cache
|
||||
// Already subscribed: just show what's cached
|
||||
const allCached = nostrClient.getActiveVideos();
|
||||
this.renderVideoList(allCached);
|
||||
|
||||
const filteredCached = allCached.filter((video) => {
|
||||
if (this.blacklistedEventIds.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||
return !initialBlacklist.includes(authorNpub);
|
||||
});
|
||||
|
||||
this.renderVideoList(filteredCached);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1323,6 +1350,12 @@ class bitvidApp {
|
||||
* Helper to open a video by event ID (like ?v=...).
|
||||
*/
|
||||
async playVideoByEventId(eventId) {
|
||||
// First, check if this event is blacklisted
|
||||
if (this.blacklistedEventIds.has(eventId)) {
|
||||
this.showError("This content has been removed or is not allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) Check local subscription map
|
||||
let video = this.videosMap.get(eventId);
|
||||
@@ -1330,11 +1363,12 @@ class bitvidApp {
|
||||
if (!video) {
|
||||
video = await this.getOldEventById(eventId);
|
||||
}
|
||||
// 3) If still no luck, show error and return
|
||||
// 3) If still not found, show error and return
|
||||
if (!video) {
|
||||
this.showError("Video not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Decrypt magnet if private & owned
|
||||
if (
|
||||
video.isPrivate &&
|
||||
@@ -1345,15 +1379,18 @@ 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",
|
||||
@@ -1373,10 +1410,12 @@ 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)
|
||||
if (this.videoTitle) {
|
||||
this.videoTitle.textContent = video.title || "Untitled";
|
||||
}
|
||||
if (this.videoDescription) {
|
||||
this.videoDescription.textContent =
|
||||
video.description || "No description available.";
|
||||
@@ -1397,15 +1436,18 @@ class bitvidApp {
|
||||
this.creatorAvatar.src = creatorProfile.picture;
|
||||
this.creatorAvatar.alt = creatorProfile.name;
|
||||
}
|
||||
// 9) Clean up any existing torrent instance before starting a new stream.
|
||||
|
||||
// 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.
|
||||
// 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(
|
||||
cacheBustedMagnet,
|
||||
this.modalVideo
|
||||
);
|
||||
|
||||
// 11) Start intervals to update stats
|
||||
const updateInterval = setInterval(() => {
|
||||
if (!document.body.contains(this.modalVideo)) {
|
||||
@@ -1415,6 +1457,7 @@ 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)) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// js/lists.js
|
||||
|
||||
// Whitelist of npubs that can access the video upload functions
|
||||
const npubs = [
|
||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe", // bitvid
|
||||
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx", // thePR0M3TH3AN
|
||||
@@ -15,5 +16,9 @@ const npubs = [
|
||||
|
||||
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
||||
|
||||
// Blacklist of npubs that events will not be displayed in the bitvid official client
|
||||
export const initialWhitelist = npubs;
|
||||
export const initialBlacklist = [""];
|
||||
|
||||
// Block specific events with the nevent
|
||||
export const initialEventBlacklist = [""];
|
||||
|
119
src/js/nostr.js
119
src/js/nostr.js
@@ -281,59 +281,66 @@ class NostrClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits a video => old style
|
||||
* Edits a video by creating a *new event* with a brand-new d tag,
|
||||
* but reuses the same videoRootId as the original.
|
||||
*
|
||||
* This version forces version=2 for the original note and uses
|
||||
* lowercase comparison for public keys.
|
||||
*/
|
||||
async editVideo(originalEventStub, updatedData, pubkey) {
|
||||
if (!pubkey) {
|
||||
async editVideo(originalEventStub, updatedData, userPubkey) {
|
||||
if (!userPubkey) {
|
||||
throw new Error("Not logged in to edit.");
|
||||
}
|
||||
if (!originalEventStub.pubkey || originalEventStub.pubkey !== pubkey) {
|
||||
|
||||
// Convert the provided pubkey to lowercase
|
||||
const userPubkeyLower = userPubkey.toLowerCase();
|
||||
|
||||
// Use getEventById to fetch the full original event details
|
||||
const baseEvent = await this.getEventById(originalEventStub.id);
|
||||
if (!baseEvent) {
|
||||
throw new Error("Could not retrieve the original event to edit.");
|
||||
}
|
||||
|
||||
// Check that the original event is version 2 or higher
|
||||
if (baseEvent.version < 2) {
|
||||
throw new Error(
|
||||
"This video is not in the supported version for editing."
|
||||
);
|
||||
}
|
||||
|
||||
// Ownership check (compare lowercase hex public keys)
|
||||
if (
|
||||
!baseEvent.pubkey ||
|
||||
baseEvent.pubkey.toLowerCase() !== userPubkeyLower
|
||||
) {
|
||||
throw new Error("You do not own this video (pubkey mismatch).");
|
||||
}
|
||||
|
||||
let baseEvent = originalEventStub;
|
||||
if (!baseEvent.videoRootId) {
|
||||
const fetched = await this.getEventById(originalEventStub.id);
|
||||
if (!fetched) {
|
||||
throw new Error("Could not retrieve the original event to edit.");
|
||||
}
|
||||
baseEvent = fetched;
|
||||
}
|
||||
|
||||
let oldRootId = baseEvent.videoRootId || null;
|
||||
|
||||
// Decrypt old magnet if private
|
||||
// Decrypt the old magnet if the note is private
|
||||
let oldPlainMagnet = baseEvent.magnet || "";
|
||||
if (baseEvent.isPrivate && oldPlainMagnet) {
|
||||
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
||||
}
|
||||
|
||||
// Determine if the updated note should be private
|
||||
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
||||
|
||||
let finalPlainMagnet = (updatedData.magnet || "").trim();
|
||||
if (!finalPlainMagnet) {
|
||||
finalPlainMagnet = oldPlainMagnet;
|
||||
}
|
||||
let finalMagnet = finalPlainMagnet;
|
||||
if (wantPrivate) {
|
||||
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
||||
}
|
||||
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
|
||||
let finalPlainMagnet = (updatedData.magnet || "").trim() || oldPlainMagnet;
|
||||
let finalMagnet = wantPrivate
|
||||
? fakeEncrypt(finalPlainMagnet)
|
||||
: finalPlainMagnet;
|
||||
|
||||
if (!oldRootId) {
|
||||
oldRootId = baseEvent.id;
|
||||
if (isDevMode) {
|
||||
console.log(
|
||||
"No existing root => using baseEvent.id as root:",
|
||||
oldRootId
|
||||
);
|
||||
}
|
||||
}
|
||||
// Use the existing videoRootId (or fall back to the base event's ID)
|
||||
const oldRootId = baseEvent.videoRootId || baseEvent.id;
|
||||
|
||||
// Generate a new d-tag so that the edit gets its own share link
|
||||
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
// Build the updated content object
|
||||
const contentObject = {
|
||||
videoRootId: oldRootId,
|
||||
version: updatedData.version ?? baseEvent.version ?? 1,
|
||||
version: updatedData.version ?? baseEvent.version ?? 2,
|
||||
deleted: false,
|
||||
isPrivate: wantPrivate,
|
||||
title: updatedData.title ?? baseEvent.title,
|
||||
@@ -345,11 +352,12 @@ class NostrClient {
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
// Use the provided userPubkey (or you can also force it to lowercase here if desired)
|
||||
pubkey: userPubkeyLower,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", newD],
|
||||
["d", newD], // new share link tag
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
@@ -379,6 +387,7 @@ class NostrClient {
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return signedEvent;
|
||||
} catch (err) {
|
||||
console.error("Edit failed:", err);
|
||||
@@ -474,13 +483,27 @@ class NostrClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteAllVersions => old style
|
||||
* "Deleting" => Mark all content with the same videoRootId as {deleted:true}
|
||||
* and blank out magnet/desc.
|
||||
*
|
||||
* This version now asks for confirmation before proceeding.
|
||||
*/
|
||||
async deleteAllVersions(videoRootId, pubkey) {
|
||||
if (!pubkey) {
|
||||
throw new Error("Not logged in to delete all versions.");
|
||||
}
|
||||
|
||||
// Ask for confirmation before proceeding
|
||||
if (
|
||||
!window.confirm(
|
||||
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
||||
)
|
||||
) {
|
||||
console.log("Deletion cancelled by user.");
|
||||
return null; // Cancel deletion if user clicks "Cancel"
|
||||
}
|
||||
|
||||
// 1) Find all events in our local allEvents that share the same root.
|
||||
const matchingEvents = [];
|
||||
for (const [id, vid] of this.allEvents.entries()) {
|
||||
if (
|
||||
@@ -495,6 +518,8 @@ class NostrClient {
|
||||
throw new Error("No existing events found for that root.");
|
||||
}
|
||||
|
||||
// 2) For each event, create a "revert" event to mark it as deleted.
|
||||
// This will prompt the user (via the extension) to sign the deletion.
|
||||
for (const vid of matchingEvents) {
|
||||
await this.revertVideo(
|
||||
{
|
||||
@@ -515,16 +540,23 @@ class NostrClient {
|
||||
pubkey
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* subscribeVideos => old approach
|
||||
*/
|
||||
/**
|
||||
* Subscribe to *all* videos (old and new) with a single subscription,
|
||||
* then call onVideo() each time a new or updated event arrives.
|
||||
*/
|
||||
subscribeVideos(onVideo) {
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
"#t": ["video"],
|
||||
// Remove or adjust limit if you prefer,
|
||||
// and set since=0 to retrieve historical events:
|
||||
limit: 500,
|
||||
since: 0,
|
||||
};
|
||||
@@ -533,7 +565,6 @@ class NostrClient {
|
||||
}
|
||||
|
||||
const sub = this.pool.sub(this.relays, [filter]);
|
||||
// Accumulate invalid
|
||||
const invalidDuringSub = [];
|
||||
|
||||
sub.on("event", (event) => {
|
||||
@@ -543,18 +574,22 @@ class NostrClient {
|
||||
invalidDuringSub.push({ id: video.id, reason: video.reason });
|
||||
return;
|
||||
}
|
||||
// normal logic here
|
||||
// Store in allEvents
|
||||
this.allEvents.set(event.id, video);
|
||||
|
||||
// If it's a "deleted" note, remove from activeMap
|
||||
if (video.deleted) {
|
||||
const activeKey = getActiveKey(video);
|
||||
this.activeMap.delete(activeKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, if it's newer than what we have, update activeMap
|
||||
const activeKey = getActiveKey(video);
|
||||
const prevActive = this.activeMap.get(activeKey);
|
||||
if (!prevActive || video.created_at > prevActive.created_at) {
|
||||
this.activeMap.set(activeKey, video);
|
||||
onVideo(video);
|
||||
onVideo(video); // trigger the callback that re-renders
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
@@ -571,7 +606,9 @@ class NostrClient {
|
||||
);
|
||||
}
|
||||
if (isDevMode) {
|
||||
console.log("[subscribeVideos] Reached EOSE for all relays");
|
||||
console.log(
|
||||
"[subscribeVideos] Reached EOSE for all relays (historical load done)"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user