Add shared hashtag normalization helper

This commit is contained in:
thePR0M3TH3AN committed 2026-01-18 13:35:34 -05:00
1 parent cedc2a91d0
commit 0ac64867da
10 files changed
+145 -77

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ export {
export {
createDedupeByRootStage,
createBlacklistFilterStage,
createTagPreferenceFilterStage,
createWatchHistorySuppressionStage,
createModerationStage,
createResolvePostedAtStage,
+53 -1
View File
@@ -10,10 +10,25 @@ 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, hasDisinterestedTag } 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;
@@ -258,6 +273,43 @@ export function createResolvePostedAtStage({
};
}
export function createTagPreferenceFilterStage({
stageName = "tag-preference-filter",
} = {}) {
return async function tagPreferenceFilterStage(items = [], context = {}) {
const tagPreferences = context?.runtime?.tagPreferences;
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
if (!disinterests.size) {
return items;
}
const results = [];
for (const item of items) {
const video = item?.video;
if (!video || typeof video !== "object") {
results.push(item);
continue;
}
if (hasDisinterestedTag(video, disinterests)) {
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;
}
results.push(item);
}
return results;
};
}
export function createBlacklistFilterStage({
stageName = "blacklist-filter",
shouldIncludeVideo,
@@ -277,7 +329,7 @@ export function createBlacklistFilterStage({
const options = { blacklistedEventIds: blacklist, isAuthorBlocked };
const tagPreferences = context?.runtime?.tagPreferences;
const disinterests = toSet(tagPreferences?.disinterests);
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
const hasTagPreferences = disinterests.size > 0;
const results = [];
+9 -2
View File
@@ -1,5 +1,7 @@
// js/feedEngine/utils.js
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
export function isPlainObject(value) {
if (value === null || typeof value !== "object") {
return false;
@@ -33,11 +35,16 @@ export function hasDisinterestedTag(video, disinterestsSet) {
return false;
}
const matchesDisinterest = (tag) => {
const normalized = normalizeHashtag(tag);
return Boolean(normalized && disinterestsSet.has(normalized));
};
// Check raw tags
if (Array.isArray(video.tags)) {
for (const tag of video.tags) {
if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") {
if (disinterestsSet.has(tag[1].toLowerCase())) {
if (matchesDisinterest(tag[1])) {
return true;
}
}
@@ -47,7 +54,7 @@ export function hasDisinterestedTag(video, disinterestsSet) {
// Check NIP-71 metadata hashtags if available
if (video.nip71 && Array.isArray(video.nip71.hashtags)) {
for (const tag of video.nip71.hashtags) {
if (typeof tag === "string" && disinterestsSet.has(tag.toLowerCase())) {
if (typeof tag === "string" && matchesDisinterest(tag)) {
return true;
}
}