Adjust moderation defaults and store per-profile settings

This commit is contained in:
thePR0M3TH3AN committed 2025-10-21 19:26:25 -04:00
1 parent 213ba3970f
commit 15b94cfff3
7 files changed
+240 -121

No files matched your search

+2 -2
View File
@@ -5364,10 +5364,10 @@ class Application {
const defaults = this.defaultModerationSettings || getDefaultModerationSettings();
const defaultBlur = Number.isFinite(defaults?.blurThreshold)
? Math.max(0, Math.floor(defaults.blurThreshold))
: 3;
: 1;
const defaultAutoplay = Number.isFinite(defaults?.autoplayBlockThreshold)
? Math.max(0, Math.floor(defaults.autoplayBlockThreshold))
: 2;
: 1;
const defaultTrustedMuteHide = Number.isFinite(
defaults?.trustedMuteHideThreshold,
)
+2 -2
View File
@@ -4,8 +4,8 @@ import { accessControl } from "../accessControl.js";
import { userBlocks, USER_BLOCK_EVENTS } from "../userBlocks.js";
import logger from "../utils/logger.js";
const AUTOPLAY_TRUST_THRESHOLD = 2;
const BLUR_TRUST_THRESHOLD = 3;
const AUTOPLAY_TRUST_THRESHOLD = 1;
const BLUR_TRUST_THRESHOLD = 1;
function normalizeUserLogger(candidate) {
if (
+185 -74
View File
@@ -11,6 +11,7 @@ import {
removeUrlHealthFromStorage,
writeUrlHealthToStorage,
} from "../utils/storage.js";
import createProfileSettingsStore from "./profileSettingsStore.js";
const PROFILE_CACHE_STORAGE_KEY = "bitvid:profileCache:v1";
const PROFILE_CACHE_VERSION = 1;
@@ -29,7 +30,7 @@ const HEX64_REGEX = /^[0-9a-f]{64}$/i;
const MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v1";
const MODERATION_OVERRIDE_STORAGE_VERSION = 1;
const MODERATION_SETTINGS_STORAGE_KEY = "bitvid:moderationSettings:v1";
const MODERATION_SETTINGS_STORAGE_VERSION = 1;
const MODERATION_SETTINGS_STORAGE_VERSION = 2;
function computeDefaultModerationSettings() {
let runtimeMuteHide = DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD;
@@ -58,8 +59,8 @@ function computeDefaultModerationSettings() {
}
return {
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: sanitizeModerationThreshold(
runtimeMuteHide,
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
@@ -71,12 +72,19 @@ function computeDefaultModerationSettings() {
};
}
const DEFAULT_MODERATION_SETTINGS = Object.freeze(computeDefaultModerationSettings());
function createDefaultModerationSettings() {
return { ...computeDefaultModerationSettings() };
}
const GLOBAL_MODERATION_PROFILE_KEY = "__global__";
const moderationSettingsStore = createProfileSettingsStore({
clone: (value) => ({ ...value }),
});
moderationSettingsStore.set(
GLOBAL_MODERATION_PROFILE_KEY,
createDefaultModerationSettings(),
);
function sanitizeModerationThreshold(value, fallback) {
if (value === null || value === undefined) {
return fallback;
@@ -111,7 +119,6 @@ const profileCache = new Map();
const urlHealthCache = new Map();
const urlHealthInFlight = new Map();
const moderationOverrides = new Map();
let moderationSettings = createDefaultModerationSettings();
function hasSavedProfilesChanged(previousProfiles, nextProfiles) {
if (previousProfiles === nextProfiles) {
@@ -228,6 +235,7 @@ export function setActiveProfilePubkey(pubkey, { persist = true } = {}) {
}
activeProfilePubkey = nextValue;
ensureModerationSettingsForKey(getActiveModerationStoreKey());
if (persist) {
persistSavedProfiles({ persistActive: true });
}
@@ -809,68 +817,152 @@ function haveModerationSettingsChanged(previous, next) {
);
}
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 { ...moderationSettings };
return getModerationSettingsForPubkey(activeProfilePubkey);
}
export function loadModerationSettingsFromStorage() {
moderationSettingsStore.clear();
moderationSettingsStore.set(
GLOBAL_MODERATION_PROFILE_KEY,
createDefaultModerationSettings(),
);
if (typeof localStorage === "undefined") {
moderationSettings = createDefaultModerationSettings();
return getModerationSettings();
}
const raw = localStorage.getItem(MODERATION_SETTINGS_STORAGE_KEY);
if (!raw) {
moderationSettings = createDefaultModerationSettings();
return getModerationSettings();
}
let shouldRewrite = false;
try {
const payload = JSON.parse(raw);
if (!payload || typeof payload !== "object") {
moderationSettings = createDefaultModerationSettings();
return getModerationSettings();
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,
),
trustedSpamHideThreshold: sanitizeModerationThreshold(
overrides.trustedSpamHideThreshold,
defaults.trustedSpamHideThreshold,
),
};
moderationSettingsStore.set(
GLOBAL_MODERATION_PROFILE_KEY,
legacySettings,
);
shouldRewrite = true;
} else if (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,
),
trustedSpamHideThreshold: sanitizeModerationThreshold(
overrides.trustedSpamHideThreshold,
defaults.trustedSpamHideThreshold,
),
};
moderationSettingsStore.set(storeKey, settings);
}
} else {
shouldRewrite = true;
}
if (payload.version !== MODERATION_SETTINGS_STORAGE_VERSION) {
moderationSettings = createDefaultModerationSettings();
return getModerationSettings();
}
const defaults = createDefaultModerationSettings();
const overrides =
payload.overrides && typeof payload.overrides === "object"
? payload.overrides
: {};
moderationSettings = {
blurThreshold: sanitizeModerationThreshold(
overrides.blurThreshold,
defaults.blurThreshold,
),
autoplayBlockThreshold: sanitizeModerationThreshold(
overrides.autoplayBlockThreshold,
defaults.autoplayBlockThreshold,
),
trustedMuteHideThreshold: sanitizeModerationThreshold(
overrides.trustedMuteHideThreshold,
defaults.trustedMuteHideThreshold,
),
trustedSpamHideThreshold: sanitizeModerationThreshold(
overrides.trustedSpamHideThreshold,
defaults.trustedSpamHideThreshold,
),
};
} catch (error) {
moderationSettings = createDefaultModerationSettings();
userLogger.warn(
"[cache.loadModerationSettingsFromStorage] Failed to parse payload:",
error,
);
shouldRewrite = true;
}
if (shouldRewrite) {
persistModerationSettingsToStorage();
}
return getModerationSettings();
@@ -882,31 +974,46 @@ function persistModerationSettingsToStorage() {
}
const defaults = createDefaultModerationSettings();
const overrides = {};
const entries = {};
if (moderationSettings.blurThreshold !== defaults.blurThreshold) {
overrides.blurThreshold = moderationSettings.blurThreshold;
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;
}
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 (moderationSettings.autoplayBlockThreshold !== defaults.autoplayBlockThreshold) {
overrides.autoplayBlockThreshold = moderationSettings.autoplayBlockThreshold;
}
if (
moderationSettings.trustedMuteHideThreshold !==
defaults.trustedMuteHideThreshold
) {
overrides.trustedMuteHideThreshold = moderationSettings.trustedMuteHideThreshold;
}
if (
moderationSettings.trustedSpamHideThreshold !==
defaults.trustedSpamHideThreshold
) {
overrides.trustedSpamHideThreshold = moderationSettings.trustedSpamHideThreshold;
}
if (Object.keys(overrides).length === 0) {
if (Object.keys(entries).length === 0) {
try {
localStorage.removeItem(MODERATION_SETTINGS_STORAGE_KEY);
} catch (error) {
@@ -920,7 +1027,7 @@ function persistModerationSettingsToStorage() {
const payload = {
version: MODERATION_SETTINGS_STORAGE_VERSION,
overrides,
entries,
savedAt: Date.now(),
};
@@ -938,8 +1045,10 @@ function persistModerationSettingsToStorage() {
}
export function setModerationSettings(partial = {}, { persist = true } = {}) {
const key = getActiveModerationStoreKey();
const defaults = createDefaultModerationSettings();
const next = { ...moderationSettings };
const current = ensureModerationSettingsForKey(key);
const next = { ...current };
let changed = false;
if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) {
@@ -994,33 +1103,35 @@ export function setModerationSettings(partial = {}, { persist = true } = {}) {
if (persist) {
persistModerationSettingsToStorage();
}
return { ...moderationSettings };
return ensureModerationSettingsForKey(key);
}
moderationSettings = next;
moderationSettingsStore.set(key, next);
if (persist) {
persistModerationSettingsToStorage();
}
return { ...moderationSettings };
return ensureModerationSettingsForKey(key);
}
export function resetModerationSettings({ persist = true } = {}) {
const previous = moderationSettings;
moderationSettings = createDefaultModerationSettings();
const key = getActiveModerationStoreKey();
const previous = ensureModerationSettingsForKey(key);
const defaults = createDefaultModerationSettings();
moderationSettingsStore.set(key, defaults);
const changed = haveModerationSettingsChanged(previous, moderationSettings);
const changed = haveModerationSettingsChanged(previous, defaults);
if (persist) {
persistModerationSettingsToStorage();
}
if (!changed) {
return { ...moderationSettings };
return ensureModerationSettingsForKey(key);
}
return { ...moderationSettings };
return ensureModerationSettingsForKey(key);
}
export function getModerationOverridesMap() {
+2 -2
View File
@@ -59,8 +59,8 @@ function createInternalDefaultNwcSettings() {
}
const DEFAULT_INTERNAL_MODERATION_SETTINGS = Object.freeze({
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: getTrustedMuteHideThreshold(),
trustedSpamHideThreshold: getTrustedSpamHideThreshold(),
});
+23 -15
View File
@@ -244,14 +244,16 @@ test("moderation thresholds emit logger hooks only when crossing", async (t) =>
const createReporterEntry = (timestamp) => new Map([[reportType, { created_at: timestamp }]]);
const expectSingleAction = (expectedAction, expectedCount) => {
assert.equal(calls.length, 1);
const [message, detail] = calls[0];
assert.equal(message, "[moderationService] moderation threshold crossed");
assert.equal(detail.action, expectedAction);
assert.equal(detail.eventId, eventId);
assert.equal(detail.reportType, reportType);
assert.equal(detail.trustedCount, expectedCount);
const expectActions = (...expected) => {
assert.equal(calls.length, expected.length);
expected.forEach((entry, index) => {
const [message, detail] = calls[index];
assert.equal(message, "[moderationService] moderation threshold crossed");
assert.equal(detail.action, entry.action);
assert.equal(detail.eventId, eventId);
assert.equal(detail.reportType, reportType);
assert.equal(detail.trustedCount, entry.count);
});
calls.length = 0;
};
@@ -260,34 +262,40 @@ test("moderation thresholds emit logger hooks only when crossing", async (t) =>
};
reports.set(trustedReporters[0], createReporterEntry(100));
service.recomputeSummaryForEvent(eventId);
expectActions(
{ action: "autoplay-block-enabled", count: 1 },
{ action: "blur-enabled", count: 1 },
);
service.recomputeSummaryForEvent(eventId);
expectNoAction();
reports.set(trustedReporters[1], createReporterEntry(200));
service.recomputeSummaryForEvent(eventId);
expectSingleAction("autoplay-block-enabled", 2);
service.recomputeSummaryForEvent(eventId);
expectNoAction();
reports.set(trustedReporters[2], createReporterEntry(300));
service.recomputeSummaryForEvent(eventId);
expectSingleAction("blur-enabled", 3);
expectNoAction();
service.recomputeSummaryForEvent(eventId);
expectNoAction();
reports.delete(trustedReporters[2]);
service.recomputeSummaryForEvent(eventId);
expectSingleAction("blur-cleared", 2);
expectNoAction();
reports.delete(trustedReporters[1]);
service.recomputeSummaryForEvent(eventId);
expectSingleAction("autoplay-block-cleared", 1);
expectNoAction();
reports.delete(trustedReporters[0]);
service.recomputeSummaryForEvent(eventId);
expectNoAction();
expectActions(
{ action: "autoplay-block-cleared", count: 0 },
{ action: "blur-cleared", count: 0 },
);
service.recomputeSummaryForEvent(eventId);
expectNoAction();
+9 -9
View File
@@ -55,8 +55,8 @@ test("moderation stage enforces admin lists without whitelist bypass", async ()
const stage = createModerationStage({
service,
reportType: "nudity",
autoplayThreshold: 2,
blurThreshold: 3,
autoplayThreshold: 1,
blurThreshold: 1,
});
const items = [
@@ -199,8 +199,8 @@ test("moderation stage applies provided thresholds", async () => {
const relaxedStage = createModerationStage({
service,
autoplayThreshold: 2,
blurThreshold: 3,
autoplayThreshold: 1,
blurThreshold: 1,
});
const strictResult = await strictStage(items, {});
@@ -281,8 +281,8 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy
const stage = createModerationStage({
service,
autoplayThreshold: 2,
blurThreshold: 3,
autoplayThreshold: 1,
blurThreshold: 1,
reportType: "nudity",
});
@@ -322,11 +322,11 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy
state.summaryById.set("muted-video", {
eventId: "muted-video",
totalTrusted: 1,
types: { nudity: { trusted: 1, total: 1, latest: 1_700_000_500 } },
totalTrusted: 0,
types: { nudity: { trusted: 0, total: 0, latest: 1_700_000_500 } },
updatedAt: 1_700_000_600,
});
state.trustedCounts.set("muted-video", 1);
state.trustedCounts.set("muted-video", 0);
state.mutersByAuthor.set(mutedHex, []);
state.mutedAuthors.delete(mutedHex);
@@ -67,16 +67,16 @@ beforeEach(async () => {
};
storedSettings = {
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: 3,
};
moderationService = {
getDefaultModerationSettings() {
return {
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: 3,
};
@@ -87,7 +87,7 @@ beforeEach(async () => {
updateModerationSettings(partial = {}) {
if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) {
const value = partial.blurThreshold;
storedSettings.blurThreshold = value === null ? 3 : value;
storedSettings.blurThreshold = value === null ? 1 : value;
}
if (
Object.prototype.hasOwnProperty.call(
@@ -96,7 +96,7 @@ beforeEach(async () => {
)
) {
const value = partial.autoplayBlockThreshold;
storedSettings.autoplayBlockThreshold = value === null ? 2 : value;
storedSettings.autoplayBlockThreshold = value === null ? 1 : value;
}
if (
Object.prototype.hasOwnProperty.call(
@@ -120,8 +120,8 @@ beforeEach(async () => {
},
resetModerationSettings() {
storedSettings = {
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: 3,
};
@@ -185,8 +185,8 @@ afterEach(() => {
test("moderation settings save updates service and disables control", async () => {
await controller.load();
assert.equal(controller.moderationBlurInput.value, "3");
assert.equal(controller.moderationAutoplayInput.value, "2");
assert.equal(controller.moderationBlurInput.value, "1");
assert.equal(controller.moderationAutoplayInput.value, "1");
assert.equal(controller.moderationMuteHideInput.value, "1");
assert.equal(controller.moderationSpamHideInput.value, "3");
assert.equal(controller.moderationSaveButton.disabled, true);
@@ -249,13 +249,13 @@ test("moderation reset restores defaults and clearing inputs uses defaults", asy
const resetContext = await controller.handleModerationSettingsReset();
assert.equal(resetContext.success, true);
assert.deepEqual(storedSettings, {
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: 3,
});
assert.equal(controller.moderationBlurInput.value, "3");
assert.equal(controller.moderationAutoplayInput.value, "2");
assert.equal(controller.moderationBlurInput.value, "1");
assert.equal(controller.moderationAutoplayInput.value, "1");
assert.equal(controller.moderationMuteHideInput.value, "1");
assert.equal(controller.moderationSpamHideInput.value, "3");
assert.equal(
@@ -276,7 +276,7 @@ test("moderation reset restores defaults and clearing inputs uses defaults", asy
assert.equal(saveResult.success, true);
assert.deepEqual(storedSettings, {
blurThreshold: 7,
autoplayBlockThreshold: 2,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: 8,
});
@@ -302,8 +302,8 @@ test("moderation reset restores defaults and clearing inputs uses defaults", asy
const revertContext = await controller.handleModerationSettingsSave();
assert.equal(revertContext.success, true);
assert.deepEqual(storedSettings, {
blurThreshold: 3,
autoplayBlockThreshold: 2,
blurThreshold: 1,
autoplayBlockThreshold: 1,
trustedMuteHideThreshold: 1,
trustedSpamHideThreshold: 3,
});