Merge pull request #142 from PR0M3TH3AN/codex/automate-dm-for-npub-blacklist/whitelist

Update blacklist DM with appeals link
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-09-28 21:03:13 -04:00
commit a0356967ce
2 files changed
+299 -1

No files matched your search

+109 -1
View File
@@ -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 = `![BitVid status update](${ADMIN_DM_IMAGE_URL})\n\n${messageBody}`;
return nostrClient.sendDirectMessage(
normalizedTarget,
message,
actorHex
);
}
async onAccessControlUpdated() {
try {
await this.refreshAdminPaneState();
+190
View File
@@ -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.
*/