From 7c7ac3e9f26068719b98740c98bf818bd7c579b2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:41:18 +0000 Subject: [PATCH] Enable session-local moderation for anonymous users - Update `ModerationService` to recognize session actors and load mutes from local storage. - Update `UserBlockListManager` to persist blocks locally for session actors without relay publication. - Ensure `submitReport` falls back to the session actor's signer. - Add `resolveStorage` helper to safely access localStorage. --- js/services/moderationService.js | 140 +++++++++++++++++++++++++++++-- js/userBlocks.js | 64 ++++++++++---- 2 files changed, 181 insertions(+), 23 deletions(-) diff --git a/js/services/moderationService.js b/js/services/moderationService.js index 7a9750b4..98b77ae4 100644 --- a/js/services/moderationService.js +++ b/js/services/moderationService.js @@ -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"; diff --git a/js/userBlocks.js b/js/userBlocks.js index 474cfe84..57669725 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -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, {