mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-11 15:22:34 +00:00
update
This commit is contained in:
1 parent
b85a101749
commit
281a6511f1
165 files changed
+48618
-14019
No files matched your search
@@ -10,6 +10,7 @@ export {
|
||||
createBlacklistFilterStage,
|
||||
createWatchHistorySuppressionStage,
|
||||
createModerationStage,
|
||||
createResolvePostedAtStage,
|
||||
} from "./stages.js";
|
||||
export { createChronologicalSorter } from "./sorters.js";
|
||||
export {
|
||||
|
||||
@@ -1,14 +1,95 @@
|
||||
// js/feedEngine/sorters.js
|
||||
|
||||
export function createChronologicalSorter({ direction = "desc" } = {}) {
|
||||
export function createChronologicalSorter({
|
||||
direction = "desc",
|
||||
postedAtResolver,
|
||||
} = {}) {
|
||||
const normalizedDirection = direction === "asc" ? "asc" : "desc";
|
||||
|
||||
return function chronologicalSorter(items = []) {
|
||||
return function chronologicalSorter(items = [], context = {}) {
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const copy = [...items];
|
||||
const getTimestamp = (entry) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
const video = entry.video;
|
||||
if (!video || typeof video !== "object") {
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
const resolvePostedAtHook = () => {
|
||||
if (typeof postedAtResolver === "function") {
|
||||
try {
|
||||
const resolved = postedAtResolver({ entry, context, video });
|
||||
if (Number.isFinite(resolved)) {
|
||||
return Math.floor(resolved);
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.("[chronological-sorter] postedAtResolver threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
const hook = context?.hooks?.timestamps;
|
||||
if (!hook || typeof hook !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
hook.getKnownVideoPostedAt,
|
||||
hook.getKnownPostedAt,
|
||||
hook.getVideoPostedAt,
|
||||
hook.resolveVideoPostedAt,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate !== "function") {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const value = candidate(video, { entry, context });
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[chronological-sorter] timestamps hook threw",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const hookTimestamp = resolvePostedAtHook();
|
||||
if (Number.isFinite(hookTimestamp)) {
|
||||
return hookTimestamp;
|
||||
}
|
||||
|
||||
const rootCreatedAt = Number(video.rootCreatedAt);
|
||||
if (Number.isFinite(rootCreatedAt)) {
|
||||
return Math.floor(rootCreatedAt);
|
||||
}
|
||||
|
||||
const nip71CreatedAt = Number(video?.nip71Source?.created_at);
|
||||
if (Number.isFinite(nip71CreatedAt)) {
|
||||
return Math.floor(nip71CreatedAt);
|
||||
}
|
||||
|
||||
const createdAt = Number(video.created_at);
|
||||
if (Number.isFinite(createdAt)) {
|
||||
return Math.floor(createdAt);
|
||||
}
|
||||
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
};
|
||||
|
||||
copy.sort((a, b) => {
|
||||
const aMuted =
|
||||
a?.metadata?.moderation?.trustedMuted === true ||
|
||||
@@ -21,8 +102,8 @@ export function createChronologicalSorter({ direction = "desc" } = {}) {
|
||||
return aMuted ? 1 : -1;
|
||||
}
|
||||
|
||||
const aTs = Number(a?.video?.created_at);
|
||||
const bTs = Number(b?.video?.created_at);
|
||||
const aTs = getTimestamp(a);
|
||||
const bTs = getTimestamp(b);
|
||||
const normalizedATs = Number.isFinite(aTs) ? aTs : Number.NEGATIVE_INFINITY;
|
||||
const normalizedBTs = Number.isFinite(bTs) ? bTs : Number.NEGATIVE_INFINITY;
|
||||
|
||||
|
||||
+321
-22
@@ -2,6 +2,10 @@
|
||||
|
||||
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";
|
||||
@@ -75,6 +79,185 @@ export function createDedupeByRootStage({
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (const resolver of resolvers) {
|
||||
if (typeof resolver !== "function") {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await resolver(video, detail);
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(`[${stageName}] resolve timestamp hook threw`, 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 createBlacklistFilterStage({
|
||||
stageName = "blacklist-filter",
|
||||
shouldIncludeVideo,
|
||||
@@ -93,6 +276,10 @@ export function createBlacklistFilterStage({
|
||||
|
||||
const options = { blacklistedEventIds: blacklist, isAuthorBlocked };
|
||||
|
||||
// TODO(tag-preferences): incorporate context.runtime.tagPreferences once the
|
||||
// filtering helpers are in place so this stage can drop disinterested tags
|
||||
// without disturbing existing blacklist logic.
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
@@ -196,34 +383,112 @@ export function createWatchHistorySuppressionStage({
|
||||
|
||||
export function createModerationStage({
|
||||
stageName = "moderation",
|
||||
autoplayThreshold = 2,
|
||||
blurThreshold = 3,
|
||||
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))
|
||||
: 2;
|
||||
const normalizedBlurThreshold = Number.isFinite(blurThreshold)
|
||||
? Math.max(0, Math.floor(blurThreshold))
|
||||
: 3;
|
||||
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";
|
||||
|
||||
@@ -251,6 +516,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 : "";
|
||||
@@ -579,8 +849,11 @@ export function createModerationStage({
|
||||
trustedMuters = [];
|
||||
}
|
||||
|
||||
const blockAutoplay = trustedCount >= normalizedAutoplayThreshold;
|
||||
const blurThumbnail = trustedCount >= normalizedBlurThreshold;
|
||||
const blockAutoplay =
|
||||
trustedCount >= normalizedAutoplayThreshold || trustedMuted;
|
||||
const blurFromReports = trustedCount >= normalizedBlurThreshold;
|
||||
let blurThumbnail = blurFromReports;
|
||||
let blurReason = blurThumbnail ? "trusted-report" : "";
|
||||
const adminWhitelist = adminStatus?.whitelisted === true;
|
||||
const adminWhitelistBypass = false;
|
||||
|
||||
@@ -594,7 +867,6 @@ export function createModerationStage({
|
||||
: {};
|
||||
|
||||
metadataModeration.blockAutoplay = blockAutoplay;
|
||||
metadataModeration.blurThumbnail = blurThumbnail;
|
||||
metadataModeration.summary = summary;
|
||||
metadataModeration.trustedCount = trustedCount;
|
||||
metadataModeration.reportType = normalizedReportType;
|
||||
@@ -612,7 +884,6 @@ export function createModerationStage({
|
||||
}
|
||||
|
||||
video.moderation.blockAutoplay = blockAutoplay;
|
||||
video.moderation.blurThumbnail = blurThumbnail;
|
||||
video.moderation.trustedCount = trustedCount;
|
||||
video.moderation.reportType = normalizedReportType;
|
||||
video.moderation.adminWhitelist = adminWhitelist;
|
||||
@@ -660,9 +931,7 @@ export function createModerationStage({
|
||||
|
||||
let hideBypass = "";
|
||||
if (hideTriggered) {
|
||||
if (adminWhitelist) {
|
||||
hideBypass = "admin-whitelist";
|
||||
} else if (viewerOverrideActive) {
|
||||
if (viewerOverrideActive) {
|
||||
hideBypass = "viewer-override";
|
||||
} else if (feedPolicyBypass) {
|
||||
hideBypass = "feed-policy";
|
||||
@@ -711,6 +980,36 @@ export function createModerationStage({
|
||||
}
|
||||
}
|
||||
|
||||
if (!blurThumbnail && (trustedMuted || hideTriggered)) {
|
||||
blurThumbnail = true;
|
||||
if (hideTriggered) {
|
||||
blurReason = hideReason || "trusted-hide";
|
||||
} else if (trustedMuted) {
|
||||
blurReason = "trusted-mute";
|
||||
}
|
||||
} else if (blurThumbnail) {
|
||||
if (hideTriggered) {
|
||||
blurReason = hideReason || "trusted-hide";
|
||||
} else if (trustedMuted && !blurFromReports) {
|
||||
blurReason = "trusted-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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// js/feedEngine/watchHistoryFeed.js
|
||||
|
||||
import { pointerKey, normalizePointerInput } from "../nostr.js";
|
||||
import { pointerKey, normalizePointerInput } from "../nostr/watchHistory.js";
|
||||
import watchHistoryService from "../watchHistoryService.js";
|
||||
import nostrService from "../services/nostrService.js";
|
||||
import { createWatchHistoryMetadataResolver } from "../watchHistoryMetadata.js";
|
||||
|
||||
Reference in new issue
Block a user