Files
thePR0M3TH3ANandClaude Opus 4.8 a0131c718e Safety: account-level (per-creator) web-of-trust overrides
Add "trusted creator" overrides that suppress the web-of-trust warning/blur
for ALL of a creator's videos until reset — the account-level companion to the
existing per-video "Show anyway".

- cache.js: per-author override store (bitvid:moderationAuthorOverrides:v1),
  keyed by author hex, with get/set/has/clear/list + persist/load, mirroring
  the per-video store.
- moderationDecorator: overrideActive now also honors an account-level override
  for the video's author, so the whole class of WoT warnings clears for every
  video by that creator.
- app.js: load the author overrides on startup.
- Services + contract: getAuthorModerationOverrides / setAuthorModerationOverride
  / clearAuthorModerationOverride; set/clear refresh the feed + fire the shared
  moderation-override event so the Safety tab list updates.
- Safety tab: a "Trusted creators" section — add by npub, list each with a
  Reset, mirroring the Active-overrides section.
- tests: storage set/get/has/clear/list, hex normalization, persist→load,
  invalid-input guard.

Next: the "Always show this creator" button on the video warning overlay
(second entry point) — a larger multi-component UI change, done separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 15:58:10 -04:00

1999 lines
52 KiB
JavaScript

import {
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
DEFAULT_BLUR_THRESHOLD,
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
getTrustedMuteHideThreshold,
getTrustedSpamHideThreshold,
PROFILE_CACHE_TTL_MS,
URL_HEALTH_TTL_MS,
URL_HEALTH_RETRY_MS,
URL_PROBE_TIMEOUT_MS,
} from "../constants.js";
import { userLogger } from "../utils/logger.js";
import { sanitizeProfileMediaUrl } from "../utils/profileMedia.js";
import {
readUrlHealthFromStorage,
removeUrlHealthFromStorage,
writeUrlHealthToStorage,
} from "../utils/storage.js";
import createProfileSettingsStore from "./profileSettingsStore.js";
const PROFILE_CACHE_STORAGE_KEY = "bitvid:profileCache:v1";
const PROFILE_CACHE_VERSION = 1;
const SAVED_PROFILES_STORAGE_KEY = "bitvid:savedProfiles:v1";
const SAVED_PROFILES_STORAGE_VERSION = 1;
const URL_HEALTH_TIMEOUT_RETRY_MS = URL_HEALTH_RETRY_MS;
export { URL_PROBE_TIMEOUT_MS };
const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds
const MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v2";
const MODERATION_OVERRIDE_STORAGE_VERSION = 2;
// Account-level ("trusted creator") web-of-trust overrides, keyed by author hex.
// A showAnyway entry suppresses the WoT blur/hide/warning for ALL of that
// author's videos until the viewer resets it in the Safety tab.
const AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY =
"bitvid:moderationAuthorOverrides:v1";
const AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION = 1;
const MODERATION_SETTINGS_STORAGE_KEY = "bitvid:moderationSettings:v1";
const MODERATION_SETTINGS_STORAGE_VERSION = 3;
const DM_PRIVACY_SETTINGS_STORAGE_KEY = "bitvid:dmPrivacySettings:v1";
const DM_PRIVACY_SETTINGS_STORAGE_VERSION = 1;
function computeDefaultModerationSettings() {
const defaultBlur = sanitizeModerationThreshold(
DEFAULT_BLUR_THRESHOLD,
DEFAULT_BLUR_THRESHOLD,
);
const defaultAutoplay = sanitizeModerationThreshold(
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
);
const defaultTrustedMute = sanitizeModerationThreshold(
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
);
const defaultTrustedSpam = sanitizeModerationThreshold(
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
);
let runtimeMuteHide = defaultTrustedMute;
let runtimeSpamHide = defaultTrustedSpam;
try {
if (typeof getTrustedMuteHideThreshold === "function") {
runtimeMuteHide = sanitizeModerationThreshold(
getTrustedMuteHideThreshold(),
defaultTrustedMute,
);
}
} catch (error) {
userLogger.warn(
"[cache.computeDefaultModerationSettings] Failed to read runtime mute hide threshold:",
error,
);
runtimeMuteHide = defaultTrustedMute;
}
try {
if (typeof getTrustedSpamHideThreshold === "function") {
runtimeSpamHide = sanitizeModerationThreshold(
getTrustedSpamHideThreshold(),
defaultTrustedSpam,
);
}
} catch (error) {
userLogger.warn(
"[cache.computeDefaultModerationSettings] Failed to read runtime spam hide threshold:",
error,
);
runtimeSpamHide = defaultTrustedSpam;
}
return {
blurThreshold: defaultBlur,
autoplayBlockThreshold: defaultAutoplay,
trustedMuteHideThreshold: sanitizeModerationThreshold(
runtimeMuteHide,
defaultTrustedMute,
),
trustedMuteHideThresholds: {},
trustedSpamHideThreshold: sanitizeModerationThreshold(
runtimeSpamHide,
defaultTrustedSpam,
),
};
}
function createDefaultModerationSettings() {
return { ...computeDefaultModerationSettings() };
}
const GLOBAL_MODERATION_PROFILE_KEY = "__global__";
const moderationSettingsStore = createProfileSettingsStore({
clone: (value) => ({ ...value }),
});
moderationSettingsStore.set(
GLOBAL_MODERATION_PROFILE_KEY,
createDefaultModerationSettings(),
);
function createDefaultDmPrivacySettings() {
return {
readReceiptsEnabled: false,
typingIndicatorsEnabled: false,
};
}
const GLOBAL_DM_PRIVACY_PROFILE_KEY = "__dmPrivacyDefault__";
const dmPrivacySettingsStore = createProfileSettingsStore({
clone: (value) => ({ ...value }),
});
dmPrivacySettingsStore.set(
GLOBAL_DM_PRIVACY_PROFILE_KEY,
createDefaultDmPrivacySettings(),
);
function sanitizeModerationThreshold(value, fallback) {
if (value === null || value === undefined) {
return fallback;
}
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
const clamped = Math.max(0, Math.floor(numeric));
if (!Number.isFinite(clamped)) {
return fallback;
}
return clamped;
}
function sanitizeModerationThresholdMap(
value,
fallback = {},
{ mergeFallback = false } = {},
) {
const fallbackMap = fallback && typeof fallback === "object" ? fallback : {};
if (!value || typeof value !== "object") {
return mergeFallback ? { ...fallbackMap } : {};
}
const sanitized = {};
for (const [key, entry] of Object.entries(value)) {
if (typeof key !== "string") {
continue;
}
const normalizedKey = key.trim().toLowerCase();
if (!normalizedKey) {
continue;
}
const sanitizedValue = sanitizeModerationThreshold(
entry,
fallbackMap[normalizedKey],
);
if (Number.isFinite(sanitizedValue)) {
sanitized[normalizedKey] = sanitizedValue;
}
}
return mergeFallback ? { ...fallbackMap, ...sanitized } : sanitized;
}
function sanitizeProfileString(value) {
if (typeof value !== "string") {
return "";
}
const trimmed = value.trim();
return trimmed;
}
let savedProfiles = [];
let activeProfilePubkey = null;
let hadExplicitActiveProfile = false;
const profileCache = new Map();
const urlHealthCache = new Map();
const urlHealthInFlight = new Map();
const moderationOverrides = new Map();
const authorModerationOverrides = new Map();
function hasSavedProfilesChanged(previousProfiles, nextProfiles) {
if (previousProfiles === nextProfiles) {
return false;
}
if (!Array.isArray(previousProfiles) || !Array.isArray(nextProfiles)) {
return true;
}
if (previousProfiles.length !== nextProfiles.length) {
return true;
}
for (let index = 0; index < previousProfiles.length; index += 1) {
const prev = previousProfiles[index];
const next = nextProfiles[index];
if (prev === next) {
continue;
}
if (!prev || !next || typeof prev !== "object" || typeof next !== "object") {
return true;
}
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
for (const key of keys) {
if (prev[key] !== next[key]) {
return true;
}
}
}
return false;
}
function safeDecodeNpub(npub) {
if (typeof npub !== "string") {
return null;
}
const trimmed = npub.trim();
if (!trimmed) {
return null;
}
try {
const decoded = window?.NostrTools?.nip19?.decode(trimmed);
if (decoded && decoded.type === "npub" && typeof decoded.data === "string") {
return decoded.data;
}
} catch (error) {
return null;
}
return null;
}
function normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") {
return null;
}
const trimmed = pubkey.trim();
if (!trimmed) {
return null;
}
if (HEX64_REGEX.test(trimmed)) {
return trimmed.toLowerCase();
}
if (trimmed.startsWith("npub1")) {
const decoded = safeDecodeNpub(trimmed);
if (decoded && HEX64_REGEX.test(decoded)) {
return decoded.toLowerCase();
}
}
return null;
}
function normalizeEventId(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim().toLowerCase();
if (!trimmed) {
return null;
}
return HEX64_REGEX.test(trimmed) ? trimmed : null;
}
function buildModerationOverrideKey(authorPubkey, eventId) {
const normalizedAuthor =
typeof authorPubkey === "string" ? authorPubkey.trim().toLowerCase() : "";
return `${normalizedAuthor}:${eventId}`;
}
function normalizeModerationOverrideDescriptor(eventIdOrDescriptor, authorPubkey) {
let eventId = null;
let author = null;
if (eventIdOrDescriptor && typeof eventIdOrDescriptor === "object") {
eventId =
eventIdOrDescriptor.eventId ||
eventIdOrDescriptor.id ||
eventIdOrDescriptor.eventID ||
null;
author =
eventIdOrDescriptor.authorPubkey ||
eventIdOrDescriptor.pubkey ||
eventIdOrDescriptor.author ||
null;
} else if (typeof eventIdOrDescriptor === "string") {
eventId = eventIdOrDescriptor;
}
if (!author && typeof authorPubkey === "string") {
author = authorPubkey;
}
const normalizedEventId = normalizeEventId(eventId);
if (!normalizedEventId) {
return null;
}
const normalizedAuthor = normalizeHexPubkey(author);
const authorKey = normalizedAuthor || "";
return {
eventId: normalizedEventId,
authorPubkey: normalizedAuthor || "",
key: buildModerationOverrideKey(authorKey, normalizedEventId),
};
}
export function getSavedProfiles() {
return savedProfiles;
}
export function getProfileCacheMap() {
return profileCache;
}
export function getActiveProfilePubkey() {
return activeProfilePubkey;
}
export function setActiveProfilePubkey(pubkey, { persist = true } = {}) {
const normalized = normalizeHexPubkey(pubkey);
const nextValue =
normalized || (typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : null);
if (activeProfilePubkey === nextValue) {
if (persist) {
persistSavedProfiles({ persistActive: true });
}
return activeProfilePubkey;
}
activeProfilePubkey = nextValue;
ensureModerationSettingsForKey(getActiveModerationStoreKey());
if (persist) {
persistSavedProfiles({ persistActive: true });
}
return activeProfilePubkey;
}
export function mutateSavedProfiles(mutator, { persist = true, persistActive = true } = {}) {
if (typeof mutator !== "function") {
return { changed: false, profiles: getSavedProfiles() };
}
const draft = savedProfiles.slice();
const result = mutator(draft);
const nextProfiles = Array.isArray(result) ? result : draft;
const changed = hasSavedProfilesChanged(savedProfiles, nextProfiles);
if (changed) {
savedProfiles = nextProfiles;
if (persist) {
persistSavedProfiles({ persistActive });
}
} else if (persist && persistActive === false) {
persistSavedProfiles({ persistActive: false });
}
return { changed, profiles: getSavedProfiles() };
}
export function setSavedProfiles(nextProfiles, options) {
return mutateSavedProfiles(
() => (Array.isArray(nextProfiles) ? nextProfiles.slice() : []),
options
);
}
export function readSavedProfilesPayloadFromStorage() {
if (typeof localStorage === "undefined") {
return null;
}
const raw = localStorage.getItem(SAVED_PROFILES_STORAGE_KEY);
if (!raw) {
return null;
}
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") {
return null;
}
return parsed;
} catch (error) {
userLogger.warn(
"[cache.readSavedProfilesPayloadFromStorage] Failed to parse payload:",
error
);
return null;
}
}
function writeSavedProfilesPayloadToStorage(payload) {
if (typeof localStorage === "undefined") {
return;
}
try {
localStorage.setItem(SAVED_PROFILES_STORAGE_KEY, JSON.stringify(payload));
} catch (error) {
const isQuotaError =
error &&
(error.name === "QuotaExceededError" ||
error.name === "NS_ERROR_DOM_QUOTA_REACHED" ||
error.code === 22 ||
error.code === 1014);
if (isQuotaError) {
userLogger.warn(
"[cache.writeSavedProfilesPayloadToStorage] Storage quota exceeded; keeping in-memory copy only.",
error
);
} else {
userLogger.warn(
"[cache.writeSavedProfilesPayloadToStorage] Failed to persist saved profiles:",
error
);
}
}
}
export function persistSavedProfiles({ persistActive = true } = {}) {
if (typeof localStorage === "undefined") {
return;
}
if (!savedProfiles.length && !activeProfilePubkey) {
try {
localStorage.removeItem(SAVED_PROFILES_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistSavedProfiles] Failed to remove empty payload:",
error
);
}
return;
}
let activePubkeyToPersist = activeProfilePubkey || null;
if (!persistActive) {
const storedPayload = readSavedProfilesPayloadFromStorage();
if (storedPayload && typeof storedPayload === "object") {
const candidate =
typeof storedPayload.activePubkey === "string"
? storedPayload.activePubkey
: typeof storedPayload.activePubKey === "string"
? storedPayload.activePubKey
: null;
const normalizedStored = normalizeHexPubkey(candidate);
if (normalizedStored) {
activePubkeyToPersist = normalizedStored;
} else if (candidate === null) {
activePubkeyToPersist = null;
}
}
}
const payload = {
version: SAVED_PROFILES_STORAGE_VERSION,
entries: savedProfiles.map((entry) => ({
pubkey: entry.pubkey,
npub:
typeof entry.npub === "string" && entry.npub.trim() ? entry.npub.trim() : null,
name: typeof entry.name === "string" ? entry.name : "",
picture: typeof entry.picture === "string" ? entry.picture : "",
authType:
typeof entry.authType === "string" && entry.authType.trim()
? entry.authType.trim()
: null,
providerId:
typeof entry.providerId === "string" && entry.providerId.trim()
? entry.providerId.trim()
: null,
})),
activePubkey: activePubkeyToPersist,
};
writeSavedProfilesPayloadToStorage(payload);
}
function processSavedProfilesPayload(parsed) {
const result = {
profiles: [],
activePubkey: null,
hasExplicitActiveProfile: false,
needsRewrite: false,
};
if (
!parsed ||
typeof parsed !== "object" ||
parsed.version !== SAVED_PROFILES_STORAGE_VERSION
) {
result.needsRewrite = true;
return result;
}
const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
const seenPubkeys = new Set();
for (const candidate of entries) {
if (!candidate || typeof candidate !== "object") {
continue;
}
const normalizedPubkey = normalizeHexPubkey(candidate.pubkey);
if (!normalizedPubkey || seenPubkeys.has(normalizedPubkey)) {
result.needsRewrite = true;
continue;
}
seenPubkeys.add(normalizedPubkey);
const rawAuthType =
typeof candidate.authType === "string" ? candidate.authType : null;
let storedAuthType = null;
if (rawAuthType) {
const trimmedAuthType = rawAuthType.trim();
if (trimmedAuthType) {
storedAuthType = trimmedAuthType;
if (trimmedAuthType !== rawAuthType) {
result.needsRewrite = true;
}
}
}
if (!storedAuthType) {
storedAuthType = "nip07";
if (rawAuthType !== "nip07") {
result.needsRewrite = true;
}
}
const rawProviderId =
typeof candidate.providerId === "string" ? candidate.providerId : null;
let storedProviderId = null;
if (rawProviderId) {
const trimmedProviderId = rawProviderId.trim();
if (trimmedProviderId) {
storedProviderId = trimmedProviderId;
if (trimmedProviderId !== rawProviderId) {
result.needsRewrite = true;
}
}
}
if (!storedProviderId && storedAuthType) {
storedProviderId = storedAuthType;
if (rawProviderId !== storedProviderId) {
result.needsRewrite = true;
}
}
const npub =
typeof candidate.npub === "string" && candidate.npub.trim()
? candidate.npub.trim()
: null;
const entry = {
pubkey: normalizedPubkey,
npub,
name: typeof candidate.name === "string" ? candidate.name : "",
picture: typeof candidate.picture === "string" ? candidate.picture : "",
authType: storedAuthType,
providerId: storedProviderId,
};
if (
typeof candidate.npub === "string" &&
candidate.npub.trim() !== candidate.npub
) {
result.needsRewrite = true;
}
if (
entry.npub &&
typeof candidate.npub !== "string" &&
entry.npub !== candidate.npub
) {
result.needsRewrite = true;
}
result.profiles.push(entry);
}
let hasActiveField = false;
let activeCandidate;
if (Object.prototype.hasOwnProperty.call(parsed, "activePubkey")) {
hasActiveField = true;
activeCandidate = parsed.activePubkey;
} else if (Object.prototype.hasOwnProperty.call(parsed, "activePubKey")) {
hasActiveField = true;
activeCandidate = parsed.activePubKey;
}
if (hasActiveField) {
result.hasExplicitActiveProfile = true;
if (typeof activeCandidate === "string") {
const normalizedActive = normalizeHexPubkey(activeCandidate);
if (normalizedActive && seenPubkeys.has(normalizedActive)) {
result.activePubkey = normalizedActive;
} else if (activeCandidate) {
result.needsRewrite = true;
}
} else if (activeCandidate !== null && activeCandidate !== undefined) {
result.needsRewrite = true;
}
}
return result;
}
export function loadSavedProfilesFromStorage() {
savedProfiles = [];
activeProfilePubkey = null;
hadExplicitActiveProfile = false;
if (typeof localStorage === "undefined") {
return {
profiles: getSavedProfiles(),
activePubkey: activeProfilePubkey,
hasExplicitActiveProfile: hadExplicitActiveProfile,
};
}
const raw = localStorage.getItem(SAVED_PROFILES_STORAGE_KEY);
if (!raw) {
return {
profiles: getSavedProfiles(),
activePubkey: activeProfilePubkey,
hasExplicitActiveProfile: hadExplicitActiveProfile,
};
}
let needsRewrite = false;
try {
const parsed = JSON.parse(raw);
const result = processSavedProfilesPayload(parsed);
savedProfiles = result.profiles;
activeProfilePubkey = result.activePubkey;
hadExplicitActiveProfile = result.hasExplicitActiveProfile;
needsRewrite = result.needsRewrite;
} catch (error) {
userLogger.warn(
"[cache.loadSavedProfilesFromStorage] Failed to parse payload:",
error,
);
needsRewrite = true;
}
if (
!activeProfilePubkey &&
savedProfiles.length &&
!hadExplicitActiveProfile
) {
activeProfilePubkey = savedProfiles[0].pubkey;
needsRewrite = true;
}
if (needsRewrite) {
persistSavedProfiles();
}
return {
profiles: getSavedProfiles(),
activePubkey: activeProfilePubkey,
hasExplicitActiveProfile: hadExplicitActiveProfile,
};
}
export function syncSavedProfileFromCache(pubkey, { persist = false } = {}) {
const normalized = normalizeHexPubkey(pubkey);
if (!normalized) {
return false;
}
const cacheEntry = profileCache.get(normalized);
if (!cacheEntry || typeof cacheEntry !== "object") {
return false;
}
let updated = false;
mutateSavedProfiles((profiles) => {
const next = profiles.slice();
const index = next.findIndex((entry) => entry && entry.pubkey === normalized);
if (index < 0) {
return profiles;
}
const existing = next[index] || {};
const profile = cacheEntry.profile || {};
const nextEntry = {
...existing,
name: profile.name || existing.name || "",
picture: profile.picture || existing.picture || "",
};
if (
existing.name === nextEntry.name &&
existing.picture === nextEntry.picture
) {
return profiles;
}
next[index] = nextEntry;
updated = true;
return next;
}, { persist, persistActive: false });
return updated;
}
export function loadProfileCacheFromStorage() {
if (typeof localStorage === "undefined") {
return;
}
const now = Date.now();
const raw = localStorage.getItem(PROFILE_CACHE_STORAGE_KEY);
if (!raw) {
return;
}
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") {
return;
}
if (parsed.version !== PROFILE_CACHE_VERSION) {
return;
}
const entries = parsed.entries;
if (!entries || typeof entries !== "object") {
return;
}
for (const [pubkey, entry] of Object.entries(entries)) {
if (!pubkey || !entry || typeof entry !== "object") {
continue;
}
const timestamp = typeof entry.timestamp === "number" ? entry.timestamp : 0;
if (!timestamp || now - timestamp > PROFILE_CACHE_TTL_MS) {
continue;
}
const profile = entry.profile;
if (!profile || typeof profile !== "object") {
continue;
}
const normalized = {
name: profile.name || profile.display_name || "Unknown",
picture: profile.picture || "assets/svg/default-profile.svg",
};
const about = sanitizeProfileString(profile.about || profile.aboutMe);
if (about) {
normalized.about = about;
}
const website = sanitizeProfileString(profile.website || profile.url);
if (website) {
normalized.website = website;
}
const banner = sanitizeProfileString(profile.banner || profile.header);
if (banner) {
normalized.banner = banner;
}
const lud16 = sanitizeProfileString(profile.lud16);
if (lud16) {
normalized.lud16 = lud16;
}
const lud06 = sanitizeProfileString(profile.lud06);
if (lud06) {
normalized.lud06 = lud06;
}
const lightningCandidates = [
sanitizeProfileString(profile.lightningAddress),
lud16,
lud06,
].filter(Boolean);
if (lightningCandidates.length) {
normalized.lightningAddress = lightningCandidates[0];
}
profileCache.set(pubkey, {
profile: normalized,
timestamp,
});
}
} catch (error) {
userLogger.warn("[cache.loadProfileCacheFromStorage] Failed to parse payload:", error);
}
}
export function persistProfileCacheToStorage() {
if (typeof localStorage === "undefined") {
return;
}
const now = Date.now();
const entries = {};
for (const [pubkey, entry] of profileCache.entries()) {
if (!entry || typeof entry !== "object") {
continue;
}
const timestamp = typeof entry.timestamp === "number" ? entry.timestamp : 0;
if (!timestamp || now - timestamp > PROFILE_CACHE_TTL_MS) {
profileCache.delete(pubkey);
continue;
}
entries[pubkey] = {
profile: entry.profile,
timestamp,
};
}
const payload = {
version: PROFILE_CACHE_VERSION,
savedAt: now,
entries,
};
if (Object.keys(entries).length === 0) {
try {
localStorage.removeItem(PROFILE_CACHE_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistProfileCacheToStorage] Failed to clear storage:",
error
);
}
return;
}
try {
localStorage.setItem(PROFILE_CACHE_STORAGE_KEY, JSON.stringify(payload));
} catch (error) {
userLogger.warn("[cache.persistProfileCacheToStorage] Failed to persist cache:", error);
}
}
import { profileCache as unifiedProfileCache } from "./profileCache.js";
import { HEX64_REGEX } from "../utils/hex.js";
export function getProfileCacheEntry(pubkey) {
const profile = unifiedProfileCache.getProfile(pubkey);
// Maintain backward compatibility with { profile, timestamp } return shape
// although consumers mostly check .profile
if (profile) {
// We can fetch the raw data to get the timestamp if needed, but for now
// returning a synthetic entry matches most usage patterns.
return { profile, timestamp: Date.now() };
}
return null;
}
export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) {
const entry = unifiedProfileCache.setProfile(pubkey, profile, { persist });
return entry;
}
function haveModerationSettingsChanged(previous, next) {
if (!previous || !next) {
return true;
}
const prevMuteThresholds =
previous.trustedMuteHideThresholds &&
typeof previous.trustedMuteHideThresholds === "object"
? previous.trustedMuteHideThresholds
: {};
const nextMuteThresholds =
next.trustedMuteHideThresholds &&
typeof next.trustedMuteHideThresholds === "object"
? next.trustedMuteHideThresholds
: {};
const allMuteKeys = new Set([
...Object.keys(prevMuteThresholds),
...Object.keys(nextMuteThresholds),
]);
for (const key of allMuteKeys) {
if (prevMuteThresholds[key] !== nextMuteThresholds[key]) {
return true;
}
}
return (
previous.blurThreshold !== next.blurThreshold ||
previous.autoplayBlockThreshold !== next.autoplayBlockThreshold ||
previous.trustedMuteHideThreshold !== next.trustedMuteHideThreshold ||
previous.trustedSpamHideThreshold !== next.trustedSpamHideThreshold
);
}
function getModerationStoreKeyForPubkey(pubkey) {
const normalized = normalizeHexPubkey(pubkey);
return normalized || GLOBAL_MODERATION_PROFILE_KEY;
}
function getActiveModerationStoreKey() {
return getModerationStoreKeyForPubkey(activeProfilePubkey);
}
function ensureModerationSettingsForKey(key) {
const storeKey = key || GLOBAL_MODERATION_PROFILE_KEY;
if (!moderationSettingsStore.has(storeKey)) {
moderationSettingsStore.set(storeKey, createDefaultModerationSettings());
}
return moderationSettingsStore.get(storeKey);
}
export function getDefaultModerationSettings() {
return createDefaultModerationSettings();
}
export function getModerationSettingsForPubkey(pubkey) {
const key = getModerationStoreKeyForPubkey(pubkey);
const settings = ensureModerationSettingsForKey(key);
return { ...settings };
}
export function getModerationSettings() {
return getModerationSettingsForPubkey(activeProfilePubkey);
}
export function loadModerationSettingsFromStorage() {
moderationSettingsStore.clear();
moderationSettingsStore.set(
GLOBAL_MODERATION_PROFILE_KEY,
createDefaultModerationSettings(),
);
if (typeof localStorage === "undefined") {
return getModerationSettings();
}
const raw = localStorage.getItem(MODERATION_SETTINGS_STORAGE_KEY);
if (!raw) {
return getModerationSettings();
}
let shouldRewrite = false;
try {
const payload = JSON.parse(raw);
if (!payload || typeof payload !== "object") {
shouldRewrite = true;
} else if (payload.version === 1) {
const defaults = createDefaultModerationSettings();
const overrides =
payload.overrides && typeof payload.overrides === "object"
? payload.overrides
: {};
const legacySettings = {
blurThreshold: sanitizeModerationThreshold(
overrides.blurThreshold,
defaults.blurThreshold,
),
autoplayBlockThreshold: sanitizeModerationThreshold(
overrides.autoplayBlockThreshold,
defaults.autoplayBlockThreshold,
),
trustedMuteHideThreshold: sanitizeModerationThreshold(
overrides.trustedMuteHideThreshold,
defaults.trustedMuteHideThreshold,
),
trustedMuteHideThresholds: sanitizeModerationThresholdMap(
overrides.trustedMuteHideThresholds,
defaults.trustedMuteHideThresholds,
{ mergeFallback: true },
),
trustedSpamHideThreshold: sanitizeModerationThreshold(
overrides.trustedSpamHideThreshold,
defaults.trustedSpamHideThreshold,
),
};
moderationSettingsStore.set(
GLOBAL_MODERATION_PROFILE_KEY,
legacySettings,
);
shouldRewrite = true;
} else if (payload.version === 2 || payload.version === MODERATION_SETTINGS_STORAGE_VERSION) {
const defaults = createDefaultModerationSettings();
const entries =
payload.entries && typeof payload.entries === "object"
? payload.entries
: {};
for (const [key, entry] of Object.entries(entries)) {
if (!entry || typeof entry !== "object") {
shouldRewrite = true;
continue;
}
const overrides =
entry.overrides && typeof entry.overrides === "object"
? entry.overrides
: {};
const storeKey =
key === "default"
? GLOBAL_MODERATION_PROFILE_KEY
: getModerationStoreKeyForPubkey(key);
if (!storeKey) {
shouldRewrite = true;
continue;
}
const settings = {
blurThreshold: sanitizeModerationThreshold(
overrides.blurThreshold,
defaults.blurThreshold,
),
autoplayBlockThreshold: sanitizeModerationThreshold(
overrides.autoplayBlockThreshold,
defaults.autoplayBlockThreshold,
),
trustedMuteHideThreshold: sanitizeModerationThreshold(
overrides.trustedMuteHideThreshold,
defaults.trustedMuteHideThreshold,
),
trustedMuteHideThresholds: sanitizeModerationThresholdMap(
overrides.trustedMuteHideThresholds,
defaults.trustedMuteHideThresholds,
{ mergeFallback: true },
),
trustedSpamHideThreshold: sanitizeModerationThreshold(
overrides.trustedSpamHideThreshold,
defaults.trustedSpamHideThreshold,
),
};
moderationSettingsStore.set(storeKey, settings);
}
} else {
shouldRewrite = true;
}
} catch (error) {
userLogger.warn(
"[cache.loadModerationSettingsFromStorage] Failed to parse payload:",
error,
);
shouldRewrite = true;
}
if (shouldRewrite) {
persistModerationSettingsToStorage();
}
return getModerationSettings();
}
function persistModerationSettingsToStorage() {
if (typeof localStorage === "undefined") {
return;
}
const defaults = createDefaultModerationSettings();
const entries = {};
for (const [key, value] of moderationSettingsStore.entries()) {
const settings =
value && typeof value === "object"
? value
: createDefaultModerationSettings();
const overrides = {};
if (settings.blurThreshold !== defaults.blurThreshold) {
overrides.blurThreshold = settings.blurThreshold;
}
if (settings.autoplayBlockThreshold !== defaults.autoplayBlockThreshold) {
overrides.autoplayBlockThreshold = settings.autoplayBlockThreshold;
}
if (settings.trustedMuteHideThreshold !== defaults.trustedMuteHideThreshold) {
overrides.trustedMuteHideThreshold = settings.trustedMuteHideThreshold;
}
const trustedMuteHideThresholds = sanitizeModerationThresholdMap(
settings.trustedMuteHideThresholds,
defaults.trustedMuteHideThresholds,
{ mergeFallback: false },
);
if (Object.keys(trustedMuteHideThresholds).length > 0) {
overrides.trustedMuteHideThresholds = trustedMuteHideThresholds;
}
if (settings.trustedSpamHideThreshold !== defaults.trustedSpamHideThreshold) {
overrides.trustedSpamHideThreshold = settings.trustedSpamHideThreshold;
}
if (Object.keys(overrides).length === 0) {
continue;
}
const storageKey =
key === GLOBAL_MODERATION_PROFILE_KEY ? "default" : key;
entries[storageKey] = {
overrides,
savedAt: Date.now(),
};
}
if (Object.keys(entries).length === 0) {
try {
localStorage.removeItem(MODERATION_SETTINGS_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistModerationSettingsToStorage] Failed to clear overrides:",
error,
);
}
return;
}
const payload = {
version: MODERATION_SETTINGS_STORAGE_VERSION,
entries,
savedAt: Date.now(),
};
try {
localStorage.setItem(
MODERATION_SETTINGS_STORAGE_KEY,
JSON.stringify(payload),
);
} catch (error) {
userLogger.warn(
"[cache.persistModerationSettingsToStorage] Failed to persist overrides:",
error,
);
}
}
function getDmPrivacyStoreKeyForPubkey(pubkey) {
const normalized = normalizeHexPubkey(pubkey);
return normalized || GLOBAL_DM_PRIVACY_PROFILE_KEY;
}
function ensureDmPrivacySettingsForKey(key) {
const storeKey = key || GLOBAL_DM_PRIVACY_PROFILE_KEY;
if (!dmPrivacySettingsStore.has(storeKey)) {
dmPrivacySettingsStore.set(storeKey, createDefaultDmPrivacySettings());
}
return dmPrivacySettingsStore.get(storeKey);
}
function normalizeDmPrivacySettings(partial = {}) {
const defaults = createDefaultDmPrivacySettings();
const resolved = partial && typeof partial === "object" ? partial : {};
return {
readReceiptsEnabled:
typeof resolved.readReceiptsEnabled === "boolean"
? resolved.readReceiptsEnabled
: defaults.readReceiptsEnabled,
typingIndicatorsEnabled:
typeof resolved.typingIndicatorsEnabled === "boolean"
? resolved.typingIndicatorsEnabled
: defaults.typingIndicatorsEnabled,
};
}
export function getDmPrivacySettingsForPubkey(pubkey) {
const key = getDmPrivacyStoreKeyForPubkey(pubkey);
const settings = ensureDmPrivacySettingsForKey(key);
return { ...settings };
}
export function getDmPrivacySettings() {
return getDmPrivacySettingsForPubkey(activeProfilePubkey);
}
export function setDmPrivacySettings(partial = {}, options = {}) {
const targetPubkey =
typeof options?.pubkey === "string" ? options.pubkey : activeProfilePubkey;
const key = getDmPrivacyStoreKeyForPubkey(targetPubkey);
const existing = ensureDmPrivacySettingsForKey(key);
const normalized = normalizeDmPrivacySettings({ ...existing, ...partial });
dmPrivacySettingsStore.set(key, normalized);
persistDmPrivacySettingsToStorage();
return { ...normalized };
}
export function loadDmPrivacySettingsFromStorage() {
dmPrivacySettingsStore.clear();
dmPrivacySettingsStore.set(
GLOBAL_DM_PRIVACY_PROFILE_KEY,
createDefaultDmPrivacySettings(),
);
if (typeof localStorage === "undefined") {
return getDmPrivacySettings();
}
const raw = localStorage.getItem(DM_PRIVACY_SETTINGS_STORAGE_KEY);
if (!raw) {
return getDmPrivacySettings();
}
let shouldRewrite = false;
try {
const payload = JSON.parse(raw);
if (!payload || typeof payload !== "object") {
shouldRewrite = true;
} else if (payload.version === DM_PRIVACY_SETTINGS_STORAGE_VERSION) {
const defaults = createDefaultDmPrivacySettings();
const entries =
payload.entries && typeof payload.entries === "object"
? payload.entries
: {};
for (const [key, entry] of Object.entries(entries)) {
if (!entry || typeof entry !== "object") {
shouldRewrite = true;
continue;
}
const overrides =
entry.overrides && typeof entry.overrides === "object"
? entry.overrides
: {};
const storeKey =
key === "default"
? GLOBAL_DM_PRIVACY_PROFILE_KEY
: getDmPrivacyStoreKeyForPubkey(key);
if (!storeKey) {
shouldRewrite = true;
continue;
}
const settings = {
readReceiptsEnabled:
typeof overrides.readReceiptsEnabled === "boolean"
? overrides.readReceiptsEnabled
: defaults.readReceiptsEnabled,
typingIndicatorsEnabled:
typeof overrides.typingIndicatorsEnabled === "boolean"
? overrides.typingIndicatorsEnabled
: defaults.typingIndicatorsEnabled,
};
dmPrivacySettingsStore.set(storeKey, settings);
}
} else {
shouldRewrite = true;
}
} catch (error) {
userLogger.warn(
"[cache.loadDmPrivacySettingsFromStorage] Failed to parse payload:",
error,
);
shouldRewrite = true;
}
if (shouldRewrite) {
persistDmPrivacySettingsToStorage();
}
return getDmPrivacySettings();
}
function persistDmPrivacySettingsToStorage() {
if (typeof localStorage === "undefined") {
return;
}
const defaults = createDefaultDmPrivacySettings();
const entries = {};
for (const [key, value] of dmPrivacySettingsStore.entries()) {
const settings =
value && typeof value === "object"
? value
: createDefaultDmPrivacySettings();
const overrides = {};
if (settings.readReceiptsEnabled !== defaults.readReceiptsEnabled) {
overrides.readReceiptsEnabled = settings.readReceiptsEnabled;
}
if (settings.typingIndicatorsEnabled !== defaults.typingIndicatorsEnabled) {
overrides.typingIndicatorsEnabled = settings.typingIndicatorsEnabled;
}
if (Object.keys(overrides).length === 0) {
continue;
}
const storageKey =
key === GLOBAL_DM_PRIVACY_PROFILE_KEY ? "default" : key;
entries[storageKey] = {
overrides,
savedAt: Date.now(),
};
}
if (Object.keys(entries).length === 0) {
try {
localStorage.removeItem(DM_PRIVACY_SETTINGS_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistDmPrivacySettingsToStorage] Failed to clear overrides:",
error,
);
}
return;
}
const payload = {
version: DM_PRIVACY_SETTINGS_STORAGE_VERSION,
entries,
savedAt: Date.now(),
};
try {
localStorage.setItem(
DM_PRIVACY_SETTINGS_STORAGE_KEY,
JSON.stringify(payload),
);
} catch (error) {
userLogger.warn(
"[cache.persistDmPrivacySettingsToStorage] Failed to persist overrides:",
error,
);
}
}
export function setModerationSettings(partial = {}, { persist = true } = {}) {
const key = getActiveModerationStoreKey();
const defaults = createDefaultModerationSettings();
const current = ensureModerationSettingsForKey(key);
const next = { ...current };
let changed = false;
if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) {
const value = partial.blurThreshold;
const sanitized =
value === null
? defaults.blurThreshold
: sanitizeModerationThreshold(value, defaults.blurThreshold);
if (next.blurThreshold !== sanitized) {
next.blurThreshold = sanitized;
changed = true;
}
}
if (Object.prototype.hasOwnProperty.call(partial, "autoplayBlockThreshold")) {
const value = partial.autoplayBlockThreshold;
const sanitized =
value === null
? defaults.autoplayBlockThreshold
: sanitizeModerationThreshold(value, defaults.autoplayBlockThreshold);
if (next.autoplayBlockThreshold !== sanitized) {
next.autoplayBlockThreshold = sanitized;
changed = true;
}
}
if (Object.prototype.hasOwnProperty.call(partial, "trustedMuteHideThreshold")) {
const value = partial.trustedMuteHideThreshold;
const sanitized =
value === null
? defaults.trustedMuteHideThreshold
: sanitizeModerationThreshold(value, defaults.trustedMuteHideThreshold);
if (next.trustedMuteHideThreshold !== sanitized) {
next.trustedMuteHideThreshold = sanitized;
changed = true;
}
}
if (
Object.prototype.hasOwnProperty.call(partial, "trustedMuteHideThresholds")
) {
const value = partial.trustedMuteHideThresholds;
const sanitized =
value === null
? defaults.trustedMuteHideThresholds
: sanitizeModerationThresholdMap(value, defaults.trustedMuteHideThresholds, {
mergeFallback: true,
});
const current =
next.trustedMuteHideThresholds &&
typeof next.trustedMuteHideThresholds === "object"
? next.trustedMuteHideThresholds
: {};
const nextKeys = new Set([
...Object.keys(current),
...Object.keys(sanitized),
]);
let mapChanged = false;
for (const key of nextKeys) {
if (current[key] !== sanitized[key]) {
mapChanged = true;
break;
}
}
if (mapChanged) {
next.trustedMuteHideThresholds = { ...sanitized };
changed = true;
}
}
if (Object.prototype.hasOwnProperty.call(partial, "trustedSpamHideThreshold")) {
const value = partial.trustedSpamHideThreshold;
const sanitized =
value === null
? defaults.trustedSpamHideThreshold
: sanitizeModerationThreshold(value, defaults.trustedSpamHideThreshold);
if (next.trustedSpamHideThreshold !== sanitized) {
next.trustedSpamHideThreshold = sanitized;
changed = true;
}
}
if (!changed) {
if (persist) {
persistModerationSettingsToStorage();
}
return ensureModerationSettingsForKey(key);
}
moderationSettingsStore.set(key, next);
if (persist) {
persistModerationSettingsToStorage();
}
return ensureModerationSettingsForKey(key);
}
export function resetModerationSettings({ persist = true } = {}) {
const key = getActiveModerationStoreKey();
const previous = ensureModerationSettingsForKey(key);
const defaults = createDefaultModerationSettings();
moderationSettingsStore.set(key, defaults);
const changed = haveModerationSettingsChanged(previous, defaults);
if (persist) {
persistModerationSettingsToStorage();
}
if (!changed) {
return ensureModerationSettingsForKey(key);
}
return ensureModerationSettingsForKey(key);
}
export function getModerationOverridesList() {
return Array.from(moderationOverrides.values()).map((entry) => ({ ...entry }));
}
function findModerationOverrideByEventId(eventId) {
for (const entry of moderationOverrides.values()) {
if (entry?.eventId === eventId) {
return entry;
}
}
return null;
}
export function getModerationOverride(eventIdOrDescriptor, authorPubkey) {
const normalized = normalizeModerationOverrideDescriptor(
eventIdOrDescriptor,
authorPubkey,
);
if (!normalized) {
return null;
}
const entry =
moderationOverrides.get(normalized.key) ||
findModerationOverrideByEventId(normalized.eventId);
if (!entry) {
return null;
}
return { ...entry };
}
export function loadModerationOverridesFromStorage() {
if (typeof localStorage === "undefined") {
return;
}
const raw = localStorage.getItem(MODERATION_OVERRIDE_STORAGE_KEY);
if (!raw) {
return;
}
let shouldRewrite = false;
try {
const payload = JSON.parse(raw);
if (!payload || typeof payload !== "object") {
return;
}
const entries = [];
if (payload.version === MODERATION_OVERRIDE_STORAGE_VERSION) {
if (Array.isArray(payload.entries)) {
entries.push(...payload.entries);
} else {
shouldRewrite = true;
}
} else {
return;
}
moderationOverrides.clear();
for (const entry of entries) {
if (!entry || entry.showAnyway !== true) {
continue;
}
const normalized = normalizeModerationOverrideDescriptor(entry);
if (!normalized) {
continue;
}
const updatedAt = Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now();
moderationOverrides.set(normalized.key, {
eventId: normalized.eventId,
authorPubkey: normalized.authorPubkey,
showAnyway: true,
updatedAt,
});
}
} catch (error) {
moderationOverrides.clear();
userLogger.warn(
"[cache.loadModerationOverridesFromStorage] Failed to parse payload:",
error,
);
return;
}
if (shouldRewrite) {
persistModerationOverridesToStorage();
}
}
export function persistModerationOverridesToStorage() {
if (typeof localStorage === "undefined") {
return;
}
const entries = [];
for (const entry of moderationOverrides.values()) {
if (!entry || entry.showAnyway !== true) {
continue;
}
const normalized = normalizeModerationOverrideDescriptor(entry);
if (!normalized) {
continue;
}
const updatedAt = Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now();
entries.push({
eventId: normalized.eventId,
authorPubkey: normalized.authorPubkey,
showAnyway: true,
updatedAt,
});
}
if (entries.length === 0) {
try {
localStorage.removeItem(MODERATION_OVERRIDE_STORAGE_KEY);
} catch (error) {
userLogger.warn(
"[cache.persistModerationOverridesToStorage] Failed to clear overrides:",
error,
);
}
return;
}
const payload = {
version: MODERATION_OVERRIDE_STORAGE_VERSION,
savedAt: Date.now(),
entries,
};
try {
localStorage.setItem(
MODERATION_OVERRIDE_STORAGE_KEY,
JSON.stringify(payload),
);
} catch (error) {
userLogger.warn(
"[cache.persistModerationOverridesToStorage] Failed to persist overrides:",
error,
);
}
}
export function setModerationOverride(
eventIdOrDescriptor,
override = {},
{ persist = true } = {},
) {
const normalized = normalizeModerationOverrideDescriptor(
eventIdOrDescriptor,
override?.authorPubkey,
);
if (!normalized) {
return null;
}
const showAnyway = override?.showAnyway === true;
if (!showAnyway) {
const removed = moderationOverrides.delete(normalized.key);
if (removed && persist) {
persistModerationOverridesToStorage();
}
return null;
}
const updatedAt = Number.isFinite(override?.updatedAt)
? Math.floor(override.updatedAt)
: Date.now();
const existing = moderationOverrides.get(normalized.key);
const nextEntry = {
eventId: normalized.eventId,
authorPubkey: normalized.authorPubkey,
showAnyway: true,
updatedAt,
};
const changed =
!existing ||
existing.showAnyway !== nextEntry.showAnyway ||
existing.updatedAt !== nextEntry.updatedAt ||
existing.eventId !== nextEntry.eventId ||
existing.authorPubkey !== nextEntry.authorPubkey;
moderationOverrides.set(normalized.key, nextEntry);
if (persist && changed) {
persistModerationOverridesToStorage();
}
return { ...nextEntry };
}
export function clearModerationOverride(
eventIdOrDescriptor,
{ persist = true } = {},
) {
const normalized = normalizeModerationOverrideDescriptor(eventIdOrDescriptor);
if (!normalized) {
return false;
}
let removed = moderationOverrides.delete(normalized.key);
for (const [key, entry] of moderationOverrides.entries()) {
if (entry?.eventId === normalized.eventId) {
moderationOverrides.delete(key);
removed = true;
}
}
if (removed && persist) {
persistModerationOverridesToStorage();
}
return removed;
}
// ---- Account-level ("trusted creator") web-of-trust overrides ---------------
// Keyed by author hex; a showAnyway entry suppresses the WoT blur/hide/warning
// for EVERY video by that author until reset. Mirrors the per-video store above.
function normalizeAuthorOverrideHex(pubkey) {
const hex = normalizeHexPubkey(pubkey);
return hex ? hex.toLowerCase() : "";
}
export function getAuthorModerationOverridesList() {
return Array.from(authorModerationOverrides.values()).map((entry) => ({
...entry,
}));
}
export function getAuthorModerationOverride(pubkey) {
const hex = normalizeAuthorOverrideHex(pubkey);
if (!hex) {
return null;
}
const entry = authorModerationOverrides.get(hex);
return entry ? { ...entry } : null;
}
export function hasAuthorModerationOverride(pubkey) {
const hex = normalizeAuthorOverrideHex(pubkey);
if (!hex) {
return false;
}
return authorModerationOverrides.get(hex)?.showAnyway === true;
}
export function setAuthorModerationOverride(
pubkey,
override = {},
{ persist = true } = {},
) {
const hex = normalizeAuthorOverrideHex(pubkey);
if (!hex) {
return null;
}
const showAnyway = override?.showAnyway === true;
if (!showAnyway) {
const removed = authorModerationOverrides.delete(hex);
if (removed && persist) {
persistAuthorModerationOverridesToStorage();
}
return null;
}
const updatedAt = Number.isFinite(override?.updatedAt)
? Math.floor(override.updatedAt)
: Date.now();
const existing = authorModerationOverrides.get(hex);
const nextEntry = { authorPubkey: hex, showAnyway: true, updatedAt };
const changed =
!existing ||
existing.showAnyway !== true ||
existing.updatedAt !== updatedAt;
authorModerationOverrides.set(hex, nextEntry);
if (persist && changed) {
persistAuthorModerationOverridesToStorage();
}
return { ...nextEntry };
}
export function clearAuthorModerationOverride(pubkey, { persist = true } = {}) {
const hex = normalizeAuthorOverrideHex(pubkey);
if (!hex) {
return false;
}
const removed = authorModerationOverrides.delete(hex);
if (removed && persist) {
persistAuthorModerationOverridesToStorage();
}
return removed;
}
export function loadAuthorModerationOverridesFromStorage() {
if (typeof localStorage === "undefined") {
return;
}
const raw = localStorage.getItem(AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY);
if (!raw) {
return;
}
try {
const payload = JSON.parse(raw);
if (
!payload ||
typeof payload !== "object" ||
payload.version !== AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION ||
!Array.isArray(payload.entries)
) {
return;
}
authorModerationOverrides.clear();
for (const entry of payload.entries) {
if (!entry || entry.showAnyway !== true) {
continue;
}
const hex = normalizeAuthorOverrideHex(entry.authorPubkey || entry.pubkey);
if (!hex) {
continue;
}
const updatedAt = Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now();
authorModerationOverrides.set(hex, {
authorPubkey: hex,
showAnyway: true,
updatedAt,
});
}
} catch (error) {
authorModerationOverrides.clear();
userLogger.warn(
"[cache.loadAuthorModerationOverridesFromStorage] Failed to parse payload:",
error,
);
}
}
export function persistAuthorModerationOverridesToStorage() {
if (typeof localStorage === "undefined") {
return;
}
const entries = [];
for (const entry of authorModerationOverrides.values()) {
if (!entry || entry.showAnyway !== true) {
continue;
}
entries.push({
authorPubkey: entry.authorPubkey,
showAnyway: true,
updatedAt: Number.isFinite(entry.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now(),
});
}
try {
if (entries.length === 0) {
localStorage.removeItem(AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY);
return;
}
localStorage.setItem(
AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY,
JSON.stringify({
version: AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION,
savedAt: Date.now(),
entries,
}),
);
} catch (error) {
userLogger.warn(
"[cache.persistAuthorModerationOverridesToStorage] Failed to persist overrides:",
error,
);
}
}
function buildUrlProbeKey(url, options = {}) {
const trimmed = typeof url === "string" ? url : "";
const mode = options?.confirmPlayable ? "playable" : "basic";
return `${trimmed}::${mode}`;
}
function isUrlHealthEntryFresh(entry, url) {
if (!entry || typeof entry !== "object") {
return false;
}
const now = Date.now();
if (typeof entry.expiresAt !== "number" || entry.expiresAt <= now) {
return false;
}
if (url && entry.url && entry.url !== url) {
return false;
}
return true;
}
export function getCachedUrlHealth(eventId, url) {
if (!eventId) {
return null;
}
const entry = urlHealthCache.get(eventId);
if (isUrlHealthEntryFresh(entry, url)) {
return entry;
}
if (entry) {
urlHealthCache.delete(eventId);
}
const stored = readUrlHealthFromStorage(eventId);
if (!isUrlHealthEntryFresh(stored, url)) {
if (stored) {
removeUrlHealthFromStorage(eventId);
}
return null;
}
urlHealthCache.set(eventId, stored);
return stored;
}
export function storeUrlHealth(eventId, url, result, ttlMs = URL_HEALTH_TTL_MS) {
if (!eventId) {
return null;
}
const ttl = typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : URL_HEALTH_TTL_MS;
const now = Date.now();
const entry = {
status: result?.status || "checking",
message: result?.message || "⏳ CDN",
url: url || result?.url || "",
expiresAt: now + ttl,
lastCheckedAt: now,
};
urlHealthCache.set(eventId, entry);
writeUrlHealthToStorage(eventId, entry);
return entry;
}
export function setInFlightUrlProbe(eventId, url, promise, options = {}) {
if (!eventId || !promise) {
return;
}
const key = buildUrlProbeKey(url, options);
urlHealthInFlight.set(eventId, { promise, key });
promise.finally(() => {
const current = urlHealthInFlight.get(eventId);
if (current && current.promise === promise) {
urlHealthInFlight.delete(eventId);
}
});
}
export function getInFlightUrlProbe(eventId, url, options = {}) {
if (!eventId) {
return null;
}
const entry = urlHealthInFlight.get(eventId);
if (!entry) {
return null;
}
const key = buildUrlProbeKey(url, options);
if (entry.key && entry.key !== key) {
return null;
}
return entry.promise;
}
export const urlHealthConstants = {
URL_HEALTH_TTL_MS,
URL_HEALTH_TIMEOUT_RETRY_MS,
URL_PROBE_TIMEOUT_RETRY_MS,
};