added better black list and event blocking, better and more reliable edit and delete functions

This commit is contained in:
Keep Creating Online
2025-02-04 10:48:50 -05:00
parent 80d90387b8
commit 380a4e89ce
3 changed files with 161 additions and 76 deletions

View File

@@ -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, dont 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 youre 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 weve 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)) {

View File

@@ -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 = [""];

View File

@@ -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)"
);
}
});