mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-10 23:01:10 +00:00
fix(zaps): scope discovery to approved creators
This commit is contained in:
1 parent
7739a934f9
commit
2cda5025d4
7 files changed
+233
-16
No files matched your search
@@ -21,6 +21,7 @@ export {
|
||||
} from "./sorters.js";
|
||||
export {
|
||||
createActiveNostrSource,
|
||||
createWhitelistedAuthorsNostrSource,
|
||||
createSubscriptionAuthorsSource,
|
||||
createWatchHistoryPointerSource,
|
||||
} from "./sources.js";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// js/feedEngine/mostZappedFeed.js
|
||||
//
|
||||
// The "Most Zapped" tab (#47): the recently-added (active) source ranked by
|
||||
// ZAP TOTAL (sats from kind-9735 receipts). Mirrors trendingFeed.js — lives
|
||||
// outside feedCoordinator.js (size cap) and is wired in via thin delegations.
|
||||
// The "Most Zapped" tab (#47): the catalog of approved creators ranked by
|
||||
// ZAP TOTAL (sats from kind-9735 receipts). It is intentionally scoped to the
|
||||
// creator whitelist for discovery, but still runs the ordinary moderation
|
||||
// stages — whitelisting does not waive Web-of-Trust handling.
|
||||
// The metric getter uses requestVideoZapTotal, which returns the cached total
|
||||
// AND schedules a batched receipt fetch for unknown pointers; the Most Zapped
|
||||
// view re-runs the feed on zapTotals' change signal so the order settles as
|
||||
@@ -11,7 +12,7 @@
|
||||
import { FEED_TYPES } from "../constants.js";
|
||||
import { requestVideoZapTotal } from "../zapTotals.js";
|
||||
import {
|
||||
createActiveNostrSource,
|
||||
createWhitelistedAuthorsNostrSource,
|
||||
createBlacklistFilterStage,
|
||||
createDedupeByRootStage,
|
||||
createModerationStage,
|
||||
@@ -43,6 +44,19 @@ function makeGetZapTotal(app) {
|
||||
};
|
||||
}
|
||||
|
||||
function getWhitelistedAuthors(app) {
|
||||
const accessControl = app?.nostrService?.accessControl;
|
||||
if (typeof accessControl?.getWhitelistPubkeys !== "function") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const authors = accessControl.getWhitelistPubkeys();
|
||||
return Array.isArray(authors) ? authors : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function buildMostZappedFeedRuntime(app) {
|
||||
const blacklist =
|
||||
app.blacklistedEventIds instanceof Set
|
||||
@@ -59,6 +73,7 @@ export function buildMostZappedFeedRuntime(app) {
|
||||
? { ...moderationThresholds }
|
||||
: undefined,
|
||||
getZapTotal: makeGetZapTotal(app),
|
||||
whitelistedAuthors: getWhitelistedAuthors(app),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,7 +109,7 @@ export function registerMostZappedFeed(app) {
|
||||
|
||||
try {
|
||||
return app.feedEngine.registerFeed(FEED_TYPES.MOST_ZAPPED, {
|
||||
source: createActiveNostrSource({ service: app.nostrService }),
|
||||
source: createWhitelistedAuthorsNostrSource({ service: app.nostrService }),
|
||||
stages: [
|
||||
createBlacklistFilterStage({
|
||||
shouldIncludeVideo: (video, options) =>
|
||||
|
||||
@@ -62,6 +62,89 @@ export function createActiveNostrSource({ service } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Bounded discovery source for curated surfaces such as Most Zapped. It never
|
||||
// falls back to an unscoped relay request: the caller must provide approved
|
||||
// creator pubkeys in runtime.whitelistedAuthors. The successful remote result
|
||||
// is retained briefly so an asynchronous ranking update does not turn into a
|
||||
// second full relay scan for the same author set.
|
||||
export function createWhitelistedAuthorsNostrSource({
|
||||
service,
|
||||
refreshTtlMs = 2 * 60 * 1000,
|
||||
now = () => Date.now(),
|
||||
} = {}) {
|
||||
const resolvedService = resolveService(service, nostrService);
|
||||
let cachedAuthorsKey = "";
|
||||
let cachedVideos = [];
|
||||
let fetchedAt = 0;
|
||||
let inFlight = null;
|
||||
|
||||
const normalizeAuthors = (input) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
toArray(input)
|
||||
.map(normalizeAuthor)
|
||||
.filter(Boolean),
|
||||
),
|
||||
).sort();
|
||||
|
||||
return async function whitelistedAuthorsNostrSource(context = {}) {
|
||||
const authors = normalizeAuthors(context?.runtime?.whitelistedAuthors);
|
||||
const authorsKey = authors.join(",");
|
||||
if (!authors.length) {
|
||||
cachedAuthorsKey = "";
|
||||
cachedVideos = [];
|
||||
fetchedAt = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
const cacheIsCurrent =
|
||||
authorsKey === cachedAuthorsKey &&
|
||||
now() - fetchedAt < Math.max(0, Number(refreshTtlMs) || 0);
|
||||
|
||||
if (!cacheIsCurrent && !inFlight) {
|
||||
const fetchByAuthors = resolvedService?.fetchVideosByAuthors;
|
||||
inFlight = Promise.resolve(
|
||||
typeof fetchByAuthors === "function"
|
||||
? fetchByAuthors.call(resolvedService, authors)
|
||||
: [],
|
||||
)
|
||||
.then((videos) => {
|
||||
const allowed = new Set(authors);
|
||||
cachedAuthorsKey = authorsKey;
|
||||
cachedVideos = (Array.isArray(videos) ? videos : []).filter(
|
||||
(video) => allowed.has(normalizeAuthor(video?.pubkey)),
|
||||
);
|
||||
fetchedAt = now();
|
||||
return cachedVideos;
|
||||
})
|
||||
.catch((error) => {
|
||||
context?.log?.(
|
||||
"[whitelisted-authors-source] Failed to fetch approved creators",
|
||||
error,
|
||||
);
|
||||
return authorsKey === cachedAuthorsKey ? cachedVideos : [];
|
||||
})
|
||||
.finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
}
|
||||
|
||||
const videos = cacheIsCurrent ? cachedVideos : await inFlight;
|
||||
// The whitelist may change while a prior author-scoped request is still in
|
||||
// flight. Filter again at return time so that stale results can never
|
||||
// briefly render after an administrator removes a creator.
|
||||
const allowed = new Set(authors);
|
||||
return (Array.isArray(videos) ? videos : [])
|
||||
.filter((video) => allowed.has(normalizeAuthor(video?.pubkey)))
|
||||
.map((video) => ({
|
||||
video,
|
||||
metadata: {
|
||||
source: "nostr:whitelisted-authors",
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export function createSubscriptionAuthorsSource({ service } = {}) {
|
||||
const resolvedService = resolveService(service, nostrService);
|
||||
|
||||
|
||||
Reference in new issue
Block a user