Merge pull request #1174 from PR0M3TH3AN/codex/session-actor-moderation-13643754943913031156

Enable session-local moderation for anonymous users
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-10 17:43:11 -05:00
commit 0b8d45ed76
2 files changed
+181 -23

No files matched your search

+132 -8
View File
@@ -446,6 +446,19 @@ function ensureNumber(value) {
return Number.isFinite(parsed) ? parsed : 0;
}
function resolveStorage() {
if (typeof localStorage !== "undefined") {
return localStorage;
}
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage;
}
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
return globalThis.localStorage;
}
return null;
}
export class ModerationService {
constructor({
nostrClient: client = null,
@@ -459,6 +472,7 @@ export class ModerationService {
this.userLogger = normalizeUserLogger(userChannel);
this.viewerPubkey = "";
this.viewerIsSessionActor = false;
this.trustedContacts = new Set();
this.trustedSeedContacts = new Set();
this.viewerContacts = new Set();
@@ -1175,6 +1189,85 @@ export class ModerationService {
return this.viewerMuteList.has(normalized);
}
loadLocalMutes(pubkey) {
const normalized = normalizeHex(pubkey);
if (!normalized) {
return;
}
const storage = resolveStorage();
if (!storage) {
return;
}
const key = `bitvid:user-mutes:local:v1:${normalized}`;
let loaded = [];
try {
const raw = storage.getItem(key);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
loaded = parsed;
}
}
} catch (error) {
this.log(
`[moderationService] failed to load local mutes for ${normalized}`,
error,
);
}
const mutes = new Set();
for (const item of loaded) {
const target = normalizeHex(item);
if (target && target !== normalized) {
mutes.add(target);
}
}
this.replaceTrustedMuteList(normalized, mutes, {
createdAt: Date.now() / 1000,
eventId: `local-${Date.now()}`,
});
}
saveLocalMutes(pubkey, muteSet) {
const normalized = normalizeHex(pubkey);
if (!normalized) {
return;
}
const storage = resolveStorage();
if (!storage) {
return;
}
const list = [];
if (muteSet instanceof Set || Array.isArray(muteSet)) {
for (const item of muteSet) {
const target = normalizeHex(item);
if (target && target !== normalized) {
list.push(target);
}
}
}
const key = `bitvid:user-mutes:local:v1:${normalized}`;
try {
if (!list.length) {
storage.removeItem(key);
} else {
storage.setItem(key, JSON.stringify(list));
}
} catch (error) {
this.log(
`[moderationService] failed to save local mutes for ${normalized}`,
error,
);
}
}
async ensureViewerMuteListLoaded(pubkey = this.viewerPubkey) {
const normalized = normalizeHex(pubkey);
if (!normalized) {
@@ -1188,6 +1281,11 @@ export class ModerationService {
return;
}
if (this.viewerIsSessionActor && normalized === this.viewerPubkey) {
this.loadLocalMutes(normalized);
return;
}
if (this.viewerMutePromise) {
try {
await this.viewerMutePromise;
@@ -1221,6 +1319,15 @@ export class ModerationService {
throw error;
}
if (this.viewerIsSessionActor && viewer === this.viewerPubkey) {
this.saveLocalMutes(viewer, muted);
this.replaceTrustedMuteList(viewer, muted, {
createdAt: Date.now() / 1000,
eventId: `local-${Date.now()}`,
});
return { ok: true, event: null, results: [] };
}
await this.ensurePool();
const extension = typeof window !== "undefined" ? window.nostr : null;
@@ -1527,7 +1634,9 @@ export class ModerationService {
}
async refreshViewerFromClient() {
const clientPubkey = normalizeHex(this.nostrClient?.pubkey);
const clientPubkey = normalizeHex(
this.nostrClient?.pubkey || this.nostrClient?.sessionActor?.pubkey,
);
if (clientPubkey === this.viewerPubkey) {
if (this.contactListPromise) {
try {
@@ -1557,6 +1666,13 @@ export class ModerationService {
this.trustedContacts instanceof Set ? new Set(this.trustedContacts) : new Set();
this.viewerPubkey = normalized;
const loggedInPubkey = normalizeHex(this.nostrClient?.pubkey);
const sessionPubkey = normalizeHex(this.nostrClient?.sessionActor?.pubkey);
this.viewerIsSessionActor =
!!normalized && !loggedInPubkey && normalized === sessionPubkey;
this.clearTrustedMuteTracking({ previousContacts });
if (this.contactSubscription && typeof this.contactSubscription.unsub === "function") {
@@ -2127,7 +2243,10 @@ export class ModerationService {
throw error;
}
const reporterPubkey = normalizeHex(this.nostrClient?.pubkey);
const reporterPubkey = normalizeHex(
this.nostrClient?.pubkey || this.nostrClient?.sessionActor?.pubkey,
);
if (!reporterPubkey) {
const error = new Error("viewer-not-logged-in");
error.code = "viewer-not-logged-in";
@@ -2141,14 +2260,19 @@ export class ModerationService {
throw error;
}
const extension = typeof window !== "undefined" ? window.nostr : null;
if (!extension) {
const error = new Error("nostr-extension-missing");
error.code = "nostr-extension-missing";
const signer = await this.nostrClient.ensureActiveSignerForPubkey(
reporterPubkey,
);
if (!signer || typeof signer.signEvent !== "function") {
const error = new Error("signer-unavailable");
error.code = "signer-unavailable";
throw error;
}
if (typeof this.nostrClient?.ensureExtensionPermissions === "function") {
if (
signer.type === "extension" &&
typeof this.nostrClient?.ensureExtensionPermissions === "function"
) {
const permissionResult = await this.nostrClient.ensureExtensionPermissions([
"sign_event",
"get_public_key",
@@ -2185,7 +2309,7 @@ export class ModerationService {
let signedEvent;
try {
signedEvent = await extension.signEvent(event);
signedEvent = await signer.signEvent(event);
} catch (error) {
const wrapped = new Error("signature-failed");
wrapped.code = "signature-failed";
+49 -15
View File
@@ -69,6 +69,19 @@ const BLOCKLIST_SEEDED_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:seeded:v1`;
const BLOCKLIST_REMOVALS_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:removals:v1`;
const BLOCKLIST_LOCAL_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:local:v1`;
function resolveStorage() {
if (typeof localStorage !== "undefined") {
return localStorage;
}
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage;
}
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
return globalThis.localStorage;
}
return null;
}
function normalizeEncryptionToken(value) {
if (typeof value !== "string") {
return "";
@@ -498,7 +511,8 @@ function writeRemovalSet(actorHex, removals) {
return;
}
if (typeof localStorage === "undefined") {
const storage = resolveStorage();
if (!storage) {
return;
}
@@ -541,19 +555,6 @@ function normalizeHex(pubkey) {
return null;
}
function resolveStorage() {
if (typeof localStorage !== "undefined") {
return localStorage;
}
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage;
}
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
return globalThis.localStorage;
}
return null;
}
function readLocalBlocks(actorHex) {
if (typeof actorHex !== "string" || !actorHex) {
return null;
@@ -598,7 +599,8 @@ function writeLocalBlocks(actorHex, blocks) {
return;
}
if (typeof localStorage === "undefined") {
const storage = resolveStorage();
if (!storage) {
return;
}
@@ -1469,6 +1471,22 @@ class UserBlockListManager {
const snapshot = new Set(this.blockedPubkeys);
this.blockedPubkeys.add(targetHex);
const loggedInPubkey = normalizeHex(nostrClient?.pubkey);
const sessionPubkey = normalizeHex(nostrClient?.sessionActor?.pubkey);
const isSessionActor =
!!actorHex && !loggedInPubkey && actorHex === sessionPubkey;
if (isSessionActor) {
this._saveLocal(actorHex);
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
action: "block",
targetPubkey: targetHex,
actorPubkey: actorHex,
});
this._clearSeedRemoval(actorHex, targetHex);
return { ok: true };
}
try {
await this.publishBlockList(actorHex);
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
@@ -1504,6 +1522,22 @@ class UserBlockListManager {
const snapshot = new Set(this.blockedPubkeys);
this.blockedPubkeys.delete(targetHex);
const loggedInPubkey = normalizeHex(nostrClient?.pubkey);
const sessionPubkey = normalizeHex(nostrClient?.sessionActor?.pubkey);
const isSessionActor =
!!actorHex && !loggedInPubkey && actorHex === sessionPubkey;
if (isSessionActor) {
this._saveLocal(actorHex);
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
action: "unblock",
targetPubkey: targetHex,
actorPubkey: actorHex,
});
this._addSeedRemoval(actorHex, targetHex);
return { ok: true };
}
try {
await this.publishBlockList(actorHex);
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {