mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 18:02:27 +00:00
Merge pull request #742 from PR0M3TH3AN/codex/enhance-moderation-stage-for-hide-thresholds
Add trusted hide gating to moderation stage
This commit is contained in:
3 files changed
+497
-12
No files matched your search
@@ -4369,6 +4369,8 @@ class Application {
|
||||
getService: () => this.nostrService.getModerationService(),
|
||||
autoplayThreshold: thresholds.autoplayBlockThreshold,
|
||||
blurThreshold: thresholds.blurThreshold,
|
||||
trustedMuteHideThreshold: thresholds.trustedMuteHideThreshold,
|
||||
trustedReportHideThreshold: thresholds.trustedSpamHideThreshold,
|
||||
}),
|
||||
],
|
||||
sorter: createChronologicalSorter(),
|
||||
@@ -4408,6 +4410,8 @@ class Application {
|
||||
getService: () => this.nostrService.getModerationService(),
|
||||
autoplayThreshold: thresholds.autoplayBlockThreshold,
|
||||
blurThreshold: thresholds.blurThreshold,
|
||||
trustedMuteHideThreshold: thresholds.trustedMuteHideThreshold,
|
||||
trustedReportHideThreshold: thresholds.trustedSpamHideThreshold,
|
||||
}),
|
||||
],
|
||||
sorter: createChronologicalSorter(),
|
||||
|
||||
+233
-12
@@ -8,6 +8,8 @@ import logger from "../utils/logger.js";
|
||||
import { dedupeToNewestByRoot } from "../utils/videoDeduper.js";
|
||||
import { isPlainObject, toSet } from "./utils.js";
|
||||
|
||||
const FEED_HIDE_BYPASS_NAMES = new Set(["home", "recent"]);
|
||||
|
||||
function resolveDedupeFunction(customDedupe) {
|
||||
if (typeof customDedupe === "function") {
|
||||
return customDedupe;
|
||||
@@ -199,13 +201,30 @@ export function createModerationStage({
|
||||
reportType = "nudity",
|
||||
service = null,
|
||||
getService,
|
||||
trustedMuteHideThreshold = Number.POSITIVE_INFINITY,
|
||||
trustedReportHideThreshold = Number.POSITIVE_INFINITY,
|
||||
} = {}) {
|
||||
const sanitizeThreshold = (value, fallback) => {
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const normalizedAutoplayThreshold = Number.isFinite(autoplayThreshold)
|
||||
? Math.max(0, Math.floor(autoplayThreshold))
|
||||
: 2;
|
||||
const normalizedBlurThreshold = Number.isFinite(blurThreshold)
|
||||
? Math.max(0, Math.floor(blurThreshold))
|
||||
: 3;
|
||||
const normalizedMuteHideThreshold = sanitizeThreshold(
|
||||
trustedMuteHideThreshold,
|
||||
Number.POSITIVE_INFINITY,
|
||||
);
|
||||
const normalizedReportHideThreshold = sanitizeThreshold(
|
||||
trustedReportHideThreshold,
|
||||
Number.POSITIVE_INFINITY,
|
||||
);
|
||||
const normalizedReportType = typeof reportType === "string" ? reportType.trim().toLowerCase() : "nudity";
|
||||
|
||||
return async function moderationStage(items = [], context = {}) {
|
||||
@@ -319,6 +338,104 @@ export function createModerationStage({
|
||||
}
|
||||
: () => ({ hex: "", whitelisted: false, blacklisted: false });
|
||||
|
||||
const normalizedFeedName =
|
||||
typeof context?.feedName === "string" ? context.feedName.trim().toLowerCase() : "";
|
||||
const normalizedFeedVariant =
|
||||
typeof context?.runtime?.feedVariant === "string"
|
||||
? context.runtime.feedVariant.trim().toLowerCase()
|
||||
: "";
|
||||
const runtimeDisableHardHide = context?.runtime?.disableHardHide === true;
|
||||
const feedPolicyBypass =
|
||||
runtimeDisableHardHide ||
|
||||
(normalizedFeedName && FEED_HIDE_BYPASS_NAMES.has(normalizedFeedName)) ||
|
||||
(normalizedFeedVariant && FEED_HIDE_BYPASS_NAMES.has(normalizedFeedVariant));
|
||||
|
||||
const runtimeForOverride = context?.runtime && typeof context.runtime === "object"
|
||||
? context.runtime
|
||||
: null;
|
||||
|
||||
const overrideChecker = (() => {
|
||||
if (!runtimeForOverride) {
|
||||
return () => false;
|
||||
}
|
||||
|
||||
const overrideMap = runtimeForOverride.moderationOverrides;
|
||||
const overrideIds = runtimeForOverride.moderationOverrideIds;
|
||||
const hasFn =
|
||||
typeof runtimeForOverride.hasModerationOverride === "function"
|
||||
? runtimeForOverride.hasModerationOverride.bind(runtimeForOverride)
|
||||
: null;
|
||||
const getFn =
|
||||
typeof runtimeForOverride.getModerationOverride === "function"
|
||||
? runtimeForOverride.getModerationOverride.bind(runtimeForOverride)
|
||||
: null;
|
||||
|
||||
const checkEntry = (entry) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
if (entry === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof entry === "object" && entry.showAnyway === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (videoId) => {
|
||||
if (typeof videoId !== "string") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = videoId.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
const normalizedId = trimmed.toLowerCase();
|
||||
|
||||
if (overrideMap instanceof Map) {
|
||||
const entry = overrideMap.get(normalizedId) ?? overrideMap.get(trimmed);
|
||||
if (checkEntry(entry)) {
|
||||
return true;
|
||||
}
|
||||
} else if (overrideMap && typeof overrideMap === "object") {
|
||||
const entry = overrideMap[normalizedId] ?? overrideMap[trimmed];
|
||||
if (checkEntry(entry)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideIds instanceof Set) {
|
||||
if (overrideIds.has(normalizedId) || overrideIds.has(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFn) {
|
||||
try {
|
||||
if (hasFn(normalizedId) || (normalizedId !== trimmed && hasFn(trimmed))) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(`[${stageName}] hasModerationOverride threw`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (getFn) {
|
||||
try {
|
||||
const entry = getFn(normalizedId) ?? (normalizedId !== trimmed ? getFn(trimmed) : null);
|
||||
if (checkEntry(entry)) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(`[${stageName}] getModerationOverride threw`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
})();
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
@@ -489,18 +606,6 @@ export function createModerationStage({
|
||||
metadataModeration.trustedMuted = trustedMuted;
|
||||
metadataModeration.trustedMuters = trustedMuters.slice();
|
||||
metadataModeration.trustedMuteCount = trustedMuteCount;
|
||||
item.metadata.moderation = metadataModeration;
|
||||
|
||||
if (IS_DEV_MODE) {
|
||||
logger.dev.debug("[feedEngine/moderation] metadata.moderation updated", {
|
||||
stage: stageName,
|
||||
videoId: videoId || null,
|
||||
blockAutoplay,
|
||||
blurThumbnail,
|
||||
trustedCount,
|
||||
reportType: normalizedReportType,
|
||||
});
|
||||
}
|
||||
|
||||
if (!video.moderation || typeof video.moderation !== "object") {
|
||||
video.moderation = {};
|
||||
@@ -535,6 +640,93 @@ export function createModerationStage({
|
||||
delete video.moderation.summary;
|
||||
}
|
||||
|
||||
const hideCounts = {
|
||||
trustedMuteCount,
|
||||
trustedReportCount: trustedCount,
|
||||
};
|
||||
|
||||
let hideReason = "";
|
||||
let hideTriggered = false;
|
||||
|
||||
if (trustedMuted && trustedMuteCount >= normalizedMuteHideThreshold) {
|
||||
hideReason = "trusted-mute-hide";
|
||||
hideTriggered = true;
|
||||
} else if (trustedCount >= normalizedReportHideThreshold) {
|
||||
hideReason = "trusted-report-hide";
|
||||
hideTriggered = true;
|
||||
}
|
||||
|
||||
const viewerOverrideActive = overrideChecker(videoId);
|
||||
|
||||
let hideBypass = "";
|
||||
if (hideTriggered) {
|
||||
if (adminWhitelist) {
|
||||
hideBypass = "admin-whitelist";
|
||||
} else if (viewerOverrideActive) {
|
||||
hideBypass = "viewer-override";
|
||||
} else if (feedPolicyBypass) {
|
||||
hideBypass = "feed-policy";
|
||||
}
|
||||
}
|
||||
|
||||
const hidden = hideTriggered && !hideBypass;
|
||||
|
||||
if (hideTriggered) {
|
||||
metadataModeration.hidden = hidden;
|
||||
metadataModeration.hideReason = hideReason;
|
||||
metadataModeration.hideCounts = { ...hideCounts };
|
||||
if (hideBypass) {
|
||||
metadataModeration.hideBypass = hideBypass;
|
||||
} else if (metadataModeration.hideBypass) {
|
||||
delete metadataModeration.hideBypass;
|
||||
}
|
||||
video.moderation.hidden = hidden;
|
||||
video.moderation.hideReason = hideReason;
|
||||
video.moderation.hideCounts = { ...hideCounts };
|
||||
if (hideBypass) {
|
||||
video.moderation.hideBypass = hideBypass;
|
||||
} else if (video.moderation.hideBypass) {
|
||||
delete video.moderation.hideBypass;
|
||||
}
|
||||
} else {
|
||||
metadataModeration.hidden = false;
|
||||
if (metadataModeration.hideReason) {
|
||||
delete metadataModeration.hideReason;
|
||||
}
|
||||
if (metadataModeration.hideCounts) {
|
||||
delete metadataModeration.hideCounts;
|
||||
}
|
||||
if (metadataModeration.hideBypass) {
|
||||
delete metadataModeration.hideBypass;
|
||||
}
|
||||
video.moderation.hidden = false;
|
||||
if (video.moderation.hideReason) {
|
||||
delete video.moderation.hideReason;
|
||||
}
|
||||
if (video.moderation.hideCounts) {
|
||||
delete video.moderation.hideCounts;
|
||||
}
|
||||
if (video.moderation.hideBypass) {
|
||||
delete video.moderation.hideBypass;
|
||||
}
|
||||
}
|
||||
|
||||
item.metadata.moderation = metadataModeration;
|
||||
|
||||
if (IS_DEV_MODE) {
|
||||
logger.dev.debug("[feedEngine/moderation] metadata.moderation updated", {
|
||||
stage: stageName,
|
||||
videoId: videoId || null,
|
||||
blockAutoplay,
|
||||
blurThumbnail,
|
||||
trustedCount,
|
||||
reportType: normalizedReportType,
|
||||
hidden,
|
||||
hideReason: hideTriggered ? hideReason : null,
|
||||
hideBypass: hideBypass || null,
|
||||
});
|
||||
}
|
||||
|
||||
if (blockAutoplay || blurThumbnail) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
@@ -557,6 +749,35 @@ export function createModerationStage({
|
||||
});
|
||||
}
|
||||
|
||||
if (hideTriggered) {
|
||||
const hideDetail = {
|
||||
stage: stageName,
|
||||
type: "moderation",
|
||||
reason: hideReason,
|
||||
videoId: videoId || null,
|
||||
trustedMuteCount,
|
||||
trustedReportCount: trustedCount,
|
||||
hidden,
|
||||
};
|
||||
if (hideBypass) {
|
||||
hideDetail.hideBypass = hideBypass;
|
||||
}
|
||||
if (adminWhitelist) {
|
||||
hideDetail.adminWhitelist = true;
|
||||
}
|
||||
if (viewerOverrideActive) {
|
||||
hideDetail.viewerOverride = true;
|
||||
}
|
||||
if (feedPolicyBypass) {
|
||||
hideDetail.feedPolicyBypass = true;
|
||||
}
|
||||
context?.addWhy?.(hideDetail);
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createModerationStage } from "../../js/feedEngine/stages.js";
|
||||
|
||||
const hex = (value) => value.repeat(64).slice(0, 64);
|
||||
|
||||
test("moderation stage hides videos muted by trusted when threshold met", async () => {
|
||||
const muterHex = hex("a");
|
||||
const mutedHex = hex("b");
|
||||
const videoId = hex("1");
|
||||
|
||||
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: 1,
|
||||
trustedReportHideThreshold: 99,
|
||||
});
|
||||
|
||||
const items = [{ video: { id: videoId, pubkey: mutedHex }, metadata: {} }];
|
||||
const why = [];
|
||||
const context = {
|
||||
runtime: {},
|
||||
addWhy(detail) {
|
||||
why.push(detail);
|
||||
return detail;
|
||||
},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const result = await stage(items, context);
|
||||
|
||||
assert.equal(result.length, 0);
|
||||
|
||||
const metadata = items[0].metadata.moderation;
|
||||
assert.equal(metadata.hidden, true);
|
||||
assert.equal(metadata.hideReason, "trusted-mute-hide");
|
||||
assert.deepEqual(metadata.hideCounts, { trustedMuteCount: 1, trustedReportCount: 0 });
|
||||
assert.equal(metadata.hideBypass ?? null, null);
|
||||
|
||||
const videoModeration = items[0].video.moderation;
|
||||
assert.equal(videoModeration.hidden, true);
|
||||
assert.equal(videoModeration.hideReason, "trusted-mute-hide");
|
||||
assert.deepEqual(videoModeration.hideCounts, { trustedMuteCount: 1, trustedReportCount: 0 });
|
||||
|
||||
const hideWhy = why.find((entry) => entry.reason === "trusted-mute-hide");
|
||||
assert.ok(hideWhy);
|
||||
assert.equal(hideWhy.hidden, true);
|
||||
assert.equal(hideWhy.trustedMuteCount, 1);
|
||||
assert.equal(hideWhy.trustedReportCount, 0);
|
||||
});
|
||||
|
||||
test("moderation stage hides videos when trusted reports exceed threshold", async () => {
|
||||
const reporterHex = hex("c");
|
||||
const videoId = hex("2");
|
||||
|
||||
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 5;
|
||||
},
|
||||
getTrustedReporters() {
|
||||
return [{ pubkey: reporterHex, latest: 1_700_000_000 }];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({
|
||||
service,
|
||||
trustedMuteHideThreshold: 50,
|
||||
trustedReportHideThreshold: 3,
|
||||
});
|
||||
|
||||
const items = [{ video: { id: videoId, pubkey: hex("d") }, metadata: {} }];
|
||||
const why = [];
|
||||
const context = {
|
||||
runtime: {},
|
||||
addWhy(detail) {
|
||||
why.push(detail);
|
||||
return detail;
|
||||
},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const result = await stage(items, context);
|
||||
|
||||
assert.equal(result.length, 0);
|
||||
|
||||
const metadata = items[0].metadata.moderation;
|
||||
assert.equal(metadata.hidden, true);
|
||||
assert.equal(metadata.hideReason, "trusted-report-hide");
|
||||
assert.deepEqual(metadata.hideCounts, { trustedMuteCount: 0, trustedReportCount: 5 });
|
||||
|
||||
const videoModeration = items[0].video.moderation;
|
||||
assert.equal(videoModeration.hidden, true);
|
||||
assert.equal(videoModeration.hideReason, "trusted-report-hide");
|
||||
assert.deepEqual(videoModeration.hideCounts, { trustedMuteCount: 0, trustedReportCount: 5 });
|
||||
|
||||
const hideWhy = why.find((entry) => entry.reason === "trusted-report-hide");
|
||||
assert.ok(hideWhy);
|
||||
assert.equal(hideWhy.hidden, true);
|
||||
assert.equal(hideWhy.trustedReportCount, 5);
|
||||
assert.equal(hideWhy.hideBypass ?? null, null);
|
||||
});
|
||||
|
||||
test("moderation stage bypasses hard hides on home feed", async () => {
|
||||
const videoId = hex("3");
|
||||
|
||||
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 [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({
|
||||
service,
|
||||
trustedMuteHideThreshold: 10,
|
||||
trustedReportHideThreshold: 1,
|
||||
});
|
||||
|
||||
const items = [{ video: { id: videoId, pubkey: hex("e") }, metadata: {} }];
|
||||
const why = [];
|
||||
const context = {
|
||||
feedName: "home",
|
||||
runtime: { feedVariant: "home" },
|
||||
addWhy(detail) {
|
||||
why.push(detail);
|
||||
return detail;
|
||||
},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const result = await stage(items, context);
|
||||
|
||||
assert.equal(result.length, 1);
|
||||
|
||||
const metadata = result[0].metadata.moderation;
|
||||
assert.equal(metadata.hidden, false);
|
||||
assert.equal(metadata.hideReason, "trusted-report-hide");
|
||||
assert.deepEqual(metadata.hideCounts, { trustedMuteCount: 0, trustedReportCount: 4 });
|
||||
assert.equal(metadata.hideBypass, "feed-policy");
|
||||
|
||||
const hideWhy = why.find((entry) => entry.reason === "trusted-report-hide");
|
||||
assert.ok(hideWhy);
|
||||
assert.equal(hideWhy.hidden, false);
|
||||
assert.equal(hideWhy.hideBypass, "feed-policy");
|
||||
});
|
||||
|
||||
test("moderation stage allows admin-whitelisted videos despite hide thresholds", async () => {
|
||||
const whitelistedHex = hex("f");
|
||||
const videoId = hex("4");
|
||||
|
||||
const service = {
|
||||
async refreshViewerFromClient() {},
|
||||
async setActiveEventIds() {},
|
||||
getAdminListSnapshot() {
|
||||
return {
|
||||
whitelist: new Set([`npub${whitelistedHex}`]),
|
||||
whitelistHex: new Set([whitelistedHex]),
|
||||
blacklist: new Set(),
|
||||
blacklistHex: new Set(),
|
||||
};
|
||||
},
|
||||
getAccessControlStatus(identifier) {
|
||||
const normalized = typeof identifier === "string" ? identifier.trim().toLowerCase() : "";
|
||||
return { hex: normalized, whitelisted: normalized === whitelistedHex, blacklisted: false };
|
||||
},
|
||||
getTrustedReportSummary() {
|
||||
return null;
|
||||
},
|
||||
trustedReportCount() {
|
||||
return 6;
|
||||
},
|
||||
getTrustedReporters() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const stage = createModerationStage({
|
||||
service,
|
||||
trustedMuteHideThreshold: 1,
|
||||
trustedReportHideThreshold: 2,
|
||||
});
|
||||
|
||||
const items = [{ video: { id: videoId, pubkey: whitelistedHex }, metadata: {} }];
|
||||
const why = [];
|
||||
const context = {
|
||||
runtime: {},
|
||||
addWhy(detail) {
|
||||
why.push(detail);
|
||||
return detail;
|
||||
},
|
||||
log() {},
|
||||
};
|
||||
|
||||
const result = await stage(items, context);
|
||||
|
||||
assert.equal(result.length, 1);
|
||||
|
||||
const metadata = result[0].metadata.moderation;
|
||||
assert.equal(metadata.hidden, false);
|
||||
assert.equal(metadata.hideReason, "trusted-report-hide");
|
||||
assert.equal(metadata.hideBypass, "admin-whitelist");
|
||||
assert.deepEqual(metadata.hideCounts, { trustedMuteCount: 0, trustedReportCount: 6 });
|
||||
|
||||
const hideWhy = why.find((entry) => entry.reason === "trusted-report-hide");
|
||||
assert.ok(hideWhy);
|
||||
assert.equal(hideWhy.hidden, false);
|
||||
assert.equal(hideWhy.hideBypass, "admin-whitelist");
|
||||
assert.equal(hideWhy.adminWhitelist, true);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user