mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
update
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
:root {
|
||||
--color-bg: #0f172a;
|
||||
--color-card: #1e293b;
|
||||
--color-primary: #f43f5e;
|
||||
--color-primary: #fe0032;
|
||||
--color-secondary: #ff93a5;
|
||||
--color-text: #f8fafc;
|
||||
--color-muted: #94a3b8;
|
||||
|
@@ -45,65 +45,53 @@
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<header class="mb-8 flex items-center">
|
||||
<!-- Logo on the left -->
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
|
||||
<!-- Login Section -->
|
||||
<div id="loginSection" class="mb-8 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Buttons on the far right -->
|
||||
<div class="ml-auto flex items-center space-x-4">
|
||||
<!-- Login Button -->
|
||||
<button
|
||||
id="loginButton"
|
||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-blue-500 text-white text-sm font-bold leading-none whitespace-nowrap appearance-none hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
style="background-color: #fe0032"
|
||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full text-white text-sm font-bold leading-none whitespace-nowrap appearance-none hover:bg-[#e6002c] focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
|
||||
>
|
||||
login
|
||||
</button>
|
||||
|
||||
<!-- (Old) Logout Button is REMOVED or commented out -->
|
||||
<!-- <button
|
||||
id="logoutButton"
|
||||
...
|
||||
>logout</button>
|
||||
-->
|
||||
|
||||
<!-- Upload Button (hidden by default) -->
|
||||
<!-- Upload (Add Video) Button -->
|
||||
<button
|
||||
id="uploadButton"
|
||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-500 text-white text-xl font-bold leading-none whitespace-nowrap appearance-none hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 hidden"
|
||||
style="background-color: #fe0032"
|
||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full text-white text-xl font-bold leading-none whitespace-nowrap appearance-none hover:bg-[#e6002c] focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 hidden"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
|
||||
<!-- NEW: Profile Button (hidden by default) -->
|
||||
<!-- Profile Button -->
|
||||
<button
|
||||
id="profileButton"
|
||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-gray-600 text-white text-sm leading-none hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 hidden"
|
||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-black text-white text-sm leading-none hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black hidden"
|
||||
>
|
||||
<!-- We'll dynamically load the user's avatar into this <img> once logged in -->
|
||||
<img
|
||||
id="profileAvatar"
|
||||
src="assets/jpg/default-profile.jpg"
|
||||
alt="Profile"
|
||||
class="object-cover rounded-full w-full h-full"
|
||||
/>
|
||||
<!-- Ensures a border around the smaller avatar -->
|
||||
<div class="w-10 h-10 rounded-full overflow-hidden">
|
||||
<img
|
||||
id="profileAvatar"
|
||||
src="assets/jpg/default-profile.jpg"
|
||||
alt="Profile"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- (Optional) user status is hidden or removed -->
|
||||
<div>
|
||||
<!-- We can comment out or hide this in CSS if you do not want to show the hex key: -->
|
||||
<!-- <p id="userStatus" class="mt-4 text-gray-500 hidden">
|
||||
Logged in as: <span id="userPubKey"></span>
|
||||
</p> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- (Removed old loginSection) -->
|
||||
|
||||
<!-- Error Container -->
|
||||
<div
|
||||
@@ -121,9 +109,9 @@
|
||||
<!-- Success messages will appear here -->
|
||||
</div>
|
||||
|
||||
<!-- The main container for dynamic views -->
|
||||
<!-- Main container for dynamic views -->
|
||||
<main id="viewContainer" class="flex-grow mb-8">
|
||||
<!-- We'll load our "most-recent-videos.html" or other views here -->
|
||||
<!-- We'll load "most-recent-videos.html" or other views here -->
|
||||
</main>
|
||||
|
||||
<!-- Imported Video Player Modal (goes into modalContainer) -->
|
||||
@@ -140,7 +128,6 @@
|
||||
<div id="disclaimerModal" class="hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-scroll">
|
||||
<!-- Logo -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-dark-mode.svg"
|
||||
@@ -179,7 +166,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="space-y-6 text-gray-300">
|
||||
<p>
|
||||
bitvid is a decentralized video platform where content is shared
|
||||
@@ -233,7 +219,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Button in fixed container -->
|
||||
<div class="button-container">
|
||||
<button
|
||||
id="acceptDisclaimer"
|
||||
|
1359
src/js/app copy.js
1359
src/js/app copy.js
File diff suppressed because it is too large
Load Diff
@@ -1,608 +0,0 @@
|
||||
// js/nostr.js
|
||||
|
||||
import { isDevMode } from "./config.js";
|
||||
import { accessControl } from "./accessControl.js";
|
||||
|
||||
const RELAY_URLS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.snort.social",
|
||||
"wss://nostr.wine",
|
||||
"wss://relay.nostr.band",
|
||||
];
|
||||
|
||||
// Just a helper to keep error spam in check
|
||||
let errorLogCount = 0;
|
||||
const MAX_ERROR_LOGS = 100;
|
||||
function logErrorOnce(message, eventContent = null) {
|
||||
if (errorLogCount < MAX_ERROR_LOGS) {
|
||||
console.error(message);
|
||||
if (eventContent) {
|
||||
console.log(`Event Content: ${eventContent}`);
|
||||
}
|
||||
errorLogCount++;
|
||||
}
|
||||
if (errorLogCount === MAX_ERROR_LOGS) {
|
||||
console.error(
|
||||
"Maximum error log limit reached. Further errors will be suppressed."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example "encryption" that just reverses strings.
|
||||
* In real usage, swap with actual crypto.
|
||||
*/
|
||||
function fakeEncrypt(magnet) {
|
||||
return magnet.split("").reverse().join("");
|
||||
}
|
||||
function fakeDecrypt(encrypted) {
|
||||
return encrypted.split("").reverse().join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a raw Nostr event => your "video" object.
|
||||
*/
|
||||
function convertEventToVideo(event) {
|
||||
const content = JSON.parse(event.content || "{}");
|
||||
return {
|
||||
id: event.id,
|
||||
// We store a 'videoRootId' in content so we can group multiple edits
|
||||
videoRootId: content.videoRootId || null,
|
||||
version: content.version ?? 1,
|
||||
isPrivate: content.isPrivate ?? false,
|
||||
title: content.title || "",
|
||||
magnet: content.magnet || "",
|
||||
thumbnail: content.thumbnail || "",
|
||||
description: content.description || "",
|
||||
mode: content.mode || "live",
|
||||
deleted: content.deleted === true,
|
||||
pubkey: event.pubkey,
|
||||
created_at: event.created_at,
|
||||
tags: event.tags,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Key each "active" video by its root ID => so you only store
|
||||
* the newest version for each root. But for older events w/o videoRootId,
|
||||
* or w/o 'd' tag, we handle fallback logic below.
|
||||
*/
|
||||
function getActiveKey(video) {
|
||||
// If it has a videoRootId, we use that
|
||||
if (video.videoRootId) {
|
||||
return `ROOT:${video.videoRootId}`;
|
||||
}
|
||||
// Otherwise fallback to (pubkey + dTag) or if no dTag, fallback to event.id
|
||||
// This is a fallback approach so older events appear in the "active map".
|
||||
const dTag = video.tags?.find((t) => t[0] === "d");
|
||||
if (dTag) {
|
||||
return `${video.pubkey}:${dTag[1]}`;
|
||||
}
|
||||
return `LEGACY:${video.id}`;
|
||||
}
|
||||
|
||||
class NostrClient {
|
||||
constructor() {
|
||||
this.pool = null;
|
||||
this.pubkey = null;
|
||||
this.relays = RELAY_URLS;
|
||||
|
||||
// All events—old or new—so older share links still work
|
||||
this.allEvents = new Map();
|
||||
|
||||
// "activeMap" holds only the newest version for each root ID (or fallback).
|
||||
this.activeMap = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to all configured relays
|
||||
*/
|
||||
async init() {
|
||||
if (isDevMode) console.log("Connecting to relays...");
|
||||
|
||||
try {
|
||||
this.pool = new window.NostrTools.SimplePool();
|
||||
const results = await this.connectToRelays();
|
||||
const successfulRelays = results
|
||||
.filter((r) => r.success)
|
||||
.map((r) => r.url);
|
||||
if (successfulRelays.length === 0) throw new Error("No relays connected");
|
||||
if (isDevMode) {
|
||||
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Nostr init failed:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async connectToRelays() {
|
||||
return Promise.all(
|
||||
this.relays.map(
|
||||
(url) =>
|
||||
new Promise((resolve) => {
|
||||
const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]);
|
||||
const timeout = setTimeout(() => {
|
||||
sub.unsub();
|
||||
resolve({ url, success: false });
|
||||
}, 5000);
|
||||
|
||||
const succeed = () => {
|
||||
clearTimeout(timeout);
|
||||
sub.unsub();
|
||||
resolve({ url, success: true });
|
||||
};
|
||||
|
||||
sub.on("event", succeed);
|
||||
sub.on("eose", succeed);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt Nostr extension login or abort
|
||||
*/
|
||||
async login() {
|
||||
try {
|
||||
if (!window.nostr) {
|
||||
console.log("No Nostr extension found");
|
||||
throw new Error(
|
||||
"Please install a Nostr extension (Alby, nos2x, etc.)."
|
||||
);
|
||||
}
|
||||
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Got pubkey:", pubkey);
|
||||
console.log("Converted to npub:", npub);
|
||||
console.log("Whitelist:", accessControl.getWhitelist());
|
||||
console.log("Blacklist:", accessControl.getBlacklist());
|
||||
}
|
||||
|
||||
// Access control check
|
||||
if (!accessControl.canAccess(npub)) {
|
||||
if (accessControl.isBlacklisted(npub)) {
|
||||
throw new Error("Your account has been blocked on this platform.");
|
||||
} else {
|
||||
throw new Error("Access restricted to whitelisted users only.");
|
||||
}
|
||||
}
|
||||
|
||||
this.pubkey = pubkey;
|
||||
if (isDevMode) {
|
||||
console.log("Logged in with extension. Pubkey:", this.pubkey);
|
||||
}
|
||||
return this.pubkey;
|
||||
} catch (e) {
|
||||
console.error("Login error:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.pubkey = null;
|
||||
if (isDevMode) console.log("User logged out.");
|
||||
}
|
||||
|
||||
decodeNsec(nsec) {
|
||||
try {
|
||||
const { data } = window.NostrTools.nip19.decode(nsec);
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error("Invalid NSEC key.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a *new* video with a brand-new d tag & brand-new videoRootId
|
||||
*/
|
||||
async publishVideo(videoData, pubkey) {
|
||||
if (!pubkey) throw new Error("Not logged in to publish video.");
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Publishing new video with data:", videoData);
|
||||
}
|
||||
|
||||
let finalMagnet = videoData.magnet;
|
||||
if (videoData.isPrivate) {
|
||||
finalMagnet = fakeEncrypt(finalMagnet);
|
||||
}
|
||||
|
||||
// new "videoRootId" ensures all future edits know they're from the same root
|
||||
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
const contentObject = {
|
||||
videoRootId,
|
||||
version: videoData.version ?? 1,
|
||||
deleted: false,
|
||||
isPrivate: videoData.isPrivate ?? false,
|
||||
title: videoData.title || "",
|
||||
magnet: finalMagnet,
|
||||
thumbnail: videoData.thumbnail || "",
|
||||
description: videoData.description || "",
|
||||
mode: videoData.mode || "live",
|
||||
};
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", dTagValue],
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Publish event with brand-new root:", videoRootId);
|
||||
console.log("Event content:", event.content);
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
if (isDevMode) console.log("Signed event:", signedEvent);
|
||||
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
if (isDevMode) console.log(`Video published to ${url}`);
|
||||
} catch (err) {
|
||||
if (isDevMode) console.error(`Failed to publish: ${url}`, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return signedEvent;
|
||||
} catch (err) {
|
||||
if (isDevMode) console.error("Failed to sign/publish:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits a video by creating a *new event* with a brand-new d tag,
|
||||
* but reuses the same videoRootId as the original.
|
||||
* => old link remains pinned to the old event, new link is a fresh ID.
|
||||
*/
|
||||
async editVideo(originalVideo, updatedData, pubkey) {
|
||||
if (!pubkey) throw new Error("Not logged in to edit.");
|
||||
if (originalVideo.pubkey !== pubkey) {
|
||||
throw new Error("You do not own this video (different pubkey).");
|
||||
}
|
||||
|
||||
// Use the videoRootId from the originalVideo directly
|
||||
const rootId = originalVideo.videoRootId || null;
|
||||
|
||||
// Determine if the original magnet was encrypted and decrypt if necessary
|
||||
let oldPlainMagnet = originalVideo.magnet || "";
|
||||
if (originalVideo.isPrivate && oldPlainMagnet) {
|
||||
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
||||
}
|
||||
|
||||
// Determine new privacy setting and magnet
|
||||
const wantPrivate =
|
||||
updatedData.isPrivate ?? originalVideo.isPrivate ?? false;
|
||||
let finalPlainMagnet = (updatedData.magnet || "").trim();
|
||||
if (!finalPlainMagnet) {
|
||||
finalPlainMagnet = oldPlainMagnet; // Fallback to original if not provided
|
||||
}
|
||||
|
||||
let finalMagnet = finalPlainMagnet;
|
||||
if (wantPrivate) {
|
||||
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
||||
}
|
||||
|
||||
// Use existing rootId or generate a new one (if original lacked it)
|
||||
const newRootId =
|
||||
rootId || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
// Construct the new content object using originalVideo's properties where applicable
|
||||
const contentObject = {
|
||||
videoRootId: newRootId,
|
||||
version: updatedData.version ?? originalVideo.version ?? 1,
|
||||
deleted: false,
|
||||
isPrivate: wantPrivate,
|
||||
title: updatedData.title ?? originalVideo.title,
|
||||
magnet: finalMagnet,
|
||||
thumbnail: updatedData.thumbnail ?? originalVideo.thumbnail,
|
||||
description: updatedData.description ?? originalVideo.description,
|
||||
mode: updatedData.mode ?? originalVideo.mode ?? "live",
|
||||
};
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", newD], // New dTag for the edit
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Creating edited event with root ID:", newRootId);
|
||||
console.log("Event content:", event.content);
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
} catch (err) {
|
||||
if (isDevMode) console.error(`Publish failed to ${url}`, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
return signedEvent;
|
||||
} catch (err) {
|
||||
console.error("Edit failed:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Deleting" => we just mark content as {deleted:true} and blank out magnet/desc
|
||||
*/
|
||||
async deleteVideo(originalEvent, pubkey) {
|
||||
if (!pubkey) {
|
||||
throw new Error("Not logged in to delete.");
|
||||
}
|
||||
if (originalEvent.pubkey !== pubkey) {
|
||||
throw new Error("Not your event (pubkey mismatch).");
|
||||
}
|
||||
|
||||
// If front-end didn't pass the tags array, load the full event from local or from the relay:
|
||||
let baseEvent = originalEvent;
|
||||
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
|
||||
const fetched = await this.getEventById(originalEvent.id);
|
||||
if (!fetched) {
|
||||
throw new Error("Could not fetch the original event for deletion.");
|
||||
}
|
||||
// Rebuild baseEvent as a raw Nostr event that includes .tags and .content
|
||||
baseEvent = {
|
||||
id: fetched.id,
|
||||
pubkey: fetched.pubkey,
|
||||
// put the raw JSON content back into string form:
|
||||
content: JSON.stringify({
|
||||
version: fetched.version,
|
||||
deleted: fetched.deleted,
|
||||
isPrivate: fetched.isPrivate,
|
||||
title: fetched.title,
|
||||
magnet: fetched.magnet,
|
||||
thumbnail: fetched.thumbnail,
|
||||
description: fetched.description,
|
||||
mode: fetched.mode,
|
||||
}),
|
||||
tags: fetched.tags,
|
||||
};
|
||||
}
|
||||
|
||||
// Now try to get the old d-tag
|
||||
const dTag = baseEvent.tags.find((t) => t[0] === "d");
|
||||
if (!dTag) {
|
||||
throw new Error('No "d" tag => cannot delete addressable kind=30078.');
|
||||
}
|
||||
const existingD = dTag[1];
|
||||
|
||||
// Parse out the old content so we can preserve version, isPrivate, etc.
|
||||
const oldContent = JSON.parse(baseEvent.content || "{}");
|
||||
const oldVersion = oldContent.version ?? 1;
|
||||
|
||||
// Mark it "deleted" and clear out magnet, thumbnail, etc.
|
||||
const contentObject = {
|
||||
version: oldVersion,
|
||||
deleted: true,
|
||||
isPrivate: oldContent.isPrivate ?? false,
|
||||
title: oldContent.title || "",
|
||||
magnet: "",
|
||||
thumbnail: "",
|
||||
description: "Video was deleted by creator.",
|
||||
mode: oldContent.mode || "live",
|
||||
};
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
// We reuse the same d => overshadow the original event
|
||||
["d", existingD],
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Deleting video => mark 'deleted:true'.", event.content);
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
if (isDevMode) {
|
||||
console.log("Signed deleted event:", signedEvent);
|
||||
}
|
||||
|
||||
// Publish everywhere
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
if (isDevMode) {
|
||||
console.log(`Delete event published to ${url}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error(`Failed to publish deleted event to ${url}:`, err);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return signedEvent;
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error("Failed to sign deleted event:", err);
|
||||
}
|
||||
throw new Error("Failed to sign deleted event.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to *all* video events. We store them in this.allEvents so older
|
||||
* notes remain accessible by ID, plus we maintain this.activeMap for the newest
|
||||
* version of each root (or fallback).
|
||||
*/
|
||||
subscribeVideos(onVideo) {
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
"#t": ["video"],
|
||||
limit: 500,
|
||||
since: 0,
|
||||
};
|
||||
if (isDevMode) {
|
||||
console.log("[subscribeVideos] Subscribing with filter:", filter);
|
||||
}
|
||||
|
||||
const sub = this.pool.sub(this.relays, [filter]);
|
||||
sub.on("event", (event) => {
|
||||
try {
|
||||
const video = convertEventToVideo(event);
|
||||
this.allEvents.set(event.id, video);
|
||||
|
||||
// If it’s marked deleted, remove from active map if it’s the active version
|
||||
if (video.deleted) {
|
||||
const activeKey = getActiveKey(video);
|
||||
const existing = this.activeMap.get(activeKey);
|
||||
if (existing && existing.id === video.id) {
|
||||
this.activeMap.delete(activeKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not deleted => see if it’s the newest
|
||||
const activeKey = getActiveKey(video);
|
||||
const prevActive = this.activeMap.get(activeKey);
|
||||
if (!prevActive) {
|
||||
// brand new => set it
|
||||
this.activeMap.set(activeKey, video);
|
||||
onVideo(video);
|
||||
} else {
|
||||
// compare timestamps
|
||||
if (video.created_at > prevActive.created_at) {
|
||||
this.activeMap.set(activeKey, video);
|
||||
onVideo(video);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error("[subscribeVideos] Error processing event:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sub.on("eose", () => {
|
||||
if (isDevMode) {
|
||||
console.log("[subscribeVideos] Reached EOSE for all relays");
|
||||
}
|
||||
});
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk fetch from all relays, store in allEvents, rebuild activeMap
|
||||
*/
|
||||
async fetchVideos() {
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
"#t": ["video"],
|
||||
limit: 300,
|
||||
since: 0,
|
||||
};
|
||||
|
||||
const localAll = new Map();
|
||||
try {
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
const events = await this.pool.list([url], [filter]);
|
||||
for (const evt of events) {
|
||||
const vid = convertEventToVideo(evt);
|
||||
localAll.set(evt.id, vid);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Merge into this.allEvents
|
||||
for (const [id, vid] of localAll.entries()) {
|
||||
this.allEvents.set(id, vid);
|
||||
}
|
||||
|
||||
// Rebuild activeMap
|
||||
this.activeMap.clear();
|
||||
for (const [id, video] of this.allEvents.entries()) {
|
||||
if (video.deleted) continue;
|
||||
const activeKey = getActiveKey(video);
|
||||
const existing = this.activeMap.get(activeKey);
|
||||
if (!existing || video.created_at > existing.created_at) {
|
||||
this.activeMap.set(activeKey, video);
|
||||
}
|
||||
}
|
||||
|
||||
// Return an array of newest for each root
|
||||
const activeVideos = Array.from(this.activeMap.values()).sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
);
|
||||
return activeVideos;
|
||||
} catch (err) {
|
||||
console.error("fetchVideos error:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to fetch an event by ID from local cache, then from the relays
|
||||
*/
|
||||
async getEventById(eventId) {
|
||||
const local = this.allEvents.get(eventId);
|
||||
if (local) {
|
||||
return local;
|
||||
}
|
||||
// direct fetch if missing
|
||||
try {
|
||||
for (const url of this.relays) {
|
||||
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
|
||||
if (maybeEvt && maybeEvt.id === eventId) {
|
||||
const video = convertEventToVideo(maybeEvt);
|
||||
this.allEvents.set(eventId, video);
|
||||
return video;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error("getEventById direct fetch error:", err);
|
||||
}
|
||||
}
|
||||
return null; // not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Return newest versions from activeMap if you want to skip older events
|
||||
*/
|
||||
getActiveVideos() {
|
||||
return Array.from(this.activeMap.values()).sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const nostrClient = new NostrClient();
|
@@ -1,684 +0,0 @@
|
||||
// js/nostr.js
|
||||
|
||||
import { isDevMode } from "./config.js";
|
||||
import { accessControl } from "./accessControl.js";
|
||||
|
||||
const RELAY_URLS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.snort.social",
|
||||
"wss://nostr.wine",
|
||||
"wss://relay.nostr.band",
|
||||
];
|
||||
|
||||
// Rate limiting for error logs
|
||||
let errorLogCount = 0;
|
||||
const MAX_ERROR_LOGS = 100; // Adjust as needed
|
||||
|
||||
function logErrorOnce(message, eventContent = null) {
|
||||
if (errorLogCount < MAX_ERROR_LOGS) {
|
||||
console.error(message);
|
||||
if (eventContent) {
|
||||
console.log(`Event Content: ${eventContent}`);
|
||||
}
|
||||
errorLogCount++;
|
||||
}
|
||||
if (errorLogCount === MAX_ERROR_LOGS) {
|
||||
console.error(
|
||||
"Maximum error log limit reached. Further errors will be suppressed."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A very naive "encryption" function that just reverses the string.
|
||||
* In a real app, use a proper crypto library (AES-GCM, ECDH, etc.).
|
||||
*/
|
||||
function fakeEncrypt(magnet) {
|
||||
return magnet.split("").reverse().join("");
|
||||
}
|
||||
|
||||
function fakeDecrypt(encrypted) {
|
||||
return encrypted.split("").reverse().join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a raw Nostr event into your "video" object.
|
||||
*/
|
||||
function convertEventToVideo(event) {
|
||||
const content = JSON.parse(event.content || "{}");
|
||||
return {
|
||||
id: event.id,
|
||||
version: content.version ?? 1,
|
||||
isPrivate: content.isPrivate ?? false,
|
||||
title: content.title || "",
|
||||
magnet: content.magnet || "",
|
||||
thumbnail: content.thumbnail || "",
|
||||
description: content.description || "",
|
||||
mode: content.mode || "live",
|
||||
deleted: content.deleted === true,
|
||||
pubkey: event.pubkey,
|
||||
created_at: event.created_at,
|
||||
tags: event.tags,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a combined key for (pubkey, dTagValue).
|
||||
* If there's no `d` tag, we fallback to a special key so
|
||||
* those older events still appear in the grid.
|
||||
*/
|
||||
function getPubkeyDKey(evt) {
|
||||
const dTag = evt.tags.find((t) => t[0] === "d");
|
||||
if (dTag) {
|
||||
return `${evt.pubkey}:${dTag[1]}`;
|
||||
} else {
|
||||
// NEW: older events didn't have a d-tag, so use an alternative key
|
||||
// Example: "npubXYZ:no-d:id-of-event"
|
||||
return `${evt.pubkey}:no-d:${evt.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
class NostrClient {
|
||||
constructor() {
|
||||
this.pool = null;
|
||||
this.pubkey = null;
|
||||
this.relays = RELAY_URLS;
|
||||
|
||||
// All events, old or new, keyed by event.id
|
||||
this.allEvents = new Map();
|
||||
|
||||
// Only the "active" (non-deleted) newest version per (pubkey + dTag OR fallback)
|
||||
this.activeMap = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Nostr client by connecting to relays.
|
||||
*/
|
||||
async init() {
|
||||
if (isDevMode) console.log("Connecting to relays...");
|
||||
|
||||
try {
|
||||
this.pool = new window.NostrTools.SimplePool();
|
||||
const results = await this.connectToRelays();
|
||||
const successfulRelays = results
|
||||
.filter((r) => r.success)
|
||||
.map((r) => r.url);
|
||||
|
||||
if (successfulRelays.length === 0) throw new Error("No relays connected");
|
||||
if (isDevMode) {
|
||||
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Nostr init failed:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Connects to each relay, ensuring they're alive
|
||||
async connectToRelays() {
|
||||
return Promise.all(
|
||||
this.relays.map(
|
||||
(url) =>
|
||||
new Promise((resolve) => {
|
||||
const sub = this.pool.sub([url], [{ kinds: [0], limit: 1 }]);
|
||||
const timeout = setTimeout(() => {
|
||||
sub.unsub();
|
||||
resolve({ url, success: false });
|
||||
}, 5000);
|
||||
|
||||
const succeed = () => {
|
||||
clearTimeout(timeout);
|
||||
sub.unsub();
|
||||
resolve({ url, success: true });
|
||||
};
|
||||
|
||||
sub.on("event", succeed);
|
||||
sub.on("eose", succeed);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in the user using a Nostr extension or by entering an NSEC key.
|
||||
*/
|
||||
async login() {
|
||||
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 npub = window.NostrTools.nip19.npubEncode(pubkey);
|
||||
|
||||
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;
|
||||
if (isDevMode) {
|
||||
console.log(
|
||||
"Successfully logged in with extension. Public key:",
|
||||
this.pubkey
|
||||
);
|
||||
}
|
||||
return this.pubkey;
|
||||
} catch (e) {
|
||||
console.error("Login error:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.pubkey = null;
|
||||
if (isDevMode) console.log("User logged out.");
|
||||
}
|
||||
|
||||
decodeNsec(nsec) {
|
||||
try {
|
||||
const { data } = window.NostrTools.nip19.decode(nsec);
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error("Invalid NSEC key.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a new video event to all relays (creates a brand-new note).
|
||||
*/
|
||||
async publishVideo(videoData, pubkey) {
|
||||
if (!pubkey) {
|
||||
throw new Error("User is not logged in.");
|
||||
}
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Publishing video with data:", videoData);
|
||||
}
|
||||
|
||||
// If user sets "isPrivate = true", encrypt the magnet
|
||||
let finalMagnet = videoData.magnet;
|
||||
if (videoData.isPrivate === true) {
|
||||
finalMagnet = fakeEncrypt(finalMagnet);
|
||||
}
|
||||
|
||||
const version = videoData.version ?? 1;
|
||||
const uniqueD = `${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 10)}`;
|
||||
const contentObject = {
|
||||
version,
|
||||
deleted: false,
|
||||
isPrivate: videoData.isPrivate || false,
|
||||
title: videoData.title,
|
||||
magnet: finalMagnet,
|
||||
thumbnail: videoData.thumbnail,
|
||||
description: videoData.description,
|
||||
mode: videoData.mode,
|
||||
};
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", uniqueD],
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Event content after stringify:", event.content);
|
||||
console.log("Using d tag:", uniqueD);
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
if (isDevMode) {
|
||||
console.log("Signed event:", signedEvent);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
if (isDevMode) {
|
||||
console.log(`Event published to ${url}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error(`Failed to publish to ${url}:`, err.message);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return signedEvent;
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.error("Failed to sign event:", error.message);
|
||||
}
|
||||
throw new Error("Failed to sign event.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing video event by reusing the same "d" tag.
|
||||
* Allows toggling isPrivate on/off and re-encrypting or decrypting the magnet.
|
||||
*/
|
||||
async editVideo(originalEvent, updatedVideoData, pubkey) {
|
||||
if (!pubkey) {
|
||||
throw new Error("User is not logged in.");
|
||||
}
|
||||
if (originalEvent.pubkey !== pubkey) {
|
||||
throw new Error("You do not own this event (different pubkey).");
|
||||
}
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Editing video event:", originalEvent);
|
||||
console.log("New video data:", updatedVideoData);
|
||||
}
|
||||
|
||||
const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
|
||||
if (!dTag) {
|
||||
throw new Error('No "d" tag => cannot edit as addressable kind=30078.');
|
||||
}
|
||||
const existingD = dTag[1];
|
||||
|
||||
const oldContent = JSON.parse(originalEvent.content || "{}");
|
||||
const oldVersion = oldContent.version ?? 1;
|
||||
const oldDeleted = oldContent.deleted === true;
|
||||
const newVersion = updatedVideoData.version ?? oldVersion;
|
||||
|
||||
const oldWasPrivate = oldContent.isPrivate === true;
|
||||
let oldPlainMagnet = oldContent.magnet || "";
|
||||
if (oldWasPrivate && oldPlainMagnet) {
|
||||
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
||||
}
|
||||
|
||||
const newIsPrivate =
|
||||
typeof updatedVideoData.isPrivate === "boolean"
|
||||
? updatedVideoData.isPrivate
|
||||
: oldContent.isPrivate ?? false;
|
||||
|
||||
const userTypedMagnet = (updatedVideoData.magnet || "").trim();
|
||||
const finalPlainMagnet = userTypedMagnet || oldPlainMagnet;
|
||||
|
||||
let finalMagnet = finalPlainMagnet;
|
||||
if (newIsPrivate) {
|
||||
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
||||
}
|
||||
|
||||
const contentObject = {
|
||||
version: newVersion,
|
||||
deleted: oldDeleted,
|
||||
isPrivate: newIsPrivate,
|
||||
title: updatedVideoData.title,
|
||||
magnet: finalMagnet,
|
||||
thumbnail: updatedVideoData.thumbnail,
|
||||
description: updatedVideoData.description,
|
||||
mode: updatedVideoData.mode,
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Building updated content object:", contentObject);
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", existingD],
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Reusing d tag:", existingD);
|
||||
console.log("Updated event content:", event.content);
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
if (isDevMode) {
|
||||
console.log("Signed edited event:", signedEvent);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
if (isDevMode) {
|
||||
console.log(
|
||||
`Edited event published to ${url} (d="${existingD}")`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error(
|
||||
`Failed to publish edited event to ${url}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return signedEvent;
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.error("Failed to sign edited event:", error.message);
|
||||
}
|
||||
throw new Error("Failed to sign edited event.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete or hide an existing video by marking content as "deleted: true"
|
||||
*/
|
||||
async deleteVideo(originalEvent, pubkey) {
|
||||
if (!pubkey) {
|
||||
throw new Error("User is not logged in.");
|
||||
}
|
||||
if (originalEvent.pubkey !== pubkey) {
|
||||
throw new Error("You do not own this event (different pubkey).");
|
||||
}
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Deleting video event:", originalEvent);
|
||||
}
|
||||
|
||||
const dTag = originalEvent.tags.find((tag) => tag[0] === "d");
|
||||
if (!dTag) {
|
||||
throw new Error('No "d" tag => cannot delete addressable kind=30078.');
|
||||
}
|
||||
const existingD = dTag[1];
|
||||
|
||||
const oldContent = JSON.parse(originalEvent.content || "{}");
|
||||
const oldVersion = oldContent.version ?? 1;
|
||||
|
||||
const contentObject = {
|
||||
version: oldVersion,
|
||||
deleted: true,
|
||||
title: oldContent.title || "",
|
||||
magnet: "",
|
||||
thumbnail: "",
|
||||
description: "This video has been deleted.",
|
||||
mode: oldContent.mode || "live",
|
||||
isPrivate: oldContent.isPrivate || false,
|
||||
};
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", existingD],
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Reusing d tag for delete:", existingD);
|
||||
console.log("Deleted event content:", event.content);
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
if (isDevMode) {
|
||||
console.log("Signed deleted event:", signedEvent);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
if (isDevMode) {
|
||||
console.log(
|
||||
`Deleted event published to ${url} (d="${existingD}")`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error(`Failed to publish deleted event to ${url}:`, err);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return signedEvent;
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.error("Failed to sign deleted event:", error);
|
||||
}
|
||||
throw new Error("Failed to sign deleted event.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to all video events from all relays.
|
||||
* We store them in `allEvents` (so old IDs are still available),
|
||||
* and we also maintain `activeMap` for the newest versions of each pubkey-dKey.
|
||||
*
|
||||
* @param {Function} onVideo - Callback for each newly recognized "active" video
|
||||
*/
|
||||
subscribeVideos(onVideo) {
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
"#t": ["video"],
|
||||
limit: 500,
|
||||
since: 0, // we want from the beginning
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("[subscribeVideos] Subscribing with filter:", filter);
|
||||
}
|
||||
|
||||
const sub = this.pool.sub(this.relays, [filter]);
|
||||
|
||||
sub.on("event", (event) => {
|
||||
try {
|
||||
// Convert event => video object
|
||||
const video = convertEventToVideo(event);
|
||||
|
||||
// Always store it in allEvents
|
||||
this.allEvents.set(event.id, video);
|
||||
|
||||
// If deleted, remove from activeMap if it's the active version
|
||||
if (video.deleted) {
|
||||
const key = getPubkeyDKey(event); // might be no-d if no 'd' tag
|
||||
const activeVid = this.activeMap.get(key);
|
||||
if (activeVid && activeVid.id === event.id) {
|
||||
this.activeMap.delete(key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// It's not deleted => see if we should set it as active
|
||||
const key = getPubkeyDKey(event); // might be "npubXYZ:no-d:ID"
|
||||
const existingActive = this.activeMap.get(key);
|
||||
if (!existingActive) {
|
||||
// brand new => store it
|
||||
this.activeMap.set(key, video);
|
||||
onVideo(video);
|
||||
} else {
|
||||
// We have an active version; check timestamps
|
||||
if (video.created_at > existingActive.created_at) {
|
||||
// It's newer => overwrite
|
||||
this.activeMap.set(key, video);
|
||||
onVideo(video);
|
||||
} else {
|
||||
// It's an older event => ignore from "active" perspective
|
||||
// but still in allEvents for old links
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error("[subscribeVideos] Error parsing event:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sub.on("eose", () => {
|
||||
if (isDevMode) {
|
||||
console.log("[subscribeVideos] Reached EOSE for all relays");
|
||||
}
|
||||
// Optionally notify that the initial load is done
|
||||
});
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk fetch of videos from all relays. Then we build the `activeMap`
|
||||
* so your grid can show all old & new events (even if no 'd' tag).
|
||||
*/
|
||||
async fetchVideos() {
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
"#t": ["video"],
|
||||
// Increase limit if you want more than 300
|
||||
limit: 300,
|
||||
since: 0,
|
||||
};
|
||||
|
||||
const localAll = new Map();
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this.relays.map(async (url) => {
|
||||
const events = await this.pool.list([url], [filter]);
|
||||
for (const evt of events) {
|
||||
const video = convertEventToVideo(evt);
|
||||
localAll.set(evt.id, video);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Merge localAll into our global allEvents
|
||||
for (const [id, vid] of localAll.entries()) {
|
||||
this.allEvents.set(id, vid);
|
||||
}
|
||||
|
||||
// Re-build activeMap
|
||||
this.activeMap.clear();
|
||||
for (const [id, video] of this.allEvents.entries()) {
|
||||
if (video.deleted) continue; // skip
|
||||
const key = getPubkeyDKey({
|
||||
id,
|
||||
tags: video.tags,
|
||||
pubkey: video.pubkey,
|
||||
});
|
||||
const existing = this.activeMap.get(key);
|
||||
if (!existing || video.created_at > existing.created_at) {
|
||||
this.activeMap.set(key, video);
|
||||
}
|
||||
}
|
||||
|
||||
// Return sorted "active" array for your grid
|
||||
const activeVideos = Array.from(this.activeMap.values()).sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
);
|
||||
return activeVideos;
|
||||
} catch (err) {
|
||||
console.error("fetchVideos error:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an event by ID from our local cache (allEvents) if present.
|
||||
* If missing, do a direct pool.get() for that ID. This ensures older
|
||||
* "archived" events might still be loaded from the relays.
|
||||
*/
|
||||
async getEventById(eventId) {
|
||||
const local = this.allEvents.get(eventId);
|
||||
if (local) {
|
||||
return local;
|
||||
}
|
||||
|
||||
// NEW: do a direct fetch if not found
|
||||
try {
|
||||
for (const url of this.relays) {
|
||||
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
|
||||
if (maybeEvt && maybeEvt.id === eventId) {
|
||||
const video = convertEventToVideo(maybeEvt);
|
||||
// store in allEvents
|
||||
this.allEvents.set(eventId, video);
|
||||
return video;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
console.error("getEventById direct fetch error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// not found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the "active" videos, i.e. latest for each (pubkey+d or fallback).
|
||||
*/
|
||||
getActiveVideos() {
|
||||
return Array.from(this.activeMap.values()).sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
);
|
||||
}
|
||||
|
||||
isValidVideo(content) {
|
||||
try {
|
||||
const isValid =
|
||||
content &&
|
||||
typeof content === "object" &&
|
||||
typeof content.title === "string" &&
|
||||
content.title.length > 0 &&
|
||||
typeof content.magnet === "string" &&
|
||||
content.magnet.length > 0 &&
|
||||
typeof content.mode === "string" &&
|
||||
["dev", "live"].includes(content.mode) &&
|
||||
(typeof content.thumbnail === "string" ||
|
||||
typeof content.thumbnail === "undefined") &&
|
||||
(typeof content.description === "string" ||
|
||||
typeof content.description === "undefined");
|
||||
|
||||
if (isDevMode && !isValid) {
|
||||
console.log("Invalid video content:", content);
|
||||
}
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.error("Error validating video:", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const nostrClient = new NostrClient();
|
Reference in New Issue
Block a user