fix(zaps): scope discovery to approved creators

This commit is contained in:
thePR0M3TH3AN committed 2026-07-25 23:24:49 -04:00
1 parent 7739a934f9
commit 2cda5025d4
7 files changed
+233 -16

No files matched your search

+1
View File
@@ -21,6 +21,7 @@ export {
} from "./sorters.js";
export {
createActiveNostrSource,
createWhitelistedAuthorsNostrSource,
createSubscriptionAuthorsSource,
createWatchHistoryPointerSource,
} from "./sources.js";
+20 -5
View File
@@ -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) =>
+83
View File
@@ -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);