Files

1388 lines
41 KiB
JavaScript

// js/feedEngine/stages.js
import { getApplication } from "../applicationContext.js";
import { IS_DEV_MODE } from "../config.js";
import {
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
DEFAULT_BLUR_THRESHOLD,
} from "../constants.js";
import nostrService from "../services/nostrService.js";
import moderationService from "../services/moderationService.js";
import logger from "../utils/logger.js";
import { dedupeToNewestByRoot } from "../utils/videoDeduper.js";
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
import { isPlainObject, toSet, markAsNormalized, getVideoTags } from "./utils.js";
const FEED_HIDE_BYPASS_NAMES = new Set(["home", "recent"]);
function normalizeTagSet(values) {
const normalized = new Set();
const source = toSet(values);
for (const value of source) {
const tag = normalizeHashtag(value);
if (tag) {
normalized.add(tag);
}
}
return normalized;
}
function resolveDedupeFunction(customDedupe) {
if (typeof customDedupe === "function") {
return customDedupe;
}
const app = getApplication?.();
if (app && typeof app.dedupeVideosByRoot === "function") {
return (videos) => app.dedupeVideosByRoot(videos);
}
return (videos) => dedupeToNewestByRoot(videos);
}
export function createDedupeByRootStage({
stageName = "dedupe-by-root",
dedupe: customDedupe,
} = {}) {
const dedupeFn = resolveDedupeFunction(customDedupe);
return async function dedupeByRootStage(items = [], context = {}) {
const videos = items.map((item) => item?.video).filter(Boolean);
const dedupedVideos = dedupeFn(videos) || [];
const allowedIds = new Set();
for (const video of dedupedVideos) {
if (video && typeof video.id === "string") {
allowedIds.add(video.id);
}
}
if (!allowedIds.size) {
return items;
}
const keep = [];
for (const item of items) {
const video = item?.video;
const videoId = video && typeof video.id === "string" ? video.id : "";
if (!videoId || allowedIds.has(videoId)) {
keep.push(item);
continue;
}
const rootId =
typeof video?.videoRootId === "string" && video.videoRootId
? video.videoRootId
: videoId;
if (typeof item?.metadata === "object" && item.metadata) {
item.metadata.droppedByStage = stageName;
}
context?.addWhy?.({
stage: stageName,
type: "dedupe",
reason: "older-root-version",
videoId,
rootId,
});
}
return markAsNormalized(keep);
};
}
function gatherTimestampFunctions({
context,
knownOverride,
resolveOverride,
}) {
const knownFns = [];
const resolveFns = [];
const addKnown = (fn) => {
if (typeof fn === "function") {
knownFns.push(fn);
}
};
const addResolver = (fn) => {
if (typeof fn === "function") {
resolveFns.push(fn);
}
};
addKnown(knownOverride);
addResolver(resolveOverride);
const hookCandidates = [context?.hooks, context?.runtime];
for (const candidate of hookCandidates) {
if (!candidate || typeof candidate !== "object") {
continue;
}
const timestamps = candidate.timestamps;
if (!timestamps || typeof timestamps !== "object") {
continue;
}
addKnown(timestamps.getKnownVideoPostedAt);
addKnown(timestamps.getKnownPostedAt);
addResolver(timestamps.resolveVideoPostedAt);
addResolver(timestamps.getVideoPostedAt);
}
return { knownFns, resolveFns };
}
function applyResolvedTimestamp(video, timestamp) {
if (!video || typeof video !== "object") {
return false;
}
if (!Number.isFinite(timestamp)) {
return false;
}
const normalized = Math.floor(timestamp);
if (!Number.isFinite(normalized)) {
return false;
}
if (Number.isFinite(video.rootCreatedAt)) {
const existing = Math.floor(video.rootCreatedAt);
if (Number.isFinite(existing) && existing <= normalized) {
video.rootCreatedAt = existing;
return true;
}
}
video.rootCreatedAt = normalized;
return true;
}
async function resolveTimestampWithResolvers({
video,
detail,
resolvers,
stageName,
context,
}) {
if (!Array.isArray(resolvers) || resolvers.length === 0) {
return null;
}
const promises = resolvers.map(async (resolver) => {
if (typeof resolver !== "function") {
throw new Error("Resolver is not a function");
}
try {
const value = await resolver(video, detail);
if (Number.isFinite(value)) {
return Math.floor(value);
}
throw new Error("Invalid timestamp value");
} catch (error) {
if (error.message !== "Invalid timestamp value") {
context?.log?.(`[${stageName}] resolve timestamp hook threw`, error);
}
throw error;
}
});
try {
return await Promise.any(promises);
} catch (error) {
return null;
}
}
export function createResolvePostedAtStage({
stageName = "resolve-posted-at",
getKnownPostedAt,
resolvePostedAt,
} = {}) {
return async function resolvePostedAtStage(items = [], context = {}) {
if (!Array.isArray(items) || items.length === 0) {
return items;
}
const { knownFns, resolveFns } = gatherTimestampFunctions({
context,
knownOverride: getKnownPostedAt,
resolveOverride: resolvePostedAt,
});
if (knownFns.length === 0 && resolveFns.length === 0) {
return items;
}
const tasks = [];
for (const item of items) {
const video = item?.video;
if (!video || typeof video !== "object") {
continue;
}
if (Number.isFinite(video.rootCreatedAt)) {
continue;
}
const detail = { entry: item, context };
let knownValue = null;
for (const candidate of knownFns) {
try {
const value = candidate(video, detail);
if (Number.isFinite(value)) {
knownValue = Math.floor(value);
break;
}
} catch (error) {
context?.log?.(`[${stageName}] known timestamp hook threw`, error);
}
}
if (knownValue !== null && Number.isFinite(knownValue)) {
applyResolvedTimestamp(video, knownValue);
continue;
}
if (!resolveFns.length) {
continue;
}
tasks.push(async () => {
const resolved = await resolveTimestampWithResolvers({
video,
detail,
resolvers: resolveFns,
stageName,
context,
});
if (resolved !== null && Number.isFinite(resolved)) {
applyResolvedTimestamp(video, resolved);
}
});
}
for (const task of tasks) {
try {
await task();
} catch (error) {
context?.log?.(`[${stageName}] resolution task failed`, error);
}
}
return items;
};
}
export function createTagPreferenceFilterStage({
stageName = "tag-preference-filter",
enforceInterests,
matchInterests = true,
} = {}) {
const shouldMatchInterests =
typeof enforceInterests === "boolean"
? enforceInterests
: matchInterests !== false;
return async function tagPreferenceFilterStage(items = [], context = {}) {
return filterByTagPreferences({
items,
context,
stageName,
enforceInterests: shouldMatchInterests,
});
};
}
export function createDisinterestFilterStage({
stageName = "tag-disinterest-filter",
} = {}) {
return async function disinterestFilterStage(items = [], context = {}) {
return filterByTagPreferences({
items,
context,
stageName,
enforceInterests: false,
});
};
}
function filterByTagPreferences({
items = [],
context = {},
stageName,
enforceInterests,
}) {
const tagPreferences = context?.runtime?.tagPreferences;
if (tagPreferences?.available === false) {
context?.addWhy?.({
stage: stageName,
type: "status",
reason: "preferences-unavailable",
});
return items;
}
const interests = normalizeTagSet(tagPreferences?.interests);
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
if (!interests.size && !disinterests.size) {
return items;
}
const results = [];
for (const item of items) {
const video = item?.video;
if (!video || typeof video !== "object") {
results.push(item);
continue;
}
const videoTags = getVideoTags(video);
let disinterested = false;
if (disinterests.size) {
for (const tag of videoTags) {
if (disinterests.has(tag)) {
disinterested = true;
break;
}
}
}
if (disinterested) {
context?.addWhy?.({
stage: stageName,
type: "filter",
reason: "disinterested-tag",
videoId: typeof video.id === "string" ? video.id : null,
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
});
continue;
}
let matchedInterests = [];
if (interests.size) {
matchedInterests = [...videoTags].filter((tag) => interests.has(tag));
if (enforceInterests && matchedInterests.length === 0) {
context?.addWhy?.({
stage: stageName,
type: "filter",
reason: "no-interest-match",
videoId: typeof video.id === "string" ? video.id : null,
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
});
continue;
}
}
if (matchedInterests.length > 0) {
if (!isPlainObject(item.metadata)) {
item.metadata = {};
}
item.metadata.matchedInterests = matchedInterests;
context?.addWhy?.({
stage: stageName,
type: "filter",
reason: "matched-interests",
videoId: typeof video.id === "string" ? video.id : null,
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
tags: matchedInterests,
});
}
results.push(item);
}
return markAsNormalized(results);
}
export function createBlacklistFilterStage({
stageName = "blacklist-filter",
shouldIncludeVideo,
} = {}) {
const includeFn =
typeof shouldIncludeVideo === "function"
? shouldIncludeVideo
: (video, options) => nostrService.shouldIncludeVideo(video, options);
return async function blacklistFilterStage(items = [], context = {}) {
const blacklist = toSet(context?.runtime?.blacklistedEventIds);
const isAuthorBlocked =
typeof context?.runtime?.isAuthorBlocked === "function"
? context.runtime.isAuthorBlocked
: () => false;
const options = { blacklistedEventIds: blacklist, isAuthorBlocked };
const results = [];
for (const item of items) {
const video = item?.video;
if (!video || typeof video !== "object") {
results.push(item);
continue;
}
let include = false;
try {
include = includeFn(video, options) !== false;
} catch (error) {
context?.log?.(`[${stageName}] shouldIncludeVideo threw`, error);
include = false;
}
if (!include) {
context?.addWhy?.({
stage: stageName,
type: "filter",
reason: "blacklist",
videoId: typeof video.id === "string" ? video.id : null,
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
});
continue;
}
results.push(item);
}
return markAsNormalized(results);
};
}
function resolveSuppressionHook(context) {
if (!context || typeof context !== "object") {
return null;
}
const fromHooks = context?.hooks?.watchHistory;
if (isPlainObject(fromHooks) && typeof fromHooks.shouldSuppress === "function") {
return fromHooks.shouldSuppress;
}
const runtime = context?.runtime?.watchHistory;
if (isPlainObject(runtime) && typeof runtime.shouldSuppress === "function") {
return runtime.shouldSuppress;
}
return null;
}
export function createWatchHistorySuppressionStage({
stageName = "watch-history-suppression",
shouldSuppress,
} = {}) {
return async function watchHistorySuppressionStage(items = [], context = {}) {
const hook =
typeof shouldSuppress === "function" ? shouldSuppress : resolveSuppressionHook(context);
if (typeof hook !== "function") {
return items;
}
const results = [];
for (const item of items) {
let suppress = false;
try {
suppress = await hook(item, context);
} catch (error) {
context?.log?.(`[${stageName}] suppression hook threw`, error);
suppress = false;
}
if (suppress) {
const detail = {
stage: stageName,
type: "filter",
reason: "watch-history",
};
if (item?.video && typeof item.video.id === "string") {
detail.videoId = item.video.id;
}
if (item?.pointer) {
detail.pointer = item.pointer;
} else if (item?.metadata && item.metadata.pointerKey) {
detail.pointerKey = item.metadata.pointerKey;
}
context?.addWhy?.(detail);
continue;
}
results.push(item);
}
return markAsNormalized(results);
};
}
export function createModerationStage({
stageName = "moderation",
autoplayThreshold,
blurThreshold,
reportType = "nudity",
service = null,
getService,
trustedMuteHideThreshold,
trustedReportHideThreshold,
} = {}) {
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 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 resolveMuteHideThresholdBase = createThresholdResolver(
trustedMuteHideThreshold,
{
runtimeKey: "trustedMuteHideThreshold",
defaultValue: Number.POSITIVE_INFINITY,
fallbackValue: Number.POSITIVE_INFINITY,
allowInfinity: true,
},
);
const resolveMuteHideThreshold = (context, category) => {
const base = resolveMuteHideThresholdBase(context);
const normalizedCategory =
typeof category === "string" ? category.trim().toLowerCase() : "";
if (!normalizedCategory) {
return base;
}
const runtimeThresholds =
context?.runtime && typeof context.runtime === "object"
? context.runtime.moderationThresholds
: null;
const thresholdMap =
runtimeThresholds && typeof runtimeThresholds === "object"
? runtimeThresholds.trustedMuteHideThresholds
: null;
if (!thresholdMap || typeof thresholdMap !== "object") {
return base;
}
const override = thresholdMap[normalizedCategory];
if (Number.isFinite(override)) {
return Math.max(0, Math.floor(override));
}
return base;
};
const resolveReportHideThreshold = createThresholdResolver(
trustedReportHideThreshold,
{
runtimeKey: "trustedSpamHideThreshold",
defaultValue: Number.POSITIVE_INFINITY,
fallbackValue: Number.POSITIVE_INFINITY,
allowInfinity: true,
},
);
const normalizedReportType = typeof reportType === "string" ? reportType.trim().toLowerCase() : "nudity";
return async function moderationStage(items = [], context = {}) {
let resolvedService = null;
try {
resolvedService =
typeof getService === "function"
? getService({ context })
: service || nostrService.getModerationService?.() || moderationService;
} catch (error) {
context?.log?.(`[${stageName}] Failed to resolve moderation service`, error);
resolvedService = service || moderationService;
}
if (!resolvedService) {
return items;
}
try {
if (typeof resolvedService.refreshViewerFromClient === "function") {
await resolvedService.refreshViewerFromClient();
}
} catch (error) {
context?.log?.(`[${stageName}] Failed to refresh viewer context`, error);
}
const normalizedAutoplayThreshold = resolveAutoplayThreshold(context);
const normalizedBlurThreshold = resolveBlurThreshold(context);
const normalizedReportHideThreshold = resolveReportHideThreshold(context);
const activeIds = new Set();
for (const item of items) {
const videoId = typeof item?.video?.id === "string" ? item.video.id : "";
if (videoId) {
activeIds.add(videoId);
}
}
try {
if (typeof resolvedService.setActiveEventIds === "function") {
await resolvedService.setActiveEventIds(activeIds);
}
} catch (error) {
context?.log?.(`[${stageName}] Failed to prime report subscriptions`, error);
}
const toStringSet = (input, { lowercase = false } = {}) => {
const output = new Set();
if (input instanceof Set) {
for (const value of input) {
if (typeof value !== "string") {
continue;
}
const trimmed = value.trim();
if (!trimmed) {
continue;
}
output.add(lowercase ? trimmed.toLowerCase() : trimmed);
}
return output;
}
if (Array.isArray(input)) {
for (const value of input) {
if (typeof value !== "string") {
continue;
}
const trimmed = value.trim();
if (!trimmed) {
continue;
}
output.add(lowercase ? trimmed.toLowerCase() : trimmed);
}
}
return output;
};
let adminSnapshot = {
whitelist: new Set(),
whitelistHex: new Set(),
blacklist: new Set(),
blacklistHex: new Set(),
};
try {
if (typeof resolvedService.getAdminListSnapshot === "function") {
const snapshot = resolvedService.getAdminListSnapshot();
if (snapshot && typeof snapshot === "object") {
adminSnapshot = {
whitelist: toStringSet(snapshot.whitelist),
whitelistHex: toStringSet(snapshot.whitelistHex, { lowercase: true }),
blacklist: toStringSet(snapshot.blacklist),
blacklistHex: toStringSet(snapshot.blacklistHex, { lowercase: true }),
};
}
}
} catch (error) {
context?.log?.(`[${stageName}] Failed to load admin list snapshot`, error);
}
const resolveAdminStatus =
typeof resolvedService.getAccessControlStatus === "function"
? (identifier) => {
try {
return (
resolvedService.getAccessControlStatus(identifier, adminSnapshot) || {
hex: "",
whitelisted: false,
blacklisted: false,
}
);
} catch (error) {
context?.log?.(`[${stageName}] getAccessControlStatus threw`, error);
return { hex: "", whitelisted: false, blacklisted: false };
}
}
: () => ({ 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 = [];
let authorModerationInfo = new Map();
try {
if (typeof resolvedService.getModerationInfoForAuthors === "function") {
const authorSet = new Set();
for (const item of items) {
const pubkey = item?.video?.pubkey;
if (typeof pubkey === "string") {
authorSet.add(pubkey);
}
}
if (authorSet.size > 0) {
authorModerationInfo = resolvedService.getModerationInfoForAuthors(authorSet);
}
}
} catch (error) {
context?.log?.(`[${stageName}] Failed to batch fetch moderation info`, error);
}
for (const item of items) {
if (!item || typeof item !== "object") {
results.push(item);
continue;
}
const video = item.video;
if (!video || typeof video !== "object") {
results.push(item);
continue;
}
const videoId = typeof video.id === "string" ? video.id : "";
const authorPubkey = typeof video.pubkey === "string" ? video.pubkey : "";
let viewerBlocked = false;
if (authorPubkey && typeof context?.runtime?.isAuthorBlocked === "function") {
try {
viewerBlocked = !!context.runtime.isAuthorBlocked(authorPubkey);
} catch (error) {
context?.log?.(`[${stageName}] isAuthorBlocked threw`, error);
}
}
const adminStatus = resolveAdminStatus(authorPubkey);
const authorHex =
(adminStatus?.hex && typeof adminStatus.hex === "string"
? adminStatus.hex
: authorPubkey)
?.trim()
.toLowerCase() || "";
if (viewerBlocked) {
context?.addWhy?.({
stage: stageName,
type: "filter",
reason: "viewer-block",
videoId: videoId || null,
pubkey: authorHex || authorPubkey || null,
});
continue;
}
if (adminStatus?.blacklisted) {
context?.addWhy?.({
stage: stageName,
type: "filter",
reason: "admin-blacklist",
videoId: videoId || null,
pubkey: authorHex || authorPubkey || null,
});
continue;
}
let summary = null;
let trustedCount = 0;
let trustedReporters = [];
let trustedMuted = false;
let trustedMuters = [];
let trustedMuteCount = 0;
let trustedMuteCountTotal = 0;
let trustedMuteCategory = "";
let trustedMuteCountsByCategory = null;
let viewerMuted = false;
if (videoId) {
try {
if (typeof resolvedService.getTrustedReportSummary === "function") {
summary = resolvedService.getTrustedReportSummary(videoId);
}
if (typeof resolvedService.trustedReportCount === "function") {
trustedCount = resolvedService.trustedReportCount(videoId, normalizedReportType) || 0;
}
if (typeof resolvedService.getTrustedReporters === "function") {
const reporterEntries = resolvedService.getTrustedReporters(
videoId,
normalizedReportType
);
if (Array.isArray(reporterEntries) && reporterEntries.length) {
trustedReporters = reporterEntries
.map((entry) => {
if (!entry || typeof entry !== "object") {
return null;
}
const pubkey = typeof entry.pubkey === "string" ? entry.pubkey.trim() : "";
if (!pubkey) {
return null;
}
const latest = Number.isFinite(entry.latest)
? Math.floor(entry.latest)
: 0;
return { pubkey, latest };
})
.filter(Boolean);
}
}
} catch (error) {
context?.log?.(`[${stageName}] Failed to fetch moderation summary`, error);
summary = null;
trustedCount = 0;
trustedReporters = [];
}
}
if (authorHex) {
try {
let infoFound = false;
if (authorModerationInfo instanceof Map) {
const info = authorModerationInfo.get(authorHex);
if (info) {
trustedMuted = info.trustedMuted === true;
trustedMuters = Array.isArray(info.trustedMuters) ? info.trustedMuters : [];
trustedMuteCountTotal = Number.isFinite(info.trustedMuteCountTotal)
? Math.max(0, Math.floor(info.trustedMuteCountTotal))
: 0;
trustedMuteCountsByCategory =
info.trustedMuteCountsByCategory && typeof info.trustedMuteCountsByCategory === "object"
? info.trustedMuteCountsByCategory
: null;
infoFound = true;
}
}
if (!infoFound) {
if (typeof resolvedService.isAuthorMutedByTrusted === "function") {
trustedMuted = resolvedService.isAuthorMutedByTrusted(authorHex) === true;
}
if (trustedMuted && typeof resolvedService.getTrustedMutersForAuthor === "function") {
const muters = resolvedService.getTrustedMutersForAuthor(authorHex);
if (Array.isArray(muters)) {
const seen = new Set();
trustedMuters = muters
.map((value) => (typeof value === "string" ? value.trim().toLowerCase() : ""))
.filter((value) => {
if (!value || seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
}
if (
trustedMuted &&
typeof resolvedService.getTrustedMuteCountsForAuthor === "function"
) {
const summary = resolvedService.getTrustedMuteCountsForAuthor(authorHex);
if (summary && typeof summary === "object") {
trustedMuteCountTotal = Number.isFinite(summary.total)
? Math.max(0, Math.floor(summary.total))
: 0;
trustedMuteCountsByCategory =
summary.categories && typeof summary.categories === "object"
? { ...summary.categories }
: null;
}
}
}
if (typeof resolvedService.isAuthorMutedByViewer === "function") {
viewerMuted = resolvedService.isAuthorMutedByViewer(authorHex) === true;
}
} catch (error) {
context?.log?.(`[${stageName}] Failed to resolve trusted/viewer mute info`, error);
trustedMuted = false;
trustedMuters = [];
viewerMuted = false;
}
}
if (trustedMuted) {
trustedMuteCountTotal = Number.isFinite(video?.moderation?.trustedMuteCount)
? Math.max(0, Math.floor(video.moderation.trustedMuteCount))
: trustedMuteCountTotal || trustedMuters.length;
if (!trustedMuteCountTotal) {
trustedMuteCountTotal = trustedMuters.length;
}
if (trustedMuteCountTotal <= 0) {
trustedMuteCountTotal = Math.max(1, trustedMuters.length || 1);
}
trustedMuteCategory = normalizedReportType;
if (
trustedMuteCountsByCategory &&
typeof trustedMuteCountsByCategory === "object"
) {
const categoryCount =
trustedMuteCategory &&
Number.isFinite(trustedMuteCountsByCategory[trustedMuteCategory])
? Math.max(
0,
Math.floor(trustedMuteCountsByCategory[trustedMuteCategory]),
)
: null;
if (categoryCount !== null && categoryCount !== undefined) {
trustedMuteCount = categoryCount;
} else if (!trustedMuteCategory) {
let bestCategory = "";
let bestCount = 0;
for (const [category, count] of Object.entries(
trustedMuteCountsByCategory,
)) {
if (!Number.isFinite(count)) {
continue;
}
const normalizedCategory = category.trim().toLowerCase();
const normalizedCount = Math.max(0, Math.floor(count));
if (!normalizedCategory) {
continue;
}
if (normalizedCount > bestCount) {
bestCount = normalizedCount;
bestCategory = normalizedCategory;
}
}
if (bestCategory) {
trustedMuteCategory = bestCategory;
trustedMuteCount = bestCount;
} else {
trustedMuteCount = trustedMuteCountTotal;
}
} else {
trustedMuteCount = trustedMuteCountTotal;
}
} else {
trustedMuteCount = trustedMuteCountTotal;
}
if (trustedMuteCount <= 0) {
trustedMuteCount = trustedMuteCountTotal || Math.max(1, trustedMuters.length || 1);
}
} else {
trustedMuteCount = 0;
trustedMuteCountTotal = 0;
trustedMuteCategory = "";
trustedMuters = [];
}
const normalizedMuteHideThreshold = resolveMuteHideThreshold(
context,
trustedMuteCategory,
);
const adminWhitelist = adminStatus?.whitelisted === true;
const adminWhitelistBypass = false;
// A trusted mute is a ranking signal, not a content verdict. It sinks an
// author in feed sorters, but a single stale or bad-faith mute must not
// blur a creator or stop playback. Only the report threshold, the
// viewer's own mute, or the higher mute-hide threshold can do that.
const blockAutoplay = trustedCount >= normalizedAutoplayThreshold || viewerMuted;
const blurFromReports = trustedCount >= normalizedBlurThreshold;
let blurThumbnail = blurFromReports;
let blurReason = blurThumbnail ? "trusted-report" : "";
if (!item.metadata || typeof item.metadata !== "object") {
item.metadata = {};
}
const metadataModeration =
item.metadata.moderation && typeof item.metadata.moderation === "object"
? { ...item.metadata.moderation }
: {};
metadataModeration.blockAutoplay = blockAutoplay;
metadataModeration.summary = summary;
metadataModeration.trustedCount = trustedCount;
metadataModeration.reportType = normalizedReportType;
metadataModeration.trustedReporters = Array.isArray(trustedReporters)
? trustedReporters.slice()
: [];
metadataModeration.adminWhitelist = adminWhitelist;
metadataModeration.adminWhitelistBypass = adminWhitelistBypass;
metadataModeration.trustedMuted = trustedMuted;
metadataModeration.trustedMuters = trustedMuters.slice();
metadataModeration.trustedMuteCount = trustedMuteCount;
metadataModeration.viewerMuted = viewerMuted;
if (!video.moderation || typeof video.moderation !== "object") {
video.moderation = {};
}
video.moderation.blockAutoplay = blockAutoplay;
video.moderation.trustedCount = trustedCount;
video.moderation.reportType = normalizedReportType;
video.moderation.adminWhitelist = adminWhitelist;
video.moderation.adminWhitelistBypass = adminWhitelistBypass;
video.moderation.trustedMuted = trustedMuted;
if (trustedMuted) {
video.moderation.trustedMuters = trustedMuters.slice();
video.moderation.trustedMuteCount = trustedMuteCount;
} else {
if (video.moderation.trustedMuters) {
delete video.moderation.trustedMuters;
}
if (video.moderation.trustedMuteCount) {
delete video.moderation.trustedMuteCount;
}
}
video.moderation.viewerMuted = viewerMuted;
if (Array.isArray(trustedReporters) && trustedReporters.length) {
video.moderation.trustedReporters = trustedReporters.slice();
} else if (video.moderation.trustedReporters) {
delete video.moderation.trustedReporters;
}
if (summary) {
video.moderation.summary = summary;
} else if (video.moderation.summary) {
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 (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;
}
}
if (!blurThumbnail && (viewerMuted || hideTriggered)) {
blurThumbnail = true;
if (hideTriggered) {
blurReason = hideReason || "trusted-hide";
} else if (viewerMuted) {
blurReason = "viewer-mute";
}
} else if (blurThumbnail) {
if (hideTriggered) {
blurReason = hideReason || "trusted-hide";
} else if (viewerMuted && !blurFromReports) {
blurReason = "viewer-mute";
} else if (!blurReason && blurFromReports) {
blurReason = "trusted-report";
}
}
metadataModeration.blurThumbnail = blurThumbnail;
if (blurThumbnail) {
metadataModeration.blurReason = blurReason;
} else if (metadataModeration.blurReason) {
delete metadataModeration.blurReason;
}
video.moderation.blurThumbnail = blurThumbnail;
if (blurThumbnail) {
video.moderation.blurReason = blurReason;
} else if (video.moderation.blurReason) {
delete video.moderation.blurReason;
}
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,
type: "moderation",
reason: blurThumbnail ? "blur" : "autoplay-block",
videoId: videoId || null,
reportType: normalizedReportType,
trustedCount,
});
}
if (trustedMuted) {
context?.addWhy?.({
stage: stageName,
type: "moderation",
reason: "trusted-mute",
videoId: videoId || null,
pubkey: authorHex || authorPubkey || null,
trustedMuteCount,
});
}
if (viewerMuted) {
context?.addWhy?.({
stage: stageName,
type: "moderation",
reason: "viewer-mute",
videoId: videoId || null,
pubkey: authorHex || authorPubkey || null,
});
}
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);
}
return markAsNormalized(results);
};
}