mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
314 lines
7.5 KiB
JavaScript
314 lines
7.5 KiB
JavaScript
// js/userBlocks.js
|
|
import { nostrClient } from "./nostr.js";
|
|
import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER } from "./nostrEventSchemas.js";
|
|
import {
|
|
publishEventToRelays,
|
|
assertAnyRelayAccepted,
|
|
} from "./nostrPublish.js";
|
|
|
|
function normalizeHex(pubkey) {
|
|
if (typeof pubkey !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = pubkey.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
|
return trimmed.toLowerCase();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
class UserBlockListManager {
|
|
constructor() {
|
|
this.blockedPubkeys = new Set();
|
|
this.blockEventId = null;
|
|
this.loaded = false;
|
|
}
|
|
|
|
reset() {
|
|
this.blockedPubkeys.clear();
|
|
this.blockEventId = null;
|
|
this.loaded = false;
|
|
}
|
|
|
|
getBlockedPubkeys() {
|
|
return Array.from(this.blockedPubkeys);
|
|
}
|
|
|
|
isBlocked(pubkey) {
|
|
const normalized = normalizeHex(pubkey);
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
return this.blockedPubkeys.has(normalized);
|
|
}
|
|
|
|
async ensureLoaded(userPubkey) {
|
|
if (this.loaded) {
|
|
return;
|
|
}
|
|
|
|
const normalized = normalizeHex(userPubkey);
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
|
|
await this.loadBlocks(normalized);
|
|
}
|
|
|
|
async loadBlocks(userPubkey) {
|
|
const normalized = normalizeHex(userPubkey);
|
|
if (!normalized) {
|
|
this.reset();
|
|
this.loaded = true;
|
|
return;
|
|
}
|
|
|
|
if (!window?.nostr?.nip04?.decrypt) {
|
|
console.warn(
|
|
"[UserBlockList] nip04.decrypt is unavailable; treating block list as empty."
|
|
);
|
|
this.reset();
|
|
this.loaded = true;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const filter = {
|
|
kinds: [30002],
|
|
authors: [normalized],
|
|
"#d": [BLOCK_LIST_IDENTIFIER],
|
|
limit: 1,
|
|
};
|
|
|
|
const events = [];
|
|
for (const relay of nostrClient.relays) {
|
|
try {
|
|
const res = await nostrClient.pool.list([relay], [filter]);
|
|
if (Array.isArray(res) && res.length) {
|
|
events.push(...res);
|
|
}
|
|
} catch (err) {
|
|
console.error(`[UserBlockList] Relay error at ${relay}:`, err);
|
|
}
|
|
}
|
|
|
|
if (!events.length) {
|
|
this.blockedPubkeys.clear();
|
|
this.blockEventId = null;
|
|
this.loaded = true;
|
|
return;
|
|
}
|
|
|
|
events.sort((a, b) => b.created_at - a.created_at);
|
|
const newest = events[0];
|
|
this.blockEventId = newest.id;
|
|
|
|
let decrypted = "";
|
|
try {
|
|
decrypted = await window.nostr.nip04.decrypt(
|
|
normalized,
|
|
newest.content
|
|
);
|
|
} catch (err) {
|
|
console.error("[UserBlockList] Failed to decrypt block list:", err);
|
|
this.blockedPubkeys.clear();
|
|
this.loaded = true;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(decrypted);
|
|
const list = Array.isArray(parsed?.blockedPubkeys)
|
|
? parsed.blockedPubkeys
|
|
: [];
|
|
const sanitized = list
|
|
.map((entry) => normalizeHex(entry))
|
|
.filter((candidate) => {
|
|
if (!candidate) {
|
|
return false;
|
|
}
|
|
if (candidate === normalized) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
this.blockedPubkeys = new Set(sanitized);
|
|
} catch (err) {
|
|
console.error("[UserBlockList] Failed to parse block list:", err);
|
|
this.blockedPubkeys.clear();
|
|
}
|
|
} catch (error) {
|
|
console.error("[UserBlockList] loadBlocks failed:", error);
|
|
this.blockedPubkeys.clear();
|
|
} finally {
|
|
this.loaded = true;
|
|
}
|
|
}
|
|
|
|
async addBlock(targetPubkey, userPubkey) {
|
|
const actorHex = normalizeHex(userPubkey);
|
|
if (!actorHex) {
|
|
throw new Error("Invalid user pubkey.");
|
|
}
|
|
|
|
const targetHex = normalizeHex(targetPubkey);
|
|
if (!targetHex) {
|
|
const err = new Error("Invalid target pubkey.");
|
|
err.code = "invalid";
|
|
throw err;
|
|
}
|
|
|
|
if (actorHex === targetHex) {
|
|
const err = new Error("Cannot block yourself.");
|
|
err.code = "self";
|
|
throw err;
|
|
}
|
|
|
|
await this.ensureLoaded(actorHex);
|
|
|
|
if (this.blockedPubkeys.has(targetHex)) {
|
|
return { ok: true, already: true };
|
|
}
|
|
|
|
const snapshot = new Set(this.blockedPubkeys);
|
|
this.blockedPubkeys.add(targetHex);
|
|
|
|
try {
|
|
await this.publishBlockList(actorHex);
|
|
return { ok: true };
|
|
} catch (err) {
|
|
this.blockedPubkeys = snapshot;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async removeBlock(targetPubkey, userPubkey) {
|
|
const actorHex = normalizeHex(userPubkey);
|
|
if (!actorHex) {
|
|
throw new Error("Invalid user pubkey.");
|
|
}
|
|
|
|
const targetHex = normalizeHex(targetPubkey);
|
|
if (!targetHex) {
|
|
return { ok: true, already: true };
|
|
}
|
|
|
|
await this.ensureLoaded(actorHex);
|
|
|
|
if (!this.blockedPubkeys.has(targetHex)) {
|
|
return { ok: true, already: true };
|
|
}
|
|
|
|
const snapshot = new Set(this.blockedPubkeys);
|
|
this.blockedPubkeys.delete(targetHex);
|
|
|
|
try {
|
|
await this.publishBlockList(actorHex);
|
|
return { ok: true };
|
|
} catch (err) {
|
|
this.blockedPubkeys = snapshot;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async publishBlockList(userPubkey) {
|
|
if (!window?.nostr?.nip04?.encrypt) {
|
|
const err = new Error(
|
|
"NIP-04 encryption is required to update the block list."
|
|
);
|
|
err.code = "nip04-missing";
|
|
throw err;
|
|
}
|
|
|
|
if (typeof window.nostr.signEvent !== "function") {
|
|
const err = new Error("Nostr extension missing signEvent support.");
|
|
err.code = "nip04-missing";
|
|
throw err;
|
|
}
|
|
|
|
const normalized = normalizeHex(userPubkey);
|
|
if (!normalized) {
|
|
throw new Error("Invalid user pubkey.");
|
|
}
|
|
|
|
const payload = {
|
|
blockedPubkeys: Array.from(this.blockedPubkeys).filter(
|
|
(candidate) => candidate && candidate !== normalized
|
|
),
|
|
};
|
|
const plaintext = JSON.stringify(payload);
|
|
|
|
let cipherText = "";
|
|
try {
|
|
cipherText = await window.nostr.nip04.encrypt(normalized, plaintext);
|
|
} catch (error) {
|
|
const err = new Error("Failed to encrypt block list.");
|
|
err.code = "nip04-missing";
|
|
throw err;
|
|
}
|
|
|
|
const event = buildBlockListEvent({
|
|
pubkey: normalized,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
content: cipherText,
|
|
});
|
|
|
|
const signedEvent = await window.nostr.signEvent(event);
|
|
|
|
const publishResults = await publishEventToRelays(
|
|
nostrClient.pool,
|
|
nostrClient.relays,
|
|
signedEvent
|
|
);
|
|
|
|
let publishSummary;
|
|
try {
|
|
publishSummary = assertAnyRelayAccepted(publishResults, {
|
|
context: "block list",
|
|
});
|
|
} catch (publishError) {
|
|
if (publishError?.relayFailures?.length) {
|
|
publishError.relayFailures.forEach(
|
|
({ url, error: relayError, reason }) => {
|
|
console.error(
|
|
`[UserBlockList] Block list rejected by ${url}: ${reason}`,
|
|
relayError || reason
|
|
);
|
|
}
|
|
);
|
|
}
|
|
throw publishError;
|
|
}
|
|
|
|
if (publishSummary.failed.length) {
|
|
publishSummary.failed.forEach(({ url, error: relayError }) => {
|
|
const reason =
|
|
relayError instanceof Error
|
|
? relayError.message
|
|
: relayError
|
|
? String(relayError)
|
|
: "publish failed";
|
|
console.warn(
|
|
`[UserBlockList] Block list not accepted by ${url}: ${reason}`,
|
|
relayError
|
|
);
|
|
});
|
|
}
|
|
|
|
this.blockEventId = signedEvent.id;
|
|
return signedEvent;
|
|
}
|
|
}
|
|
|
|
export const userBlocks = new UserBlockListManager();
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.userBlocks = userBlocks;
|
|
}
|