From 0dafb59c8ff7e2442aa823fd3efbb2e0b596e25e Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 30 Mar 2026 14:56:56 +0200 Subject: [PATCH] bug-fix relays. AGAIN. --- src/constants.ts | 7 +++- src/lib/relay-extended-tag-req-blocks.ts | 45 ++++++++++++++++++++++++ src/services/client-query.service.ts | 18 +++++++++- src/services/client.service.ts | 18 +++++++++- 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/lib/relay-extended-tag-req-blocks.ts diff --git a/src/constants.ts b/src/constants.ts index aa983999..3cf6e3d7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -273,7 +273,12 @@ export const SOCIAL_KIND_BLOCKED_RELAY_URLS = [ 'wss://hist.nostr.land', ] -/** Relays that reject #e (and similar) tag filters; skip for reply/quote/stats fetches. */ +/** + * Relays that reject certain tag filters in REQs (e.g. `#e` on some stacks) and, on nostr.sovbit.host, + * filter keys whose tag letter is uppercase (`#E`, `#A`, `#I`, …). Skip for reply/quote/stats fetches and + * whenever filters use a capital letter after `#` in a tag key (see `relayFiltersUseCapitalLetterTagKeys` in + * `relay-extended-tag-req-blocks.ts`). + */ export const E_TAG_FILTER_BLOCKED_RELAY_URLS = [ 'wss://nostr.v0l.io', 'wss://nostr.sovbit.host' diff --git a/src/lib/relay-extended-tag-req-blocks.ts b/src/lib/relay-extended-tag-req-blocks.ts new file mode 100644 index 00000000..179e39fe --- /dev/null +++ b/src/lib/relay-extended-tag-req-blocks.ts @@ -0,0 +1,45 @@ +import { E_TAG_FILTER_BLOCKED_RELAY_URLS } from '@/constants' +import { normalizeUrl } from '@/lib/url' +import type { Filter } from 'nostr-tools' + +let blockedLowerMemo: Set | null = null + +function extendedTagReqBlockedLowerSet(): Set { + if (!blockedLowerMemo) { + blockedLowerMemo = new Set( + E_TAG_FILTER_BLOCKED_RELAY_URLS.map((u) => (normalizeUrl(u) || u).toLowerCase()).filter(Boolean) + ) + } + return blockedLowerMemo +} + +/** NIP-01 tag filters are `#` + tag name; keys like `#E` / `#A` / `#I` are uppercase variants. */ +const CAPITAL_LEADING_TAG_FILTER_KEY = /^#[A-Z]/ + +function filterUsesCapitalLetterTagKey(f: Filter): boolean { + for (const k of Object.keys(f as Record)) { + if (CAPITAL_LEADING_TAG_FILTER_KEY.test(k)) return true + } + return false +} + +/** + * True if any filter object includes a tag filter whose key starts with `#` and an uppercase ASCII letter + * (e.g. `#E`, `#A`, `#I`). Some relays (notably nostr.sovbit.host) reject those keys entirely. + */ +export function relayFiltersUseCapitalLetterTagKeys(filter: Filter | Filter[]): boolean { + const filters = Array.isArray(filter) ? filter : [filter] + return filters.some(filterUsesCapitalLetterTagKey) +} + +/** + * Relays in {@link E_TAG_FILTER_BLOCKED_RELAY_URLS} reject `#e`-style queries and, on some stacks, any tag + * filter key that uses a capital letter after `#`. Drop them before REQ so we do not spam NOTICE/rate-limit responses. + */ +export function relayUrlsStripExtendedTagReqBlocked(urls: string[]): string[] { + const blocked = extendedTagReqBlockedLowerSet() + return urls.filter((u) => { + const n = normalizeUrl(u) || u.trim() + return n && !blocked.has(n.toLowerCase()) + }) +} diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts index a9c976d7..93bd7820 100644 --- a/src/services/client-query.service.ts +++ b/src/services/client-query.service.ts @@ -1,4 +1,5 @@ import { + FAST_READ_RELAY_URLS, FEED_FIRST_RELAY_RESULT_GRACE_MIN_LIMIT, FIRST_RELAY_RESULT_GRACE_MS, relayFilterIncludesSocialKindBlockedKind, @@ -9,6 +10,10 @@ import { RELAY_POOL_CONNECTION_TIMEOUT_MS, SEARCHABLE_RELAY_URLS } from '@/constants' +import { + relayFiltersUseCapitalLetterTagKeys, + relayUrlsStripExtendedTagReqBlocked +} from '@/lib/relay-extended-tag-req-blocks' import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' import { queueRelayAuthSign } from '@/lib/relay-auth-sign-queue' import { @@ -460,6 +465,12 @@ export class QueryService { const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped) } + if (relayFiltersUseCapitalLetterTagKeys(filters)) { + relays = relayUrlsStripExtendedTagReqBlocked(relays) + if (relays.length === 0) { + relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) + } + } if (this.shouldSkipRelayForSession) { relays = relays.filter((url) => { const n = normalizeUrl(url) || url @@ -692,7 +703,6 @@ export class QueryService { const originalDedupedRelays = Array.from(new Set(urls)) let relays = originalDedupedRelays if (relays.length === 0) { - const { FAST_READ_RELAY_URLS } = await import('@/constants') relays = [...FAST_READ_RELAY_URLS] } const filters = Array.isArray(filter) ? filter : [filter] @@ -704,6 +714,12 @@ export class QueryService { const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped) } + if (relayFiltersUseCapitalLetterTagKeys(filters)) { + relays = relayUrlsStripExtendedTagReqBlocked(relays) + if (relays.length === 0) { + relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) + } + } const { onevent, ...queryOpts } = options ?? {} return this.query(relays, filter, onevent, queryOpts) } diff --git a/src/services/client.service.ts b/src/services/client.service.ts index cb12f5ea..84534a20 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -57,6 +57,10 @@ import { isIndexRelayTransportFailure, publishEventToIndexRelay } from '@/lib/index-relay-http' +import { + relayFiltersUseCapitalLetterTagKeys, + relayUrlsStripExtendedTagReqBlocked +} from '@/lib/relay-extended-tag-req-blocks' import { stripLocalNetworkRelaysFromRelayList } from '@/lib/relay-list-sanitize' import { isHttpRelayUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl, simplifyUrl } from '@/lib/url' import { isSafari } from '@/lib/utils' @@ -1829,6 +1833,12 @@ class ClientService extends EventTarget { const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)) relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped) } + if (relayFiltersUseCapitalLetterTagKeys(filters)) { + relays = relayUrlsStripExtendedTagReqBlocked(relays) + if (relays.length === 0) { + relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) + } + } relays = this.relayUrlsAfterStrikesOrRecover(relays) // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -2153,7 +2163,13 @@ class ClientService extends EventTarget { relayReqLog?: { groupId: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void } } = {} ) { - const relays = Array.from(new Set(urls)) + let relays = Array.from(new Set(urls)) + if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) { + relays = relayUrlsStripExtendedTagReqBlocked(relays) + if (relays.length === 0) { + relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS]) + } + } const key = this.generateTimelineKey(relays, filter) let timeline = this.timelines[key]