This commit is contained in:
Keep Creating Online
2025-01-08 22:52:25 -05:00
parent 71e95c395e
commit 1fdebf1eeb
5 changed files with 740 additions and 497 deletions

View File

@@ -406,6 +406,15 @@
<!-- Footer --> <!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4"> <footer class="mt-auto pb-8 text-center px-4">
<a
href="http://bitvid.network/"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.network
</a>
|
<a <a
href="https://bitvid.btc.us" href="https://bitvid.btc.us"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200" class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
@@ -424,14 +433,6 @@
bitvid.eth.limo bitvid.eth.limo
</a> </a>
| |
<a
href="https://bitvid.netlify.app"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.netlify.app
</a>
<div class="mt-2 space-x-4"> <div class="mt-2 space-x-4">
<a <a
href="https://github.com/PR0M3TH3AN/bitvid" href="https://github.com/PR0M3TH3AN/bitvid"
@@ -498,7 +499,10 @@
<!-- Load Nostr library --> <!-- Load Nostr library -->
<script src="js/libs/nostr.bundle.js"></script> <script src="js/libs/nostr.bundle.js"></script>
<!-- Load JavaScript Modules --> <!-- Load JavaScript Modules -->
<script src="js/libs/nostr.bundle.js"></script>
<script type="module" src="js/config.js"></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>
<script type="module" src="js/webtorrent.js"></script> <script type="module" src="js/webtorrent.js"></script>
<script type="module" src="js/nostr.js"></script> <script type="module" src="js/nostr.js"></script>
<script type="module" src="js/app.js"></script> <script type="module" src="js/app.js"></script>

154
src/js/accessControl.js Normal file
View File

@@ -0,0 +1,154 @@
// js/accessControl.js
import { isDevMode, isWhitelistEnabled } from "./config.js";
import { initialWhitelist, initialBlacklist } from "./lists.js";
class AccessControl {
constructor() {
// Debug logging for initialization
console.log("DEBUG: AccessControl constructor called");
console.log("DEBUG: initialWhitelist from import:", initialWhitelist);
console.log("DEBUG: typeof initialWhitelist:", typeof initialWhitelist);
console.log("DEBUG: initialWhitelist length:", initialWhitelist.length);
// Initialize empty sets
this.whitelist = new Set(initialWhitelist);
this.blacklist = new Set(initialBlacklist.filter((x) => x)); // Filter out empty strings
// Debug the sets
console.log("DEBUG: Whitelist after Set creation:", [...this.whitelist]);
console.log("DEBUG: Blacklist after Set creation:", [...this.blacklist]);
// Save to localStorage
this.saveWhitelist();
this.saveBlacklist();
}
// Rest of the class remains the same...
loadWhitelist() {
try {
const stored = localStorage.getItem("bitvid_whitelist");
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading whitelist:", error);
return [];
}
}
loadBlacklist() {
try {
const stored = localStorage.getItem("bitvid_blacklist");
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error("Error loading blacklist:", error);
return [];
}
}
saveWhitelist() {
try {
localStorage.setItem(
"bitvid_whitelist",
JSON.stringify([...this.whitelist])
);
} catch (error) {
console.error("Error saving whitelist:", error);
}
}
saveBlacklist() {
try {
localStorage.setItem(
"bitvid_blacklist",
JSON.stringify([...this.blacklist])
);
} catch (error) {
console.error("Error saving blacklist:", error);
}
}
addToWhitelist(npub) {
if (!this.isValidNpub(npub)) {
throw new Error("Invalid npub format");
}
this.whitelist.add(npub);
this.saveWhitelist();
if (isDevMode) console.log(`Added ${npub} to whitelist`);
}
removeFromWhitelist(npub) {
this.whitelist.delete(npub);
this.saveWhitelist();
if (isDevMode) console.log(`Removed ${npub} from whitelist`);
}
addToBlacklist(npub) {
if (!this.isValidNpub(npub)) {
throw new Error("Invalid npub format");
}
this.blacklist.add(npub);
this.saveBlacklist();
if (isDevMode) console.log(`Added ${npub} to blacklist`);
}
removeFromBlacklist(npub) {
this.blacklist.delete(npub);
this.saveBlacklist();
if (isDevMode) console.log(`Removed ${npub} from blacklist`);
}
isWhitelisted(npub) {
const result = this.whitelist.has(npub);
if (isDevMode)
console.log(
`Checking if ${npub} is whitelisted:`,
result,
"Current whitelist:",
[...this.whitelist]
);
return result;
}
isBlacklisted(npub) {
return this.blacklist.has(npub);
}
canAccess(npub) {
if (this.isBlacklisted(npub)) {
return false;
}
const canAccess = !isWhitelistEnabled || this.isWhitelisted(npub);
if (isDevMode) console.log(`Checking access for ${npub}:`, canAccess);
return canAccess;
}
filterVideos(videos) {
return videos.filter((video) => {
try {
const npub = window.NostrTools.nip19.npubEncode(video.pubkey);
return !this.isBlacklisted(npub);
} catch (error) {
console.error("Error filtering video:", error);
return false;
}
});
}
isValidNpub(npub) {
try {
return npub.startsWith("npub1") && npub.length === 63;
} catch (error) {
return false;
}
}
getWhitelist() {
return [...this.whitelist];
}
getBlacklist() {
return [...this.blacklist];
}
}
export const accessControl = new AccessControl();

