mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-05 04:12:25 +00:00
Merge pull request #916 from PR0M3TH3AN/codex/enhance-moderation-stage-for-dynamic-thresholds
Resolve moderation thresholds from runtime context
This commit is contained in:
8 files changed
+522
-26
No files matched your search
@@ -24,6 +24,7 @@ bitvid is follow-centric. Your Home feed comes from people you follow (F1). Disc
|
||||
## Runtime flow (blur, hide, override)
|
||||
- [`ModerationService`](../../js/services/moderationService.js) orchestrates ingest and scoring. Review the service for the trusted-report math and helper entry points.
|
||||
- [`createModerationStage`](../../js/feedEngine/stages.js) wires the service into the feed engine where moderation summaries decorate timeline items.
|
||||
- The moderation stage now resolves blur/autoplay/hide thresholds on every execution using `context.runtime.moderationThresholds` (when provided) or dynamic resolver functions, so feed refreshes immediately respect updated viewer settings.
|
||||
- [`bitvidApp.decorateVideoModeration`](../../js/app.js) connects stage output to the UI layer alongside feature-flag plumbing.
|
||||
- [`VideoCard.refreshModerationUi`](../../js/ui/components/VideoCard.js) applies badges, blur states, hide metadata (`data-moderation-hidden`), and the "show anyway" toggles.
|
||||
|
||||
|
||||
@@ -5403,7 +5403,7 @@ class Application {
|
||||
return null;
|
||||
}
|
||||
|
||||
handleModerationSettingsChange({ settings } = {}) {
|
||||
async handleModerationSettingsChange({ settings } = {}) {
|
||||
const normalized = this.normalizeModerationSettings(settings);
|
||||
this.moderationSettings = normalized;
|
||||
|
||||
@@ -5449,6 +5449,15 @@ class Application {
|
||||
this.decorateVideoModeration(this.currentVideo);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.onVideosShouldRefresh({ reason: "moderation-settings-change" });
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to refresh videos after moderation settings change:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -6679,7 +6688,28 @@ class Application {
|
||||
}
|
||||
|
||||
try {
|
||||
const thresholds = this.getActiveModerationThresholds();
|
||||
const app = this;
|
||||
const resolveThresholdFromApp = (key) => ({ runtimeValue, defaultValue }) => {
|
||||
if (
|
||||
Number.isFinite(runtimeValue) ||
|
||||
runtimeValue === Number.POSITIVE_INFINITY
|
||||
) {
|
||||
return runtimeValue;
|
||||
}
|
||||
|
||||
if (app && typeof app.getActiveModerationThresholds === "function") {
|
||||
const active = app.getActiveModerationThresholds();
|
||||
const candidate = active && typeof active === "object" ? active[key] : undefined;
|
||||
if (
|
||||
Number.isFinite(candidate) ||
|
||||
candidate === Number.POSITIVE_INFINITY
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
return this.feedEngine.registerFeed("recent", {
|
||||
source: createActiveNostrSource({ service: this.nostrService }),
|
||||
stages: [
|
||||
@@ -6695,10 +6725,10 @@ class Application {
|
||||
}),
|
||||
createModerationStage({
|
||||
getService: () => this.nostrService.getModerationService(),
|
||||
autoplayThreshold: thresholds.autoplayBlockThreshold,
|
||||
blurThreshold: thresholds.blurThreshold,
|
||||
trustedMuteHideThreshold: thresholds.trustedMuteHideThreshold,
|
||||
trustedReportHideThreshold: thresholds.trustedSpamHideThreshold,
|
||||
autoplayThreshold: resolveThresholdFromApp("autoplayBlockThreshold"),
|
||||
blurThreshold: resolveThresholdFromApp("blurThreshold"),
|
||||
trustedMuteHideThreshold: resolveThresholdFromApp("trustedMuteHideThreshold"),
|
||||
trustedReportHideThreshold: resolveThresholdFromApp("trustedSpamHideThreshold"),
|
||||
}),
|
||||
],
|
||||
sorter: createChronologicalSorter(),
|
||||
@@ -6723,7 +6753,28 @@ class Application {
|
||||
}
|
||||
|
||||
try {
|
||||
const thresholds = this.getActiveModerationThresholds();
|
||||
const app = this;
|
||||
const resolveThresholdFromApp = (key) => ({ runtimeValue, defaultValue }) => {
|
||||
if (
|
||||
Number.isFinite(runtimeValue) ||
|
||||
runtimeValue === Number.POSITIVE_INFINITY
|
||||
) {
|
||||
return runtimeValue;
|
||||
}
|
||||
|
||||
if (app && typeof app.getActiveModerationThresholds === "function") {
|
||||
const active = app.getActiveModerationThresholds();
|
||||
const candidate = active && typeof active === "object" ? active[key] : undefined;
|
||||
if (
|
||||
Number.isFinite(candidate) ||
|
||||
candidate === Number.POSITIVE_INFINITY
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
return this.feedEngine.registerFeed("subscriptions", {
|
||||
source: createSubscriptionAuthorsSource({ service: this.nostrService }),
|
||||
stages: [
|
||||
@@ -6738,10 +6789,10 @@ class Application {
|
||||
}),
|
||||
createModerationStage({
|
||||
getService: () => this.nostrService.getModerationService(),
|
||||
autoplayThreshold: thresholds.autoplayBlockThreshold,
|
||||
blurThreshold: thresholds.blurThreshold,
|
||||
trustedMuteHideThreshold: thresholds.trustedMuteHideThreshold,
|
||||
trustedReportHideThreshold: thresholds.trustedSpamHideThreshold,
|
||||
autoplayThreshold: resolveThresholdFromApp("autoplayBlockThreshold"),
|
||||
blurThreshold: resolveThresholdFromApp("blurThreshold"),
|
||||
trustedMuteHideThreshold: resolveThresholdFromApp("trustedMuteHideThreshold"),
|
||||
trustedReportHideThreshold: resolveThresholdFromApp("trustedSpamHideThreshold"),
|
||||
}),
|
||||
],
|
||||
sorter: createChronologicalSorter(),
|
||||
@@ -6792,6 +6843,7 @@ class Application {
|
||||
? this.getHashtagPreferences()
|
||||
: {};
|
||||
const { interests = [], disinterests = [] } = preferenceSource || {};
|
||||
const moderationThresholds = this.getActiveModerationThresholds();
|
||||
|
||||
return {
|
||||
blacklistedEventIds: blacklist,
|
||||
@@ -6800,6 +6852,9 @@ class Application {
|
||||
interests: Array.isArray(interests) ? [...interests] : [],
|
||||
disinterests: Array.isArray(disinterests) ? [...disinterests] : [],
|
||||
},
|
||||
moderationThresholds: moderationThresholds
|
||||
? { ...moderationThresholds }
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+98
-15
@@ -204,34 +204,112 @@ export function createWatchHistorySuppressionStage({
|
||||
|
||||
export function createModerationStage({
|
||||
stageName = "moderation",
|
||||
autoplayThreshold = DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
||||
blurThreshold = DEFAULT_BLUR_THRESHOLD,
|
||||
autoplayThreshold,
|
||||
blurThreshold,
|
||||
reportType = "nudity",
|
||||
service = null,
|
||||
getService,
|
||||
trustedMuteHideThreshold = Number.POSITIVE_INFINITY,
|
||||
trustedReportHideThreshold = Number.POSITIVE_INFINITY,
|
||||
trustedMuteHideThreshold,
|
||||
trustedReportHideThreshold,
|
||||
} = {}) {
|
||||
const sanitizeThreshold = (value, fallback) => {
|
||||
const sanitizeThreshold = (value, { fallback, allowInfinity = false } = {}) => {
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
if (allowInfinity && value === Number.POSITIVE_INFINITY) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const normalizedAutoplayThreshold = Number.isFinite(autoplayThreshold)
|
||||
? Math.max(0, Math.floor(autoplayThreshold))
|
||||
: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD;
|
||||
const normalizedBlurThreshold = Number.isFinite(blurThreshold)
|
||||
? Math.max(0, Math.floor(blurThreshold))
|
||||
: DEFAULT_BLUR_THRESHOLD;
|
||||
const normalizedMuteHideThreshold = sanitizeThreshold(
|
||||
const createThresholdResolver = (
|
||||
candidate,
|
||||
{ runtimeKey, defaultValue, fallbackValue, allowInfinity = false },
|
||||
) => {
|
||||
return (context) => {
|
||||
const runtimeThresholds =
|
||||
context?.runtime && typeof context.runtime === "object"
|
||||
? context.runtime.moderationThresholds
|
||||
: null;
|
||||
|
||||
const runtimeValue =
|
||||
runtimeThresholds && typeof runtimeThresholds === "object"
|
||||
? runtimeThresholds[runtimeKey]
|
||||
: undefined;
|
||||
|
||||
const runtimeValid =
|
||||
Number.isFinite(runtimeValue) ||
|
||||
(allowInfinity && runtimeValue === Number.POSITIVE_INFINITY);
|
||||
|
||||
let value;
|
||||
|
||||
if (typeof candidate === "function") {
|
||||
try {
|
||||
value = candidate({
|
||||
context,
|
||||
runtimeThresholds,
|
||||
runtimeValue,
|
||||
defaultValue,
|
||||
});
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
`[${stageName}] threshold resolver threw for ${runtimeKey}`,
|
||||
error,
|
||||
);
|
||||
value = undefined;
|
||||
}
|
||||
} else if (Number.isFinite(candidate)) {
|
||||
value = candidate;
|
||||
} else if (allowInfinity && candidate === Number.POSITIVE_INFINITY) {
|
||||
value = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
const hasValidValue =
|
||||
Number.isFinite(value) ||
|
||||
(allowInfinity && value === Number.POSITIVE_INFINITY);
|
||||
|
||||
if (!hasValidValue) {
|
||||
if (runtimeValid) {
|
||||
value = runtimeValue;
|
||||
} else {
|
||||
value = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitizeThreshold(value, {
|
||||
fallback: fallbackValue,
|
||||
allowInfinity,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const resolveAutoplayThreshold = createThresholdResolver(autoplayThreshold, {
|
||||
runtimeKey: "autoplayBlockThreshold",
|
||||
defaultValue: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
||||
fallbackValue: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
||||
});
|
||||
const resolveBlurThreshold = createThresholdResolver(blurThreshold, {
|
||||
runtimeKey: "blurThreshold",
|
||||
defaultValue: DEFAULT_BLUR_THRESHOLD,
|
||||
fallbackValue: DEFAULT_BLUR_THRESHOLD,
|
||||
});
|
||||
const resolveMuteHideThreshold = createThresholdResolver(
|
||||
trustedMuteHideThreshold,
|
||||
Number.POSITIVE_INFINITY,
|
||||
{
|
||||
runtimeKey: "trustedMuteHideThreshold",
|
||||
defaultValue: Number.POSITIVE_INFINITY,
|
||||
fallbackValue: Number.POSITIVE_INFINITY,
|
||||
allowInfinity: true,
|
||||
},
|
||||
);
|
||||
const normalizedReportHideThreshold = sanitizeThreshold(
|
||||
const resolveReportHideThreshold = createThresholdResolver(
|
||||
trustedReportHideThreshold,
|
||||
Number.POSITIVE_INFINITY,
|
||||
{
|
||||
runtimeKey: "trustedSpamHideThreshold",
|
||||
defaultValue: Number.POSITIVE_INFINITY,
|
||||
fallbackValue: Number.POSITIVE_INFINITY,
|
||||
allowInfinity: true,
|
||||
},
|
||||
);
|
||||
const normalizedReportType = typeof reportType === "string" ? reportType.trim().toLowerCase() : "nudity";
|
||||
|
||||
@@ -259,6 +337,11 @@ export function createModerationStage({
|
||||
context?.log?.(`[${stageName}] Failed to refresh viewer context`, error);
|
||||
}
|
||||
|
||||
const normalizedAutoplayThreshold = resolveAutoplayThreshold(context);
|
||||
const normalizedBlurThreshold = resolveBlurThreshold(context);
|
||||
const normalizedMuteHideThreshold = resolveMuteHideThreshold(context);
|
||||
const normalizedReportHideThreshold = resolveReportHideThreshold(context);
|
||||
|
||||
const activeIds = new Set();
|
||||
for (const item of items) {
|
||||
const videoId = typeof item?.video?.id === "string" ? item.video.id : "";
|
||||
|
||||
@@ -883,6 +883,11 @@ class SubscriptionsManager {
|
||||
? app.getHashtagPreferences()
|
||||
: {};
|
||||
|
||||
const moderationThresholds =
|
||||
typeof app?.getActiveModerationThresholds === "function"
|
||||
? app.getActiveModerationThresholds()
|
||||
: null;
|
||||
|
||||
return {
|
||||
subscriptionAuthors: normalizedAuthors,
|
||||
authors: normalizedAuthors,
|
||||
@@ -897,6 +902,9 @@ class SubscriptionsManager {
|
||||
? [...preferenceSource.disinterests]
|
||||
: [],
|
||||
},
|
||||
moderationThresholds: moderationThresholds
|
||||
? { ...moderationThresholds }
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import test, { mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import {
|
||||
withMockedNostrTools,
|
||||
createModerationAppHarness,
|
||||
} from "../helpers/moderation-test-helpers.mjs";
|
||||
|
||||
const VIDEO_HEX = "1".repeat(64);
|
||||
const SECOND_VIDEO_HEX = "2".repeat(64);
|
||||
const CURRENT_VIDEO_HEX = "3".repeat(64);
|
||||
|
||||
function buildModerationState() {
|
||||
return {
|
||||
trustedMuted: false,
|
||||
trustedMuteCount: 0,
|
||||
trustedCount: 0,
|
||||
reportType: "nudity",
|
||||
};
|
||||
}
|
||||
|
||||
test("handleModerationSettingsChange refreshes feeds with updated thresholds", async (t) => {
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const app = await createModerationAppHarness();
|
||||
app.decorateVideoModeration = mock.fn(() => {});
|
||||
|
||||
const storedVideo = { id: VIDEO_HEX, moderation: buildModerationState() };
|
||||
const listVideo = { id: SECOND_VIDEO_HEX, moderation: buildModerationState() };
|
||||
const currentVideo = { id: CURRENT_VIDEO_HEX, moderation: buildModerationState() };
|
||||
|
||||
app.videosMap.set(storedVideo.id, storedVideo);
|
||||
app.videoListView = {
|
||||
videoCardInstances: [
|
||||
{
|
||||
video: listVideo,
|
||||
refreshModerationUi: mock.fn(() => {}),
|
||||
},
|
||||
],
|
||||
currentVideos: [listVideo],
|
||||
};
|
||||
app.currentVideo = currentVideo;
|
||||
|
||||
const refreshMock = mock.fn(async () => {});
|
||||
app.onVideosShouldRefresh = refreshMock;
|
||||
|
||||
const pendingSettings = {
|
||||
blurThreshold: 7,
|
||||
autoplayBlockThreshold: 6,
|
||||
trustedMuteHideThreshold: 9,
|
||||
trustedSpamHideThreshold: 11,
|
||||
};
|
||||
|
||||
const expectedSettings = app.normalizeModerationSettings(pendingSettings);
|
||||
const result = await app.handleModerationSettingsChange({ settings: pendingSettings });
|
||||
|
||||
assert.deepEqual(result, expectedSettings);
|
||||
assert.deepEqual(app.moderationSettings, expectedSettings);
|
||||
|
||||
assert.equal(refreshMock.mock.calls.length, 1);
|
||||
assert.equal(
|
||||
refreshMock.mock.calls[0]?.arguments?.[0]?.reason,
|
||||
"moderation-settings-change",
|
||||
);
|
||||
|
||||
assert.ok(app.decorateVideoModeration.mock.calls.length >= 3);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import { ModerationService } from "../../js/services/moderationService.js";
|
||||
import { getDefaultModerationSettings } from "../../js/state/cache.js";
|
||||
|
||||
const DEFAULT_CONTACT_OWNER = "f".repeat(64);
|
||||
|
||||
@@ -199,6 +200,15 @@ export async function createModerationAppHarness() {
|
||||
app.handleModerationOverride = Application.prototype.handleModerationOverride;
|
||||
app.handleModerationBlock = Application.prototype.handleModerationBlock;
|
||||
app.handleModerationHide = Application.prototype.handleModerationHide;
|
||||
app.handleModerationSettingsChange =
|
||||
Application.prototype.handleModerationSettingsChange;
|
||||
app.normalizeModerationSettings =
|
||||
Application.prototype.normalizeModerationSettings;
|
||||
app.getActiveModerationThresholds =
|
||||
Application.prototype.getActiveModerationThresholds;
|
||||
|
||||
app.defaultModerationSettings = getDefaultModerationSettings();
|
||||
app.moderationSettings = { ...app.defaultModerationSettings };
|
||||
|
||||
app.videosMap = new Map();
|
||||
app.currentVideo = null;
|
||||
|
||||
@@ -225,6 +225,142 @@ test("moderation stage applies provided thresholds", async () => {
|
||||
assert.equal(relaxedResult[0].metadata.moderation.blurReason, "trusted-report");
|
||||
});
|
||||
|
||||
test("moderation stage respects runtime threshold changes", async () => {
|
||||
const videoId = "runtime-threshold";
|
||||
const authorHex = "e".repeat(64);
|
||||
|
||||
const service = {
|
||||
async refreshViewerFromClient() {},
|
||||
async setActiveEventIds() {},
|
||||
getAdminListSnapshot() {
|
||||
return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() };
|
||||
},
|
||||
getAccessControlStatus(identifier) {
|
||||
return { hex: identifier, whitelisted: false, blacklisted: false };
|
||||
},
|
||||
getTrustedReportSummary() {
|
||||
return null;
|
||||
},
|
||||
trustedReportCount() {
|
||||
return 3;
|
||||
},
|
||||
getTrustedReporters() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({ service });
|
||||
|
||||
const strictContext = {
|
||||
runtime: {
|
||||
moderationThresholds: {
|
||||
autoplayBlockThreshold: 1,
|
||||
blurThreshold: 1,
|
||||
trustedMuteHideThreshold: Number.POSITIVE_INFINITY,
|
||||
trustedSpamHideThreshold: 2,
|
||||
},
|
||||
},
|
||||
addWhy() {},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const relaxedContext = {
|
||||
runtime: {
|
||||
moderationThresholds: {
|
||||
autoplayBlockThreshold: 5,
|
||||
blurThreshold: 5,
|
||||
trustedMuteHideThreshold: Number.POSITIVE_INFINITY,
|
||||
trustedSpamHideThreshold: 10,
|
||||
},
|
||||
},
|
||||
addWhy() {},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const buildItem = () => ({ video: { id: videoId, pubkey: authorHex }, metadata: {} });
|
||||
|
||||
const hiddenItems = [buildItem()];
|
||||
const hiddenResult = await stage(hiddenItems, strictContext);
|
||||
|
||||
assert.equal(hiddenResult.length, 0);
|
||||
const hiddenMetadata = hiddenItems[0].metadata.moderation;
|
||||
assert.equal(hiddenMetadata.hidden, true);
|
||||
assert.equal(hiddenMetadata.hideReason, "trusted-report-hide");
|
||||
assert.equal(hiddenMetadata.blockAutoplay, true);
|
||||
assert.equal(hiddenMetadata.blurThumbnail, true);
|
||||
|
||||
const visibleItems = [buildItem()];
|
||||
const visibleResult = await stage(visibleItems, relaxedContext);
|
||||
|
||||
assert.equal(visibleResult.length, 1);
|
||||
const visibleModeration = visibleResult[0].video.moderation;
|
||||
assert.equal(visibleModeration.hidden, false);
|
||||
assert.equal(visibleModeration.blockAutoplay, false);
|
||||
assert.equal(visibleModeration.blurThumbnail, false);
|
||||
});
|
||||
|
||||
test("moderation stage supports function-based threshold resolvers", async () => {
|
||||
const videoId = "resolver-threshold";
|
||||
const authorHex = "f".repeat(64);
|
||||
|
||||
let autoplaySetting = 5;
|
||||
let blurSetting = 6;
|
||||
let hideSetting = Number.POSITIVE_INFINITY;
|
||||
|
||||
const service = {
|
||||
async refreshViewerFromClient() {},
|
||||
async setActiveEventIds() {},
|
||||
getAdminListSnapshot() {
|
||||
return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() };
|
||||
},
|
||||
getAccessControlStatus(identifier) {
|
||||
return { hex: identifier, whitelisted: false, blacklisted: false };
|
||||
},
|
||||
getTrustedReportSummary() {
|
||||
return null;
|
||||
},
|
||||
trustedReportCount() {
|
||||
return 2;
|
||||
},
|
||||
getTrustedReporters() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({
|
||||
service,
|
||||
autoplayThreshold: () => autoplaySetting,
|
||||
blurThreshold: () => blurSetting,
|
||||
trustedReportHideThreshold: () => hideSetting,
|
||||
});
|
||||
|
||||
const createContext = () => ({ addWhy() {}, log() {} });
|
||||
const buildItem = () => ({ video: { id: videoId, pubkey: authorHex }, metadata: {} });
|
||||
|
||||
const relaxedItems = [buildItem()];
|
||||
const relaxedResult = await stage(relaxedItems, createContext());
|
||||
|
||||
assert.equal(relaxedResult.length, 1);
|
||||
const relaxedModeration = relaxedResult[0].video.moderation;
|
||||
assert.equal(relaxedModeration.blockAutoplay, false);
|
||||
assert.equal(relaxedModeration.blurThumbnail, false);
|
||||
assert.equal(relaxedModeration.hidden, false);
|
||||
|
||||
autoplaySetting = 1;
|
||||
blurSetting = 1;
|
||||
hideSetting = 1;
|
||||
|
||||
const strictItems = [buildItem()];
|
||||
const strictResult = await stage(strictItems, createContext());
|
||||
|
||||
assert.equal(strictResult.length, 0);
|
||||
const strictMetadata = strictItems[0].metadata.moderation;
|
||||
assert.equal(strictMetadata.blockAutoplay, true);
|
||||
assert.equal(strictMetadata.blurThumbnail, true);
|
||||
assert.equal(strictMetadata.hidden, true);
|
||||
assert.equal(strictMetadata.hideReason, "trusted-report-hide");
|
||||
});
|
||||
|
||||
test("moderation stage propagates whitelist, muters, and threshold updates", async () => {
|
||||
const whitelistedHex = "a".repeat(64);
|
||||
const mutedHex = "b".repeat(64);
|
||||
|
||||
@@ -137,6 +137,141 @@ test("moderation stage hides videos when trusted reports exceed threshold", asyn
|
||||
assert.equal(hideWhy.hideBypass ?? null, null);
|
||||
});
|
||||
|
||||
test("moderation stage respects runtime hide threshold changes", async () => {
|
||||
const reporterHex = hex("4");
|
||||
const mutedHex = hex("5");
|
||||
const videoId = hex("6");
|
||||
|
||||
const service = {
|
||||
async refreshViewerFromClient() {},
|
||||
async setActiveEventIds() {},
|
||||
getAdminListSnapshot() {
|
||||
return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() };
|
||||
},
|
||||
getAccessControlStatus(identifier) {
|
||||
return { hex: identifier, whitelisted: false, blacklisted: false };
|
||||
},
|
||||
getTrustedReportSummary() {
|
||||
return null;
|
||||
},
|
||||
trustedReportCount() {
|
||||
return 4;
|
||||
},
|
||||
getTrustedReporters() {
|
||||
return [{ pubkey: reporterHex, latest: 1_700_000_000 }];
|
||||
},
|
||||
isAuthorMutedByTrusted(pubkey) {
|
||||
return pubkey === mutedHex;
|
||||
},
|
||||
getTrustedMutersForAuthor(pubkey) {
|
||||
return pubkey === mutedHex ? [reporterHex] : [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({ service });
|
||||
|
||||
const strictContext = {
|
||||
runtime: {
|
||||
moderationThresholds: {
|
||||
autoplayBlockThreshold: 1,
|
||||
blurThreshold: 1,
|
||||
trustedMuteHideThreshold: 1,
|
||||
trustedSpamHideThreshold: 2,
|
||||
},
|
||||
},
|
||||
addWhy() {},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const relaxedContext = {
|
||||
runtime: {
|
||||
moderationThresholds: {
|
||||
autoplayBlockThreshold: 10,
|
||||
blurThreshold: 10,
|
||||
trustedMuteHideThreshold: 10,
|
||||
trustedSpamHideThreshold: 10,
|
||||
},
|
||||
},
|
||||
addWhy() {},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const hiddenItems = [{ video: { id: videoId, pubkey: mutedHex }, metadata: {} }];
|
||||
const hiddenResult = await stage(hiddenItems, strictContext);
|
||||
|
||||
assert.equal(hiddenResult.length, 0);
|
||||
assert.equal(hiddenItems[0].metadata.moderation.hidden, true);
|
||||
assert.equal(hiddenItems[0].metadata.moderation.hideReason, "trusted-mute-hide");
|
||||
|
||||
const visibleItems = [{ video: { id: videoId, pubkey: mutedHex }, metadata: {} }];
|
||||
const visibleResult = await stage(visibleItems, relaxedContext);
|
||||
|
||||
assert.equal(visibleResult.length, 1);
|
||||
const moderation = visibleResult[0].metadata.moderation;
|
||||
assert.equal(moderation.hidden, false);
|
||||
assert.equal(moderation.hideReason ?? null, null);
|
||||
});
|
||||
|
||||
test("moderation stage supports trusted mute hide resolver functions", async () => {
|
||||
const muterHex = hex("7");
|
||||
const mutedHex = hex("8");
|
||||
const videoId = hex("9");
|
||||
|
||||
let muteHideSetting = Number.POSITIVE_INFINITY;
|
||||
|
||||
const service = {
|
||||
async refreshViewerFromClient() {},
|
||||
async setActiveEventIds() {},
|
||||
getAdminListSnapshot() {
|
||||
return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() };
|
||||
},
|
||||
getAccessControlStatus(identifier) {
|
||||
return { hex: identifier, whitelisted: false, blacklisted: false };
|
||||
},
|
||||
getTrustedReportSummary() {
|
||||
return null;
|
||||
},
|
||||
trustedReportCount() {
|
||||
return 0;
|
||||
},
|
||||
getTrustedReporters() {
|
||||
return [];
|
||||
},
|
||||
isAuthorMutedByTrusted(pubkey) {
|
||||
return pubkey === mutedHex;
|
||||
},
|
||||
getTrustedMutersForAuthor(pubkey) {
|
||||
return pubkey === mutedHex ? [muterHex] : [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({
|
||||
service,
|
||||
trustedMuteHideThreshold: () => muteHideSetting,
|
||||
});
|
||||
|
||||
const buildItem = () => ({ video: { id: videoId, pubkey: mutedHex }, metadata: {} });
|
||||
const baseContext = { addWhy() {}, log() {} };
|
||||
|
||||
const relaxedItems = [buildItem()];
|
||||
const relaxedResult = await stage(relaxedItems, baseContext);
|
||||
|
||||
assert.equal(relaxedResult.length, 1);
|
||||
const relaxedModeration = relaxedResult[0].metadata.moderation;
|
||||
assert.equal(relaxedModeration.hidden, false);
|
||||
assert.equal(relaxedModeration.trustedMuted, true);
|
||||
|
||||
muteHideSetting = 1;
|
||||
|
||||
const strictItems = [buildItem()];
|
||||
const strictResult = await stage(strictItems, baseContext);
|
||||
|
||||
assert.equal(strictResult.length, 0);
|
||||
const strictMetadata = strictItems[0].metadata.moderation;
|
||||
assert.equal(strictMetadata.hidden, true);
|
||||
assert.equal(strictMetadata.hideReason, "trusted-mute-hide");
|
||||
});
|
||||
|
||||
test("moderation stage bypasses hard hides on home feed", async () => {
|
||||
const videoId = hex("3");
|
||||
|
||||
|
||||
Reference in new issue
Block a user