mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #142 from PR0M3TH3AN/codex/automate-dm-for-npub-blacklist/whitelist
Update blacklist DM with appeals link
This commit is contained in:
2 files changed
+299
-1
No files matched your search
@@ -4,7 +4,7 @@ import { loadView } from "./viewManager.js";
|
||||
import { nostrClient } from "./nostr.js";
|
||||
import { torrentClient } from "./webtorrent.js";
|
||||
import { isDevMode, ADMIN_SUPER_NPUB } from "./config.js";
|
||||
import { accessControl } from "./accessControl.js";
|
||||
import { accessControl, normalizeNpub } from "./accessControl.js";
|
||||
import { safeDecodeMagnet } from "./magnetUtils.js";
|
||||
import { extractMagnetHints, normalizeAndAugmentMagnet } from "./magnet.js";
|
||||
import { deriveTorrentPlaybackConfig } from "./playbackUtils.js";
|
||||
@@ -64,6 +64,9 @@ const UNSUPPORTED_BTITH_MESSAGE =
|
||||
"This magnet link is missing a compatible BitTorrent v1 info hash.";
|
||||
|
||||
const FALLBACK_THUMBNAIL_SRC = "assets/jpg/video-thumbnail-fallback.jpg";
|
||||
const ADMIN_DM_IMAGE_URL =
|
||||
"https://beta.bitvid.network/assets/jpg/video-thumbnail-fallback.jpg";
|
||||
const BITVID_WEBSITE_URL = "https://bitvid.network/";
|
||||
const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/;
|
||||
const EMPTY_VIDEO_LIST_SIGNATURE = "__EMPTY__";
|
||||
const PROFILE_CACHE_STORAGE_KEY = "bitvid:profileCache:v1";
|
||||
@@ -4247,6 +4250,38 @@ class bitvidApp {
|
||||
buttonToToggle.disabled = false;
|
||||
buttonToToggle.removeAttribute("aria-busy");
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
try {
|
||||
const notifyResult = await this.sendAdminListNotification({
|
||||
listType,
|
||||
actorNpub,
|
||||
targetNpub: target,
|
||||
});
|
||||
if (!notifyResult?.ok) {
|
||||
const errorMessage = this.describeNotificationError(
|
||||
notifyResult?.error
|
||||
);
|
||||
if (errorMessage) {
|
||||
this.showError(errorMessage);
|
||||
}
|
||||
if (isDevMode && notifyResult?.error) {
|
||||
console.warn(
|
||||
"[admin] Failed to send list notification DM:",
|
||||
notifyResult
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to send list notification DM:", error);
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"List update succeeded, but DM notification threw an unexpected error.",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describeAdminError(code) {
|
||||
@@ -4272,6 +4307,79 @@ class bitvidApp {
|
||||
}
|
||||
}
|
||||
|
||||
describeNotificationError(code) {
|
||||
switch (code) {
|
||||
case "nostr-extension-missing":
|
||||
return "List updated, but the DM notification failed because no Nostr extension is connected.";
|
||||
case "nostr-uninitialized":
|
||||
return "List updated, but the DM notification system is still connecting to Nostr relays. Please try again in a moment.";
|
||||
case "nip04-unavailable":
|
||||
return "List updated, but your Nostr extension does not support NIP-04 encryption, so the DM notification was not sent.";
|
||||
case "sign-event-unavailable":
|
||||
return "List updated, but your Nostr extension could not sign the DM notification.";
|
||||
case "missing-actor-pubkey":
|
||||
return "List updated, but we could not determine your public key to send the DM notification.";
|
||||
case "publish-failed":
|
||||
return "List updated, but the DM notification could not be delivered to any relay.";
|
||||
case "encryption-failed":
|
||||
case "signature-failed":
|
||||
return "List updated, but the DM notification failed while preparing the encrypted message.";
|
||||
case "invalid-target":
|
||||
case "empty-message":
|
||||
return "";
|
||||
default:
|
||||
return "List updated, but the DM notification could not be sent.";
|
||||
}
|
||||
}
|
||||
|
||||
async sendAdminListNotification({ listType, actorNpub, targetNpub }) {
|
||||
const normalizedTarget = normalizeNpub(targetNpub);
|
||||
if (!normalizedTarget) {
|
||||
return { ok: false, error: "invalid-target" };
|
||||
}
|
||||
|
||||
if (!this.pubkey) {
|
||||
return { ok: false, error: "missing-actor-pubkey" };
|
||||
}
|
||||
|
||||
const actorHex = this.pubkey;
|
||||
const fallbackActor = this.safeEncodeNpub(actorHex) || "a BitVid moderator";
|
||||
const actorDisplay = normalizeNpub(actorNpub) || fallbackActor;
|
||||
const isWhitelist = listType === "whitelist";
|
||||
|
||||
const introLine = isWhitelist
|
||||
? `Great news—your npub ${normalizedTarget} has been added to the BitVid whitelist by ${actorDisplay}.`
|
||||
: `We wanted to let you know that your npub ${normalizedTarget} has been placed on the BitVid blacklist by ${actorDisplay}.`;
|
||||
|
||||
const statusLine = isWhitelist
|
||||
? `You now have full creator access across BitVid (${BITVID_WEBSITE_URL}).`
|
||||
: `This hides your channel and prevents uploads across BitVid (${BITVID_WEBSITE_URL}) for now.`;
|
||||
|
||||
const followUpLine = isWhitelist
|
||||
? "Please take a moment to review our community guidelines (https://bitvid.network/#view=community-guidelines), and reply to this DM if you have any questions."
|
||||
: "Please review our community guidelines (https://bitvid.network/#view=community-guidelines). If you believe this was a mistake, you can submit an appeal at https://bitvid.network/?modal=appeals to request reinstatement, or reply to this DM with any questions.";
|
||||
|
||||
const messageBody = [
|
||||
"Hi there,",
|
||||
"",
|
||||
introLine,
|
||||
"",
|
||||
statusLine,
|
||||
"",
|
||||
followUpLine,
|
||||
"",
|
||||
"— The BitVid Team",
|
||||
].join("\n");
|
||||
|
||||
const message = `\n\n${messageBody}`;
|
||||
|
||||
return nostrClient.sendDirectMessage(
|
||||
normalizedTarget,
|
||||
message,
|
||||
actorHex
|
||||
);
|
||||
}
|
||||
|
||||
async onAccessControlUpdated() {
|
||||
try {
|
||||
await this.refreshAdminPaneState();
|
||||
|
||||
+190
@@ -52,6 +52,94 @@ function fakeDecrypt(encrypted) {
|
||||
return encrypted.split("").reverse().join("");
|
||||
}
|
||||
|
||||
function decodeNpubToHex(npub) {
|
||||
if (typeof npub !== "string" || !npub.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (
|
||||
!window?.NostrTools?.nip19 ||
|
||||
typeof window.NostrTools.nip19.decode !== "function"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = window.NostrTools.nip19.decode(npub.trim());
|
||||
if (decoded?.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(`[nostr] Failed to decode npub: ${npub}`, error);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const DM_PUBLISH_TIMEOUT_MS = 10_000;
|
||||
|
||||
function publishEventToRelay(pool, url, event) {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finalize = (success, error = null) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolve({ url, success, error });
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
finalize(false, new Error("publish timeout"));
|
||||
}, DM_PUBLISH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const pub = pool.publish([url], event);
|
||||
|
||||
if (pub && typeof pub.on === "function") {
|
||||
pub.on("ok", () => {
|
||||
clearTimeout(timeoutId);
|
||||
finalize(true);
|
||||
});
|
||||
pub.on("seen", () => {
|
||||
clearTimeout(timeoutId);
|
||||
finalize(true);
|
||||
});
|
||||
pub.on("failed", (reason) => {
|
||||
clearTimeout(timeoutId);
|
||||
const err =
|
||||
reason instanceof Error
|
||||
? reason
|
||||
: new Error(String(reason || "publish failed"));
|
||||
finalize(false, err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pub && typeof pub.then === "function") {
|
||||
pub
|
||||
.then(() => {
|
||||
clearTimeout(timeoutId);
|
||||
finalize(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeoutId);
|
||||
finalize(false, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
finalize(false, error);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
finalize(true);
|
||||
});
|
||||
}
|
||||
|
||||
const EXTENSION_MIME_MAP = {
|
||||
mp4: "video/mp4",
|
||||
m4v: "video/x-m4v",
|
||||
@@ -590,6 +678,108 @@ class NostrClient {
|
||||
if (isDevMode) console.log("User logged out.");
|
||||
}
|
||||
|
||||
async sendDirectMessage(targetNpub, message, actorPubkeyOverride = null) {
|
||||
const trimmedTarget = typeof targetNpub === "string" ? targetNpub.trim() : "";
|
||||
const trimmedMessage = typeof message === "string" ? message.trim() : "";
|
||||
|
||||
if (!trimmedTarget) {
|
||||
return { ok: false, error: "invalid-target" };
|
||||
}
|
||||
|
||||
if (!trimmedMessage) {
|
||||
return { ok: false, error: "empty-message" };
|
||||
}
|
||||
|
||||
if (!this.pool) {
|
||||
return { ok: false, error: "nostr-uninitialized" };
|
||||
}
|
||||
|
||||
const extension = window?.nostr;
|
||||
if (!extension) {
|
||||
return { ok: false, error: "nostr-extension-missing" };
|
||||
}
|
||||
|
||||
const nip04 = extension.nip04;
|
||||
if (!nip04 || typeof nip04.encrypt !== "function") {
|
||||
return { ok: false, error: "nip04-unavailable" };
|
||||
}
|
||||
|
||||
if (typeof extension.signEvent !== "function") {
|
||||
return { ok: false, error: "sign-event-unavailable" };
|
||||
}
|
||||
|
||||
let actorHex =
|
||||
typeof actorPubkeyOverride === "string" && actorPubkeyOverride.trim()
|
||||
? actorPubkeyOverride.trim()
|
||||
: "";
|
||||
|
||||
if (!actorHex && typeof this.pubkey === "string") {
|
||||
actorHex = this.pubkey.trim();
|
||||
}
|
||||
|
||||
if (!actorHex && typeof extension.getPublicKey === "function") {
|
||||
try {
|
||||
actorHex = await extension.getPublicKey();
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to fetch actor pubkey from extension:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!actorHex) {
|
||||
return { ok: false, error: "missing-actor-pubkey" };
|
||||
}
|
||||
|
||||
const targetHex = decodeNpubToHex(trimmedTarget);
|
||||
if (!targetHex) {
|
||||
return { ok: false, error: "invalid-target" };
|
||||
}
|
||||
|
||||
let ciphertext = "";
|
||||
try {
|
||||
ciphertext = await nip04.encrypt(targetHex, trimmedMessage);
|
||||
} catch (error) {
|
||||
return { ok: false, error: "encryption-failed", details: error };
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 4,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["p", targetHex]],
|
||||
content: ciphertext,
|
||||
pubkey: actorHex,
|
||||
};
|
||||
|
||||
let signedEvent;
|
||||
try {
|
||||
signedEvent = await extension.signEvent(event);
|
||||
} catch (error) {
|
||||
return { ok: false, error: "signature-failed", details: error };
|
||||
}
|
||||
|
||||
const relays =
|
||||
Array.isArray(this.relays) && this.relays.length ? this.relays : RELAY_URLS;
|
||||
|
||||
const publishResults = await Promise.all(
|
||||
relays.map((url) => publishEventToRelay(this.pool, url, signedEvent))
|
||||
);
|
||||
|
||||
const success = publishResults.some((result) => result.success);
|
||||
if (!success) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "publish-failed",
|
||||
details: publishResults.filter((result) => !result.success),
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a new video using the v3 content schema.
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user