From 380a4e89ce15d30ae7a092ff97238a8d558b6953 Mon Sep 17 00:00:00 2001 From: Keep Creating Online <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:48:50 -0500 Subject: [PATCH] added better black list and event blocking, better and more reliable edit and delete functions --- src/js/app.js | 113 +++++++++++++++++++++++++++++++-------------- src/js/lists.js | 5 ++ src/js/nostr.js | 119 +++++++++++++++++++++++++++++++----------------- 3 files changed, 161 insertions(+), 76 deletions(-) diff --git a/src/js/app.js b/src/js/app.js index 75dddce..26a833b 100644 --- a/src/js/app.js +++ b/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 = `

- Loading videos... + Loading videos as they arrive...

`; } - // 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 = ` -

- No videos available at this time. -

`; - } - 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)) { diff --git a/src/js/lists.js b/src/js/lists.js index cdf5da1..5242a1a 100644 --- a/src/js/lists.js +++ b/src/js/lists.js @@ -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 = [""]; diff --git a/src/js/nostr.js b/src/js/nostr.js index 8cdd94a..c78ff60 100644 --- a/src/js/nostr.js +++ b/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)" + ); } });