mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
752 lines
21 KiB
JavaScript
752 lines
21 KiB
JavaScript
// js/nostr.js
|
|
|
|
import { isDevMode } from "./config.js";
|
|
import { accessControl } from "./accessControl.js";
|
|
|
|
/**
|
|
* The usual relays
|
|
*/
|
|
const RELAY_URLS = [
|
|
"wss://relay.damus.io",
|
|
"wss://nos.lol",
|
|
"wss://relay.snort.social",
|
|
"wss://relay.primal.net",
|
|
"wss://relay.nostr.band",
|
|
];
|
|
|
|
// To limit error spam
|
|
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, replace 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.
|
|
* CHANGED: skip if version <2
|
|
*/
|
|
function convertEventToVideo(event) {
|
|
try {
|
|
const content = JSON.parse(event.content || "{}");
|
|
|
|
// Example checks:
|
|
const isSupportedVersion = content.version >= 2;
|
|
const hasRequiredFields = !!(content.title && content.magnet);
|
|
|
|
if (!isSupportedVersion) {
|
|
return {
|
|
id: event.id,
|
|
invalid: true,
|
|
reason: "version <2",
|
|
};
|
|
}
|
|
if (!hasRequiredFields) {
|
|
return {
|
|
id: event.id,
|
|
invalid: true,
|
|
reason: "missing title/magnet",
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: event.id,
|
|
videoRootId: content.videoRootId || event.id,
|
|
version: content.version,
|
|
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,
|
|
invalid: false,
|
|
};
|
|
} catch (err) {
|
|
// JSON parse error
|
|
return { id: event.id, invalid: true, reason: "json parse error" };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the video has videoRootId => use that as the “group key”.
|
|
* Otherwise fallback to (pubkey + dTag), or if no dTag => “LEGACY:id”
|
|
*/
|
|
function getActiveKey(video) {
|
|
if (video.videoRootId) {
|
|
return `ROOT:${video.videoRootId}`;
|
|
}
|
|
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;
|
|
|
|
// Store all events so older links still work
|
|
this.allEvents = new Map();
|
|
|
|
// “activeMap” holds only the newest version for each root
|
|
this.activeMap = new Map();
|
|
}
|
|
|
|
/**
|
|
* Connect to the 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 login with a Nostr extension
|
|
*/
|
|
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
|
|
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 (err) {
|
|
console.error("Login error:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
logout() {
|
|
this.pubkey = null;
|
|
if (isDevMode) console.log("User logged out.");
|
|
}
|
|
|
|
/**
|
|
* Publish a new video
|
|
* CHANGED: Force version=2 for all new notes
|
|
*/
|
|
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);
|
|
}
|
|
|
|
// brand-new root & d
|
|
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
|
|
const contentObject = {
|
|
videoRootId,
|
|
version: 2, // forcibly set version=2
|
|
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.
|
|
*
|
|
* This version forces version=2 for the original note and uses
|
|
* lowercase comparison for public keys.
|
|
*/
|
|
async editVideo(originalEventStub, updatedData, userPubkey) {
|
|
if (!userPubkey) {
|
|
throw new Error("Not logged in to edit.");
|
|
}
|
|
|
|
// Convert the provided pubkey to lowercase
|
|
const userPubkeyLower = userPubkey.toLowerCase();
|
|
|
|
// Use getEventById to fetch the full original event details
|
|
const baseEvent = await this.getEventById(originalEventStub.id);
|
|
if (!baseEvent) {
|
|
throw new Error("Could not retrieve the original event to edit.");
|
|
}
|
|
|
|
// Check that the original event is version 2 or higher
|
|
if (baseEvent.version < 2) {
|
|
throw new Error(
|
|
"This video is not in the supported version for editing."
|
|
);
|
|
}
|
|
|
|
// Ownership check (compare lowercase hex public keys)
|
|
if (
|
|
!baseEvent.pubkey ||
|
|
baseEvent.pubkey.toLowerCase() !== userPubkeyLower
|
|
) {
|
|
throw new Error("You do not own this video (pubkey mismatch).");
|
|
}
|
|
|
|
// Decrypt the old magnet if the note is private
|
|
let oldPlainMagnet = baseEvent.magnet || "";
|
|
if (baseEvent.isPrivate && oldPlainMagnet) {
|
|
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
|
}
|
|
|
|
// Determine if the updated note should be private
|
|
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
|
|
|
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
|
|
let finalPlainMagnet = (updatedData.magnet || "").trim() || oldPlainMagnet;
|
|
let finalMagnet = wantPrivate
|
|
? fakeEncrypt(finalPlainMagnet)
|
|
: finalPlainMagnet;
|
|
|
|
// Use the existing videoRootId (or fall back to the base event's ID)
|
|
const oldRootId = baseEvent.videoRootId || baseEvent.id;
|
|
|
|
// Generate a new d-tag so that the edit gets its own share link
|
|
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
|
|
|
// Build the updated content object
|
|
const contentObject = {
|
|
videoRootId: oldRootId,
|
|
version: updatedData.version ?? baseEvent.version ?? 2,
|
|
deleted: false,
|
|
isPrivate: wantPrivate,
|
|
title: updatedData.title ?? baseEvent.title,
|
|
magnet: finalMagnet,
|
|
thumbnail: updatedData.thumbnail ?? baseEvent.thumbnail,
|
|
description: updatedData.description ?? baseEvent.description,
|
|
mode: updatedData.mode ?? baseEvent.mode ?? "live",
|
|
};
|
|
|
|
const event = {
|
|
kind: 30078,
|
|
// Use the provided userPubkey (or you can also force it to lowercase here if desired)
|
|
pubkey: userPubkeyLower,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["t", "video"],
|
|
["d", newD], // new share link tag
|
|
],
|
|
content: JSON.stringify(contentObject),
|
|
};
|
|
|
|
if (isDevMode) {
|
|
console.log("Creating edited event with root ID:", oldRootId);
|
|
console.log("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 video published to ${url}`);
|
|
}
|
|
} catch (err) {
|
|
if (isDevMode) {
|
|
console.error(`Publish failed to ${url}`, err);
|
|
}
|
|
}
|
|
})
|
|
);
|
|
|
|
return signedEvent;
|
|
} catch (err) {
|
|
console.error("Edit failed:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* revertVideo => old style
|
|
*/
|
|
async revertVideo(originalEvent, pubkey) {
|
|
if (!pubkey) {
|
|
throw new Error("Not logged in to revert.");
|
|
}
|
|
if (originalEvent.pubkey !== pubkey) {
|
|
throw new Error("Not your event (pubkey mismatch).");
|
|
}
|
|
|
|
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 reverting.");
|
|
}
|
|
baseEvent = {
|
|
id: fetched.id,
|
|
pubkey: fetched.pubkey,
|
|
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,
|
|
};
|
|
}
|
|
|
|
const dTag = baseEvent.tags.find((t) => t[0] === "d");
|
|
if (!dTag) {
|
|
throw new Error(
|
|
'No "d" tag => cannot revert addressable kind=30078 event.'
|
|
);
|
|
}
|
|
const existingD = dTag[1];
|
|
|
|
const oldContent = JSON.parse(baseEvent.content || "{}");
|
|
const oldVersion = oldContent.version ?? 1;
|
|
|
|
let finalRootId = oldContent.videoRootId || null;
|
|
if (!finalRootId) {
|
|
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
|
|
}
|
|
|
|
const contentObject = {
|
|
videoRootId: finalRootId,
|
|
version: oldVersion,
|
|
deleted: true,
|
|
isPrivate: oldContent.isPrivate ?? false,
|
|
title: oldContent.title || "",
|
|
magnet: "",
|
|
thumbnail: "",
|
|
description: "This version was reverted by the creator.",
|
|
mode: oldContent.mode || "live",
|
|
};
|
|
|
|
const event = {
|
|
kind: 30078,
|
|
pubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
["t", "video"],
|
|
["d", existingD],
|
|
],
|
|
content: JSON.stringify(contentObject),
|
|
};
|
|
|
|
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(`Failed to revert on ${url}`, err);
|
|
}
|
|
})
|
|
);
|
|
|
|
return signedEvent;
|
|
}
|
|
|
|
/**
|
|
* "Deleting" => Mark all content with the same videoRootId as {deleted:true}
|
|
* and blank out magnet/desc.
|
|
*
|
|
* This version now asks for confirmation before proceeding.
|
|
*/
|
|
async deleteAllVersions(videoRootId, pubkey) {
|
|
if (!pubkey) {
|
|
throw new Error("Not logged in to delete all versions.");
|
|
}
|
|
|
|
// Ask for confirmation before proceeding
|
|
if (
|
|
!window.confirm(
|
|
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
|
)
|
|
) {
|
|
console.log("Deletion cancelled by user.");
|
|
return null; // Cancel deletion if user clicks "Cancel"
|
|
}
|
|
|
|
// 1) Find all events in our local allEvents that share the same root.
|
|
const matchingEvents = [];
|
|
for (const [id, vid] of this.allEvents.entries()) {
|
|
if (
|
|
vid.videoRootId === videoRootId &&
|
|
vid.pubkey === pubkey &&
|
|
!vid.deleted
|
|
) {
|
|
matchingEvents.push(vid);
|
|
}
|
|
}
|
|
if (!matchingEvents.length) {
|
|
throw new Error("No existing events found for that root.");
|
|
}
|
|
|
|
// 2) For each event, create a "revert" event to mark it as deleted.
|
|
// This will prompt the user (via the extension) to sign the deletion.
|
|
for (const vid of matchingEvents) {
|
|
await this.revertVideo(
|
|
{
|
|
id: vid.id,
|
|
pubkey: vid.pubkey,
|
|
content: JSON.stringify({
|
|
version: vid.version,
|
|
deleted: vid.deleted,
|
|
isPrivate: vid.isPrivate,
|
|
title: vid.title,
|
|
magnet: vid.magnet,
|
|
thumbnail: vid.thumbnail,
|
|
description: vid.description,
|
|
mode: vid.mode,
|
|
}),
|
|
tags: vid.tags,
|
|
},
|
|
pubkey
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Saves all known events to localStorage (or a different storage if you prefer).
|
|
*/
|
|
saveLocalData() {
|
|
// Convert our allEvents map into a plain object for JSON storage
|
|
const allEventsObject = {};
|
|
for (const [id, vid] of this.allEvents.entries()) {
|
|
allEventsObject[id] = vid;
|
|
}
|
|
localStorage.setItem("bitvidEvents", JSON.stringify(allEventsObject));
|
|
}
|
|
|
|
/**
|
|
* Subscribe to *all* videos (old and new) with a single subscription,
|
|
* buffering incoming events to avoid excessive DOM updates.
|
|
*/
|
|
subscribeVideos(onVideo) {
|
|
const filter = {
|
|
kinds: [30078],
|
|
"#t": ["video"],
|
|
// Adjust limit/time as desired
|
|
limit: 500,
|
|
since: 0,
|
|
};
|
|
|
|
if (isDevMode) {
|
|
console.log("[subscribeVideos] Subscribing with filter:", filter);
|
|
}
|
|
|
|
const sub = this.pool.sub(this.relays, [filter]);
|
|
const invalidDuringSub = [];
|
|
|
|
// We'll collect events here instead of processing them instantly
|
|
let eventBuffer = [];
|
|
|
|
// 1) On each incoming event, just push to the buffer
|
|
sub.on("event", (event) => {
|
|
eventBuffer.push(event);
|
|
});
|
|
|
|
// 2) Process buffered events on a setInterval (e.g., every second)
|
|
const processInterval = setInterval(() => {
|
|
if (eventBuffer.length > 0) {
|
|
// Copy and clear the buffer
|
|
const toProcess = eventBuffer.slice();
|
|
eventBuffer = [];
|
|
|
|
// Now handle each event
|
|
for (const evt of toProcess) {
|
|
try {
|
|
const video = convertEventToVideo(evt);
|
|
|
|
if (video.invalid) {
|
|
invalidDuringSub.push({ id: video.id, reason: video.reason });
|
|
continue;
|
|
}
|
|
|
|
// Store in allEvents
|
|
this.allEvents.set(evt.id, video);
|
|
|
|
// If it's a "deleted" note, remove from activeMap
|
|
if (video.deleted) {
|
|
const activeKey = getActiveKey(video);
|
|
this.activeMap.delete(activeKey);
|
|
continue;
|
|
}
|
|
|
|
// Otherwise, if it's newer than what we have, update activeMap
|
|
const activeKey = getActiveKey(video);
|
|
const prevActive = this.activeMap.get(activeKey);
|
|
if (!prevActive || video.created_at > prevActive.created_at) {
|
|
this.activeMap.set(activeKey, video);
|
|
onVideo(video); // Trigger the callback that re-renders
|
|
}
|
|
} catch (err) {
|
|
if (isDevMode) {
|
|
console.error("[subscribeVideos] Error processing event:", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optionally, save data to local storage after processing the batch
|
|
this.saveLocalData();
|
|
}
|
|
}, 1000);
|
|
|
|
// You can still use sub.on("eose") if needed
|
|
sub.on("eose", () => {
|
|
if (isDevMode && invalidDuringSub.length > 0) {
|
|
console.warn(
|
|
`[subscribeVideos] found ${invalidDuringSub.length} invalid v2 notes:`,
|
|
invalidDuringSub
|
|
);
|
|
}
|
|
if (isDevMode) {
|
|
console.log(
|
|
"[subscribeVideos] Reached EOSE for all relays (historical load done)"
|
|
);
|
|
}
|
|
});
|
|
|
|
// Return the subscription object if you need to unsub manually later
|
|
return sub;
|
|
}
|
|
|
|
/**
|
|
* fetchVideos => old approach
|
|
*/
|
|
async fetchVideos() {
|
|
const filter = {
|
|
kinds: [30078],
|
|
"#t": ["video"],
|
|
limit: 300,
|
|
since: 0,
|
|
};
|
|
|
|
const localAll = new Map();
|
|
// NEW: track invalid
|
|
const invalidNotes = [];
|
|
|
|
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);
|
|
if (vid.invalid) {
|
|
// Accumulate if invalid
|
|
invalidNotes.push({ id: vid.id, reason: vid.reason });
|
|
} else {
|
|
// Only add if good
|
|
localAll.set(evt.id, vid);
|
|
}
|
|
}
|
|
})
|
|
);
|
|
|
|
// Merge into 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);
|
|
}
|
|
}
|
|
|
|
// OPTIONAL: Log invalid stats
|
|
if (invalidNotes.length > 0 && isDevMode) {
|
|
console.warn(
|
|
`Skipped ${invalidNotes.length} invalid v2 notes:\n`,
|
|
invalidNotes.map((n) => `${n.id.slice(0, 8)}.. => ${n.reason}`)
|
|
);
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getEventById => old approach
|
|
*/
|
|
async getEventById(eventId) {
|
|
const local = this.allEvents.get(eventId);
|
|
if (local) {
|
|
return local;
|
|
}
|
|
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;
|
|
}
|
|
|
|
getActiveVideos() {
|
|
return Array.from(this.activeMap.values()).sort(
|
|
(a, b) => b.created_at - a.created_at
|
|
);
|
|
}
|
|
}
|
|
|
|
export const nostrClient = new NostrClient();
|