View File

@@ -1,3 +1,4 @@
// js/config.js // js/config.js
export const isDevMode = true; // Set to false for production export const isDevMode = true; // Set to false for production
export const isWhitelistEnabled = true; // Set to false to allow all non-blacklisted users

13
src/js/lists.js Normal file
View File

@@ -0,0 +1,13 @@
// js/lists.js
const npubs = [
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx",
"npub1j37gc05qpqzyrmdc5vetsc9h5qtstas7tr25j0n9sdpqxghz6m4q2ej6n8",
"npub1epvnvv3kskvpnmpqgnm2atevsmdferhp7dg2s0yc7uc0hdmqmgssx09tu2",
];
console.log("DEBUG: lists.js loaded, npubs:", npubs);
export const initialWhitelist = npubs;
export const initialBlacklist = [""];

View File

@@ -1,12 +1,13 @@
// js/nostr.js // js/nostr.js
import { isDevMode } from './config.js'; import { isDevMode } from "./config.js";
import { accessControl } from "./accessControl.js";
const RELAY_URLS = [ const RELAY_URLS = [
'wss://relay.damus.io', "wss://relay.damus.io",
'wss://nos.lol', "wss://nos.lol",
'wss://relay.snort.social', "wss://relay.snort.social",
'wss://nostr.wine' "wss://nostr.wine",
]; ];
// Rate limiting for error logs // Rate limiting for error logs
@@ -22,7 +23,9 @@ function logErrorOnce(message, eventContent = null) {
errorLogCount++; errorLogCount++;
} }
if (errorLogCount === MAX_ERROR_LOGS) { if (errorLogCount === MAX_ERROR_LOGS) {
console.error('Maximum error log limit reached. Further errors will be suppressed.'); console.error(
"Maximum error log limit reached. Further errors will be suppressed."
);
} }
} }
@@ -31,10 +34,10 @@ function logErrorOnce(message, eventContent = null) {
* In a real app, use a proper crypto library (AES-GCM, ECDH, etc.). * In a real app, use a proper crypto library (AES-GCM, ECDH, etc.).
*/ */
function fakeEncrypt(magnet) { function fakeEncrypt(magnet) {
return magnet.split('').reverse().join(''); return magnet.split("").reverse().join("");
} }
function fakeDecrypt(encrypted) { function fakeDecrypt(encrypted) {
return encrypted.split('').reverse().join(''); return encrypted.split("").reverse().join("");
} }
class NostrClient { class NostrClient {
@@ -48,26 +51,31 @@ class NostrClient {
* Initializes the Nostr client by connecting to relays. * Initializes the Nostr client by connecting to relays.
*/ */
async init() { async init() {
if (isDevMode) console.log('Connecting to relays...'); if (isDevMode) console.log("Connecting to relays...");
try { try {
this.pool = new window.NostrTools.SimplePool(); this.pool = new window.NostrTools.SimplePool();
const results = await this.connectToRelays(); const results = await this.connectToRelays();
const successfulRelays = results.filter(r => r.success).map(r => r.url); 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)`); if (isDevMode)
console.log(`Connected to ${successfulRelays.length} relay(s)`);
} catch (err) { } catch (err) {
console.error('Nostr init failed:', err); console.error("Nostr init failed:", err);
throw err; throw err;
} }
} }
// Helper method to handle relay connections // Helper method to handle relay connections
async connectToRelays() { async connectToRelays() {
return Promise.all(this.relays.map(url => return Promise.all(
new Promise(resolve => { this.relays.map(
(url) =>
new Promise((resolve) => {
const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]); const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
sub.unsub(); sub.unsub();
@@ -80,40 +88,61 @@ class NostrClient {
resolve({ url, success: true }); resolve({ url, success: true });
}; };
sub.on('event', succeed); sub.on("event", succeed);
sub.on('eose', succeed); sub.on("eose", succeed);
}) })
)); )
);
} }
/** /**
* Logs in the user using a Nostr extension or by entering an NSEC key. * Logs in the user using a Nostr extension or by entering an NSEC key.
*/ */
async login() { async login() {
if (window.nostr) {
try { try {
if (!window.nostr) {
console.log("No Nostr extension found");
throw new Error(
"Please install a Nostr extension (like Alby or nos2x)."
);
}
const pubkey = await window.nostr.getPublicKey(); const pubkey = await window.nostr.getPublicKey();
const npub = window.NostrTools.nip19.npubEncode(pubkey);
// Debug logs
if (isDevMode) {
console.log("Got pubkey:", pubkey);
console.log("Converted to npub:", npub);
console.log("Whitelist:", accessControl.getWhitelist());
console.log("Blacklist:", accessControl.getBlacklist());
console.log("Is whitelisted?", accessControl.isWhitelisted(npub));
console.log("Is blacklisted?", accessControl.isBlacklisted(npub));
}
// Check access control
if (!accessControl.canAccess(npub)) {
if (accessControl.isBlacklisted(npub)) {
throw new Error(
"Your account has been blocked from accessing this platform."
);
} else {
throw new Error(
"Access is currently restricted to whitelisted users only."
);
}
}
this.pubkey = pubkey; this.pubkey = pubkey;
if (isDevMode) console.log('Logged in with extension. Public key:', this.pubkey); if (isDevMode)
console.log(
"Successfully logged in with extension. Public key:",
this.pubkey
);
return this.pubkey; return this.pubkey;
} catch (e) { } catch (e) {
if (isDevMode) console.warn('Failed to get public key from Nostr extension:', e.message); console.error("Login error:", e);
throw new Error('Failed to get public key from Nostr extension.'); throw e;
}
} else {
const nsec = prompt('Enter your NSEC key:');
if (nsec) {
try {
this.pubkey = this.decodeNsec(nsec);
if (isDevMode) console.log('Logged in with NSEC. Public key:', this.pubkey);
return this.pubkey;
} catch (error) {
if (isDevMode) console.error('Invalid NSEC key:', error.message);
throw new Error('Invalid NSEC key.');
}
} else {
throw new Error('Login cancelled or NSEC key not provided.');
}
} }
} }
@@ -122,7 +151,7 @@ class NostrClient {
*/ */
logout() { logout() {
this.pubkey = null; this.pubkey = null;
if (isDevMode) console.log('User logged out.'); if (isDevMode) console.log("User logged out.");
} }
/** /**
@@ -133,7 +162,7 @@ class NostrClient {
const { data } = window.NostrTools.nip19.decode(nsec); const { data } = window.NostrTools.nip19.decode(nsec);
return data; return data;
} catch (error) { } catch (error) {
throw new Error('Invalid NSEC key.'); throw new Error("Invalid NSEC key.");
} }
} }
@@ -142,11 +171,11 @@ class NostrClient {
*/ */
async publishVideo(videoData, pubkey) { async publishVideo(videoData, pubkey) {
if (!pubkey) { if (!pubkey) {
throw new Error('User is not logged in.'); throw new Error("User is not logged in.");
} }
if (isDevMode) { if (isDevMode) {
console.log('Publishing video with data:', videoData); console.log("Publishing video with data:", videoData);
} }
// If user sets "isPrivate = true", encrypt the magnet // If user sets "isPrivate = true", encrypt the magnet
@@ -158,7 +187,9 @@ class NostrClient {
// Default version is 1 if not specified // Default version is 1 if not specified
const version = videoData.version ?? 1; const version = videoData.version ?? 1;
const uniqueD = `${Date.now()}-${Math.random().toString(36).substring(2, 10)}`; const uniqueD = `${Date.now()}-${Math.random()
.toString(36)
.substring(2, 10)}`;
// Always mark "deleted" false for new posts // Always mark "deleted" false for new posts
const contentObject = { const contentObject = {
@@ -169,7 +200,7 @@ class NostrClient {
magnet: finalMagnet, magnet: finalMagnet,
thumbnail: videoData.thumbnail, thumbnail: videoData.thumbnail,
description: videoData.description, description: videoData.description,
mode: videoData.mode mode: videoData.mode,
}; };
const event = { const event = {
@@ -177,24 +208,25 @@ class NostrClient {
pubkey, pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
['t', 'video'], ["t", "video"],
['d', uniqueD] ["d", uniqueD],
], ],
content: JSON.stringify(contentObject) content: JSON.stringify(contentObject),
}; };
if (isDevMode) { if (isDevMode) {
console.log('Event content after stringify:', event.content); console.log("Event content after stringify:", event.content);
console.log('Using d tag:', uniqueD); console.log("Using d tag:", uniqueD);
} }
try { try {
const signedEvent = await window.nostr.signEvent(event); const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) { if (isDevMode) {
console.log('Signed event:', signedEvent); console.log("Signed event:", signedEvent);
} }
await Promise.all(this.relays.map(async url => { await Promise.all(
this.relays.map(async (url) => {
try { try {
await this.pool.publish([url], signedEvent); await this.pool.publish([url], signedEvent);
if (isDevMode) { if (isDevMode) {
@@ -205,14 +237,15 @@ class NostrClient {
console.error(`Failed to publish to ${url}:`, err.message); console.error(`Failed to publish to ${url}:`, err.message);
} }
} }
})); })
);
return signedEvent; return signedEvent;
} catch (error) { } catch (error) {
if (isDevMode) { if (isDevMode) {
console.error('Failed to sign event:', error.message); console.error("Failed to sign event:", error.message);
} }
throw new Error('Failed to sign event.'); throw new Error("Failed to sign event.");
} }
} }
@@ -223,51 +256,53 @@ class NostrClient {
// Minimal fix: ensures we only ever encrypt once per edit operation // Minimal fix: ensures we only ever encrypt once per edit operation
async editVideo(originalEvent, updatedVideoData, pubkey) { async editVideo(originalEvent, updatedVideoData, pubkey) {
if (!pubkey) { if (!pubkey) {
throw new Error('User is not logged in.'); throw new Error("User is not logged in.");
} }
if (originalEvent.pubkey !== pubkey) { if (originalEvent.pubkey !== pubkey) {
throw new Error('You do not own this event (different pubkey).'); throw new Error("You do not own this event (different pubkey).");
} }
if (isDevMode) { if (isDevMode) {
console.log('Editing video event:', originalEvent); console.log("Editing video event:", originalEvent);
console.log('New video data:', updatedVideoData); console.log("New video data:", updatedVideoData);
} }
// Grab the d tag from the original event // Grab the d tag from the original event
const dTag = originalEvent.tags.find(tag => tag[0] === 'd'); const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
if (!dTag) { if (!dTag) {
throw new Error('This event has no "d" tag, cannot edit as addressable kind=30078.'); throw new Error(
'This event has no "d" tag, cannot edit as addressable kind=30078.'
);
} }
const existingD = dTag[1]; const existingD = dTag[1];
// Parse old content // Parse old content
const oldContent = JSON.parse(originalEvent.content || '{}'); const oldContent = JSON.parse(originalEvent.content || "{}");
if (isDevMode) { if (isDevMode) {
console.log('Old content:', oldContent); console.log("Old content:", oldContent);
} }
// Keep old version & deleted status // Keep old version & deleted status
const oldVersion = oldContent.version ?? 1; const oldVersion = oldContent.version ?? 1;
const oldDeleted = (oldContent.deleted === true); const oldDeleted = oldContent.deleted === true;
const newVersion = updatedVideoData.version ?? oldVersion; const newVersion = updatedVideoData.version ?? oldVersion;
const oldWasPrivate = (oldContent.isPrivate === true); const oldWasPrivate = oldContent.isPrivate === true;
// 1) If old was private, decrypt the old magnet once => oldPlainMagnet // 1) If old was private, decrypt the old magnet once => oldPlainMagnet
let oldPlainMagnet = oldContent.magnet || ''; let oldPlainMagnet = oldContent.magnet || "";
if (oldWasPrivate && oldPlainMagnet) { if (oldWasPrivate && oldPlainMagnet) {
oldPlainMagnet = fakeDecrypt(oldPlainMagnet); oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
} }
// 2) If updatedVideoData.isPrivate is explicitly set, use that; else keep the old isPrivate // 2) If updatedVideoData.isPrivate is explicitly set, use that; else keep the old isPrivate
const newIsPrivate = const newIsPrivate =
typeof updatedVideoData.isPrivate === 'boolean' typeof updatedVideoData.isPrivate === "boolean"
? updatedVideoData.isPrivate ? updatedVideoData.isPrivate
: (oldContent.isPrivate ?? false); : oldContent.isPrivate ?? false;
// 3) The user might type a new magnet or keep oldPlainMagnet // 3) The user might type a new magnet or keep oldPlainMagnet
const userTypedMagnet = (updatedVideoData.magnet || '').trim(); const userTypedMagnet = (updatedVideoData.magnet || "").trim();
const finalPlainMagnet = userTypedMagnet || oldPlainMagnet; const finalPlainMagnet = userTypedMagnet || oldPlainMagnet;
// 4) If new is private => encrypt finalPlainMagnet once; otherwise store plaintext // 4) If new is private => encrypt finalPlainMagnet once; otherwise store plaintext
@@ -285,11 +320,11 @@ class NostrClient {
magnet: finalMagnet, magnet: finalMagnet,
thumbnail: updatedVideoData.thumbnail, thumbnail: updatedVideoData.thumbnail,
description: updatedVideoData.description, description: updatedVideoData.description,
mode: updatedVideoData.mode mode: updatedVideoData.mode,
}; };
if (isDevMode) { if (isDevMode) {
console.log('Building updated content object:', contentObject); console.log("Building updated content object:", contentObject);
} }
const event = { const event = {
@@ -297,43 +332,50 @@ class NostrClient {
pubkey, pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
['t', 'video'], ["t", "video"],
['d', existingD] ["d", existingD],
], ],
content: JSON.stringify(contentObject) content: JSON.stringify(contentObject),
}; };
if (isDevMode) { if (isDevMode) {
console.log('Reusing d tag:', existingD); console.log("Reusing d tag:", existingD);
console.log('Updated event content:', event.content); console.log("Updated event content:", event.content);
} }
try { try {
const signedEvent = await window.nostr.signEvent(event); const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) { if (isDevMode) {
console.log('Signed edited event:', signedEvent); console.log("Signed edited event:", signedEvent);
} }
// Publish to all relays // Publish to all relays
await Promise.all(this.relays.map(async url => { await Promise.all(
this.relays.map(async (url) => {
try { try {
await this.pool.publish([url], signedEvent); await this.pool.publish([url], signedEvent);
if (isDevMode) { if (isDevMode) {
console.log(`Edited event published to ${url} (d="${existingD}")`); console.log(
`Edited event published to ${url} (d="${existingD}")`
);
} }
} catch (err) { } catch (err) {
if (isDevMode) { if (isDevMode) {
console.error(`Failed to publish edited event to ${url}:`, err.message); console.error(
`Failed to publish edited event to ${url}:`,
err.message
);
} }
} }
})); })
);
return signedEvent; return signedEvent;
} catch (error) { } catch (error) {
if (isDevMode) { if (isDevMode) {
console.error('Failed to sign edited event:', error.message); console.error("Failed to sign edited event:", error.message);
} }
throw new Error('Failed to sign edited event.'); throw new Error("Failed to sign edited event.");
} }
} }
@@ -343,34 +385,36 @@ class NostrClient {
*/ */
async deleteVideo(originalEvent, pubkey) { async deleteVideo(originalEvent, pubkey) {
if (!pubkey) { if (!pubkey) {
throw new Error('User is not logged in.'); throw new Error("User is not logged in.");
} }
if (originalEvent.pubkey !== pubkey) { if (originalEvent.pubkey !== pubkey) {
throw new Error('You do not own this event (different pubkey).'); throw new Error("You do not own this event (different pubkey).");
} }
if (isDevMode) { if (isDevMode) {
console.log('Deleting video event:', originalEvent); console.log("Deleting video event:", originalEvent);
} }
const dTag = originalEvent.tags.find(tag => tag[0] === 'd'); const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
if (!dTag) { if (!dTag) {
throw new Error('This event has no "d" tag, cannot delete as addressable kind=30078.'); throw new Error(
'This event has no "d" tag, cannot delete as addressable kind=30078.'
);
} }
const existingD = dTag[1]; const existingD = dTag[1];
const oldContent = JSON.parse(originalEvent.content || '{}'); const oldContent = JSON.parse(originalEvent.content || "{}");
const oldVersion = oldContent.version ?? 1; const oldVersion = oldContent.version ?? 1;
const contentObject = { const contentObject = {
version: oldVersion, version: oldVersion,
deleted: true, deleted: true,
title: oldContent.title || '', title: oldContent.title || "",
magnet: '', magnet: "",
thumbnail: '', thumbnail: "",
description: 'This video has been deleted.', description: "This video has been deleted.",
mode: oldContent.mode || 'live', mode: oldContent.mode || "live",
isPrivate: oldContent.isPrivate || false isPrivate: oldContent.isPrivate || false,
}; };
const event = { const event = {
@@ -378,42 +422,49 @@ class NostrClient {
pubkey, pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
['t', 'video'], ["t", "video"],
['d', existingD] ["d", existingD],
], ],
content: JSON.stringify(contentObject) content: JSON.stringify(contentObject),
}; };
if (isDevMode) { if (isDevMode) {
console.log('Reusing d tag for delete:', existingD); console.log("Reusing d tag for delete:", existingD);
console.log('Deleted event content:', event.content); console.log("Deleted event content:", event.content);
} }
try { try {
const signedEvent = await window.nostr.signEvent(event); const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) { if (isDevMode) {
console.log('Signed deleted event:', signedEvent); console.log("Signed deleted event:", signedEvent);
} }
await Promise.all(this.relays.map(async url => { await Promise.all(
this.relays.map(async (url) => {
try { try {
await this.pool.publish([url], signedEvent); await this.pool.publish([url], signedEvent);
if (isDevMode) { if (isDevMode) {
console.log(`Deleted event published to ${url} (d="${existingD}")`); console.log(
`Deleted event published to ${url} (d="${existingD}")`
);
} }
} catch (err) { } catch (err) {
if (isDevMode) { if (isDevMode) {
console.error(`Failed to publish deleted event to ${url}:`, err.message); console.error(
`Failed to publish deleted event to ${url}:`,
err.message
);
} }
} }
})); })
);
return signedEvent; return signedEvent;
} catch (error) { } catch (error) {
if (isDevMode) { if (isDevMode) {
console.error('Failed to sign deleted event:', error.message); console.error("Failed to sign deleted event:", error.message);
} }
throw new Error('Failed to sign deleted event.'); throw new Error("Failed to sign deleted event.");
} }
} }
@@ -423,16 +474,16 @@ class NostrClient {
async fetchVideos() { async fetchVideos() {
const filter = { const filter = {
kinds: [30078], kinds: [30078],
'#t': ['video'], "#t": ["video"],
limit: 1000, limit: 1000,
since: 0 since: 0,
}; };
const videoEvents = new Map(); const videoEvents = new Map();
if (isDevMode) { if (isDevMode) {
console.log('[fetchVideos] Starting fetch from all relays...'); console.log("[fetchVideos] Starting fetch from all relays...");
console.log('[fetchVideos] Filter:', filter); console.log("[fetchVideos] Filter:", filter);
} }
try { try {
@@ -454,7 +505,7 @@ class NostrClient {
} }
} }
events.forEach(event => { events.forEach((event) => {
try { try {
const content = JSON.parse(event.content); const content = JSON.parse(event.content);
@@ -470,42 +521,57 @@ class NostrClient {
id: event.id, id: event.id,
version: content.version ?? 1, version: content.version ?? 1,
isPrivate: content.isPrivate ?? false, isPrivate: content.isPrivate ?? false,
title: content.title || '', title: content.title || "",
magnet: content.magnet || '', magnet: content.magnet || "",
thumbnail: content.thumbnail || '', thumbnail: content.thumbnail || "",
description: content.description || '', description: content.description || "",
mode: content.mode || 'live', mode: content.mode || "live",
pubkey: event.pubkey, pubkey: event.pubkey,
created_at: event.created_at, created_at: event.created_at,
tags: event.tags tags: event.tags,
}); });
} }
} catch (parseError) { } catch (parseError) {
if (isDevMode) { if (isDevMode) {
console.error('[fetchVideos] Event parsing error:', parseError); console.error(
"[fetchVideos] Event parsing error:",
parseError
);
} }
} }
}); });
} catch (relayError) { } catch (relayError) {
if (isDevMode) { if (isDevMode) {
console.error(`[fetchVideos] Error fetching from ${url}:`, relayError); console.error(
`[fetchVideos] Error fetching from ${url}:`,
relayError
);
} }
} }
}) })
); );
const videos = Array.from(videoEvents.values()) const videos = Array.from(videoEvents.values()).sort(
.sort((a, b) => b.created_at - a.created_at); (a, b) => b.created_at - a.created_at
);
// Apply access control filtering
const filteredVideos = accessControl.filterVideos(videos);
if (isDevMode) { if (isDevMode) {
console.log('[fetchVideos] All relays have responded.'); console.log("[fetchVideos] All relays have responded.");
console.log(`[fetchVideos] Total unique video events: ${videoEvents.size}`); console.log(
`[fetchVideos] Total unique video events: ${videoEvents.size}`
);
console.log(
`[fetchVideos] Videos after filtering: ${filteredVideos.length}`
);
} }
return videos; return filteredVideos;
} catch (error) { } catch (error) {
if (isDevMode) { if (isDevMode) {
console.error('FETCH VIDEOS ERROR:', error); console.error("FETCH VIDEOS ERROR:", error);
} }
return []; return [];
} }
@@ -516,34 +582,39 @@ class NostrClient {
*/ */
isValidVideo(content) { isValidVideo(content) {
try { try {
const isValid = ( const isValid =
content && content &&
typeof content === 'object' && typeof content === "object" &&
typeof content.title === 'string' && typeof content.title === "string" &&
content.title.length > 0 && content.title.length > 0 &&
typeof content.magnet === 'string' && typeof content.magnet === "string" &&
content.magnet.length > 0 && content.magnet.length > 0 &&
typeof content.mode === 'string' && typeof content.mode === "string" &&
['dev', 'live'].includes(content.mode) && ["dev", "live"].includes(content.mode) &&
(typeof content.thumbnail === 'string' || typeof content.thumbnail === 'undefined') && (typeof content.thumbnail === "string" ||
(typeof content.description === 'string' || typeof content.description === 'undefined') typeof content.thumbnail === "undefined") &&
); (typeof content.description === "string" ||
typeof content.description === "undefined");
if (isDevMode && !isValid) { if (isDevMode && !isValid) {
console.log('Invalid video content:', content); console.log("Invalid video content:", content);
console.log('Validation details:', { console.log("Validation details:", {
hasTitle: typeof content.title === 'string', hasTitle: typeof content.title === "string",
hasMagnet: typeof content.magnet === 'string', hasMagnet: typeof content.magnet === "string",
hasMode: typeof content.mode === 'string', hasMode: typeof content.mode === "string",
validThumbnail: typeof content.thumbnail === 'string' || typeof content.thumbnail === 'undefined', validThumbnail:
validDescription: typeof content.description === 'string' || typeof content.description === 'undefined' typeof content.thumbnail === "string" ||
typeof content.thumbnail === "undefined",
validDescription:
typeof content.description === "string" ||
typeof content.description === "undefined",
}); });
} }
return isValid; return isValid;
} catch (error) { } catch (error) {
if (isDevMode) { if (isDevMode) {
console.error('Error validating video:', error); console.error("Error validating video:", error);
} }
return false; return false;
} }