mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-10 07:58:47 +00:00
update
This commit is contained in:
@@ -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
154
src/js/accessControl.js
Normal 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();
|
@@ -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
13
src/js/lists.js
Normal 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 = [""];
|
353
src/js/nostr.js
353
src/js/nostr.js
@@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user