From 372cb8ca6f7bc49282efb7a294943d849cfb0acb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:28:15 -0400 Subject: [PATCH] Add hashtag preference schema and feature flag --- docs/nostr-event-schemas.md | 12 ++++++++++++ js/constants.js | 28 +++++++++++++++++++++++++++ js/nostrEventSchemas.js | 38 +++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md index 7aa8b47b..80112cd8 100644 --- a/docs/nostr-event-schemas.md +++ b/docs/nostr-event-schemas.md @@ -147,6 +147,7 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca | Watch history chunk (`NOTE_TYPES.WATCH_HISTORY_CHUNK`) | `WATCH_HISTORY_KIND` (default `30079`, clients also read legacy `30078`) | `['d', ]`, `['encrypted','nip04']`, `['snapshot', ]`, `['chunk', , ]`, optional leading `['head','1']` on the first chunk, pointer tags for each item, plus schema append tags | NIP-04 encrypted JSON chunk (`{ version, snapshot, chunkIndex, totalChunks, items[] }`) | | Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30002` | `['d', 'subscriptions']` | NIP-04 encrypted JSON `{ subPubkeys: string[] }` | | User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `30002` | `['d', 'user-blocks']` | NIP-04 encrypted JSON `{ blockedPubkeys: string[] }` | +| Hashtag preferences (`NOTE_TYPES.HASHTAG_PREFERENCES`) | `30005` | `['d', 'bitvid:tag-preferences']` plus schema-appended `['encrypted','nip44_v2']` | NIP-44 encrypted JSON `{ version, interests: string[], disinterests: string[] }` | | Admin moderation list (`NOTE_TYPES.ADMIN_MODERATION_LIST`) | `30000` | `['d', 'bitvid:admin:editors']`, repeated `['p', ]` entries | Empty content | | Admin blacklist (`NOTE_TYPES.ADMIN_BLACKLIST`) | `30000` | `['d', 'bitvid:admin:blacklist']`, repeated `['p', ]` entries | Empty content | | Admin whitelist (`NOTE_TYPES.ADMIN_WHITELIST`) | `30000` | `['d', 'bitvid:admin:whitelist']`, repeated `['p', ]` entries | Empty content | @@ -157,6 +158,17 @@ If you introduce a new Nostr feature, add its schema to `js/nostrEventSchemas.js` so that the catalogue stays complete and so existing builders inherit the same debugging knobs. +### Hashtag preference lists + +Hashtag preference events (`NOTE_TYPES.HASHTAG_PREFERENCES`) live on a +replaceable `30005` list with a stable identifier tag of +`['d','bitvid:tag-preferences']`. The builder appends `['encrypted','nip44_v2']` +so downstream clients can detect that the payload is encrypted. The `content` +field must be a NIP-44 ciphertext representing the JSON shape +`{ version, interests: string[], disinterests: string[] }`, allowing clients to +share their preferred and muted hashtags without exposing the raw preferences on +relays. + ### NIP-71 rollout bitvid now emits a paired NIP-71 event (kind `21` for long-form, `22` for short diff --git a/js/constants.js b/js/constants.js index e14b572f..5610718b 100644 --- a/js/constants.js +++ b/js/constants.js @@ -147,6 +147,7 @@ const DEFAULT_FLAGS = Object.freeze({ VIEW_FILTER_INCLUDE_LEGACY_VIDEO: false, FEATURE_WATCH_HISTORY_V2: true, FEATURE_PUBLISH_NIP71: false, + FEATURE_HASHTAG_PREFERENCES: false, FEATURE_TRUST_SEEDS: true, // Rollback: disable to drop baseline trust seeds without code changes. FEATURE_TRUSTED_HIDE_CONTROLS: true, TRUSTED_MUTE_HIDE_THRESHOLD: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, @@ -167,6 +168,7 @@ const runtimeFlags = (() => { DEFAULT_FLAGS.VIEW_FILTER_INCLUDE_LEGACY_VIDEO, FEATURE_WATCH_HISTORY_V2: DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2, FEATURE_PUBLISH_NIP71: DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71, + FEATURE_HASHTAG_PREFERENCES: DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES, FEATURE_TRUST_SEEDS: DEFAULT_FLAGS.FEATURE_TRUST_SEEDS, FEATURE_TRUSTED_HIDE_CONTROLS: DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS, TRUSTED_MUTE_HIDE_THRESHOLD: DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD, @@ -204,6 +206,11 @@ export let FEATURE_PUBLISH_NIP71 = coerceBoolean( DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71 ); +export let FEATURE_HASHTAG_PREFERENCES = coerceBoolean( + runtimeFlags.FEATURE_HASHTAG_PREFERENCES, + DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES +); + export let FEATURE_TRUST_SEEDS = coerceBoolean( runtimeFlags.FEATURE_TRUST_SEEDS, DEFAULT_FLAGS.FEATURE_TRUST_SEEDS @@ -292,6 +299,20 @@ Object.defineProperty(runtimeFlags, "FEATURE_PUBLISH_NIP71", { }, }); +Object.defineProperty(runtimeFlags, "FEATURE_HASHTAG_PREFERENCES", { + configurable: true, + enumerable: true, + get() { + return FEATURE_HASHTAG_PREFERENCES; + }, + set(next) { + FEATURE_HASHTAG_PREFERENCES = coerceBoolean( + next, + DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES + ); + }, +}); + Object.defineProperty(runtimeFlags, "FEATURE_TRUST_SEEDS", { configurable: true, enumerable: true, @@ -367,6 +388,7 @@ runtimeFlags.ACCEPT_LEGACY_V1 = ACCEPT_LEGACY_V1; runtimeFlags.VIEW_FILTER_INCLUDE_LEGACY_VIDEO = VIEW_FILTER_INCLUDE_LEGACY_VIDEO; runtimeFlags.FEATURE_WATCH_HISTORY_V2 = FEATURE_WATCH_HISTORY_V2; runtimeFlags.FEATURE_PUBLISH_NIP71 = FEATURE_PUBLISH_NIP71; +runtimeFlags.FEATURE_HASHTAG_PREFERENCES = FEATURE_HASHTAG_PREFERENCES; runtimeFlags.FEATURE_TRUST_SEEDS = FEATURE_TRUST_SEEDS; runtimeFlags.FEATURE_TRUSTED_HIDE_CONTROLS = FEATURE_TRUSTED_HIDE_CONTROLS; runtimeFlags.WSS_TRACKERS = WSS_TRACKERS; @@ -407,6 +429,11 @@ export function setTrustedHideControlsEnabled(next) { return FEATURE_TRUSTED_HIDE_CONTROLS; } +export function setHashtagPreferencesEnabled(next) { + runtimeFlags.FEATURE_HASHTAG_PREFERENCES = next; + return FEATURE_HASHTAG_PREFERENCES; +} + export function resetRuntimeFlags() { setUrlFirstEnabled(DEFAULT_FLAGS.URL_FIRST_ENABLED); setAcceptLegacyV1(DEFAULT_FLAGS.ACCEPT_LEGACY_V1); @@ -414,6 +441,7 @@ export function resetRuntimeFlags() { DEFAULT_FLAGS.VIEW_FILTER_INCLUDE_LEGACY_VIDEO ); setWatchHistoryV2Enabled(DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2); + setHashtagPreferencesEnabled(DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES); setTrustSeedsEnabled(DEFAULT_FLAGS.FEATURE_TRUST_SEEDS); setTrustedHideControlsEnabled(DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS); setTrustedMuteHideThreshold(DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD); diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index bf697ca3..f619fd7f 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -22,6 +22,7 @@ export const NOTE_TYPES = Object.freeze({ WATCH_HISTORY_CHUNK: "watchHistoryChunk", SUBSCRIPTION_LIST: "subscriptionList", USER_BLOCK_LIST: "userBlockList", + HASHTAG_PREFERENCES: "hashtagPreferences", ADMIN_MODERATION_LIST: "adminModerationList", ADMIN_BLACKLIST: "adminBlacklist", ADMIN_WHITELIST: "adminWhitelist", @@ -313,6 +314,21 @@ const BASE_SCHEMAS = { description: "Encrypted JSON: { blockedPubkeys: string[] }.", }, }, + [NOTE_TYPES.HASHTAG_PREFERENCES]: { + type: NOTE_TYPES.HASHTAG_PREFERENCES, + label: "Hashtag preferences", + kind: 30005, + identifierTag: { + name: "d", + value: "bitvid:tag-preferences", + }, + appendTags: [["encrypted", "nip44_v2"]], + content: { + format: "nip44-json", + description: + "NIP-44 encrypted JSON: { version, interests: string[], disinterests: string[] }.", + }, + }, [NOTE_TYPES.ADMIN_MODERATION_LIST]: { type: NOTE_TYPES.ADMIN_MODERATION_LIST, label: "Admin moderation list", @@ -1133,6 +1149,28 @@ export function buildBlockListEvent({ }; } +export function buildHashtagPreferenceEvent({ + pubkey, + created_at, + content, +}) { + const schema = getNostrEventSchema(NOTE_TYPES.HASHTAG_PREFERENCES); + const tags = []; + const identifierName = schema?.identifierTag?.name || "d"; + const identifierValue = schema?.identifierTag?.value || "bitvid:tag-preferences"; + if (identifierName && identifierValue) { + tags.push([identifierName, identifierValue]); + } + appendSchemaTags(tags, schema); + return { + kind: schema?.kind ?? 30005, + pubkey, + created_at, + tags, + content: typeof content === "string" ? content : String(content ?? ""), + }; +} + function resolveAdminNoteType(listKey) { switch (listKey) { case "moderation":