From 368f1a7336aa55e1bc4e8719d2177c83331c0fbd Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 23 Mar 2026 06:23:16 +0100 Subject: [PATCH] fix stringified json spam filter --- src/lib/event-ingest-filter.ts | 32 +++++++++++++++++++-------- src/services/client-events.service.ts | 24 ++++++++++---------- src/services/client-query.service.ts | 4 ++-- src/services/client.service.ts | 6 ++--- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/lib/event-ingest-filter.ts b/src/lib/event-ingest-filter.ts index f01c5c2d..4ead6cfe 100644 --- a/src/lib/event-ingest-filter.ts +++ b/src/lib/event-ingest-filter.ts @@ -1,19 +1,17 @@ +import { ExtendedKind } from '@/constants' +import { getRelayUrlFromRelayReviewEvent, getStarsFromRelayReviewEvent } from '@/lib/event-metadata' import type { Event as NEvent } from 'nostr-tools' +import { kinds } from 'nostr-tools' /** - * Kinds whose `content` is conventionally a JSON object string per Nostr usage (not treated as app-protocol spam). - * Extend when another NIP documents JSON-in-content for a kind. - */ -export const NOSTR_KINDS_ALLOWED_STRINGIFIED_JSON_OBJECT_CONTENT = new Set([0]) - -/** - * True when `content` is a stringified JSON **object** (not arrays/primitives) on a kind that should carry human text - * or other non-JSON payloads — e.g. game/app data published as kind 31987 relay reviews. + * Detects **kind-1 note** spam where `content` is a stringified JSON **object** (game/app payloads, etc.) + * instead of human-readable text. Scoped to {@link kinds.ShortTextNote} only. */ export function isStringifiedJsonObjectContentNostrEvent( event: Pick ): boolean { - if (NOSTR_KINDS_ALLOWED_STRINGIFIED_JSON_OBJECT_CONTENT.has(event.kind)) return false + if (event.kind !== kinds.ShortTextNote) return false + const c = typeof event.content === 'string' ? event.content.trim() : '' if (c.length < 2 || c[0] !== '{' || c[c.length - 1] !== '}') return false try { @@ -23,3 +21,19 @@ export function isStringifiedJsonObjectContentNostrEvent( return false } } + +/** + * Kind-31987 noise: missing `d` (relay URL) or a parseable `rating` tag (see {@link getStarsFromRelayReviewEvent}). + * Content may be JSON or prose; structure is validated on tags, not `content`. + */ +export function isIncompleteRelayReviewIngest(event: NEvent): boolean { + if (event.kind !== ExtendedKind.RELAY_REVIEW) return false + if (!getRelayUrlFromRelayReviewEvent(event)) return true + if (!getStarsFromRelayReviewEvent(event)) return true + return false +} + +/** Single gate for subscribe/cache/IDB read paths: drop kind-1 JSON-object spam and malformed relay reviews. */ +export function shouldDropEventOnIngest(event: NEvent): boolean { + return isStringifiedJsonObjectContentNostrEvent(event) || isIncompleteRelayReviewIngest(event) +} diff --git a/src/services/client-events.service.ts b/src/services/client-events.service.ts index 5eb5811a..dcbfa8a7 100644 --- a/src/services/client-events.service.ts +++ b/src/services/client-events.service.ts @@ -7,7 +7,7 @@ import indexedDb from './indexed-db.service' import type { QueryService } from './client-query.service' import client from './client.service' import { isReplaceableEvent } from '@/lib/event' -import { isStringifiedJsonObjectContentNostrEvent } from '@/lib/event-ingest-filter' +import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' /** @@ -75,7 +75,7 @@ export class EventService { private getSessionEventIfAllowed(hexId: string): NEvent | undefined { const e = this.sessionEventCache.get(hexId) if (!e) return undefined - if (isStringifiedJsonObjectContentNostrEvent(e)) { + if (shouldDropEventOnIngest(e)) { this.sessionEventCache.delete(hexId) return undefined } @@ -151,11 +151,11 @@ export class EventService { const cachedPromise = this.eventCacheMap.get(hexId) if (cachedPromise) { const resolved = await cachedPromise - if (resolved && !isStringifiedJsonObjectContentNostrEvent(resolved)) return resolved + if (resolved && !shouldDropEventOnIngest(resolved)) return resolved const fromSessionAfterMiss = this.getSessionEventIfAllowed(hexId) if (fromSessionAfterMiss) return fromSessionAfterMiss const fromDb = await indexedDb.getEventFromPublicationStore(hexId) - if (fromDb && !isStringifiedJsonObjectContentNostrEvent(fromDb)) { + if (fromDb && !shouldDropEventOnIngest(fromDb)) { this.addEventToCache(fromDb) return fromDb } @@ -168,7 +168,7 @@ export class EventService { const fromSessionAfter = this.getSessionEventIfAllowed(hexId) if (fromSessionAfter) return fromSessionAfter } - if (loaded && isStringifiedJsonObjectContentNostrEvent(loaded)) { + if (loaded && shouldDropEventOnIngest(loaded)) { return undefined } return loaded @@ -318,7 +318,7 @@ export class EventService { * Add event to session cache */ addEventToCache(event: NEvent): void { - if (isStringifiedJsonObjectContentNostrEvent(event)) return + if (shouldDropEventOnIngest(event)) return const cleanEvent = { ...event } delete (cleanEvent as any).relayStatuses // REQ filters and nip19 decode use lowercase hex; some relays/clients emit uppercase ids. @@ -340,7 +340,7 @@ export class EventService { const queryLower = query.toLowerCase() for (const [, event] of this.sessionEventCache.entries()) { - if (isStringifiedJsonObjectContentNostrEvent(event)) continue + if (shouldDropEventOnIngest(event)) continue if (allowedKinds && !allowedKinds.includes(event.kind)) continue const content = event.content.toLowerCase() @@ -436,7 +436,7 @@ export class EventService { // Try cache first if (filter.ids?.length) { const cached = await indexedDb.getEventFromPublicationStore(filter.ids[0]) - if (cached && !isStringifiedJsonObjectContentNostrEvent(cached)) { + if (cached && !shouldDropEventOnIngest(cached)) { this.addEventToCache(cached) // Extract relay hints from cached event's tags (e, a, q tags) const eventRelayHints = this.extractRelayHintsFromEvent(cached) @@ -450,7 +450,7 @@ export class EventService { // Try big relays first (uses user's inboxes + defaults) if (filter.ids?.length) { const event = await this.fetchEventFromBigRelaysDataloader.load(filter.ids[0]) - if (event && !isStringifiedJsonObjectContentNostrEvent(event)) { + if (event && !shouldDropEventOnIngest(event)) { this.addEventToCache(event) // Extract relay hints from found event's tags (e, a, q tags) const eventRelayHints = this.extractRelayHintsFromEvent(event) @@ -463,7 +463,7 @@ export class EventService { // Always try comprehensive relay list (author's outboxes + user's inboxes + hints + seen + defaults) const event = await this.tryHarderToFetchEvent(relays, filter, true) - if (event && !isStringifiedJsonObjectContentNostrEvent(event)) { + if (event && !shouldDropEventOnIngest(event)) { this.addEventToCache(event) return event } @@ -523,7 +523,7 @@ export class EventService { }) const event = events - .filter((e) => !isStringifiedJsonObjectContentNostrEvent(e)) + .filter((e) => !shouldDropEventOnIngest(e)) .sort((a, b) => b.created_at - a.created_at)[0] // For non-replaceable events, we've already returned immediately via immediateReturn @@ -561,7 +561,7 @@ export class EventService { const eventsMap = new Map() for (const event of events) { - if (isStringifiedJsonObjectContentNostrEvent(event)) continue + if (shouldDropEventOnIngest(event)) continue const key = /^[0-9a-f]{64}$/i.test(event.id) ? event.id.toLowerCase() : event.id eventsMap.set(key, event) // Note: We can't track which relay returned which event in batch queries, diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts index 5c30bfe5..bebab664 100644 --- a/src/services/client-query.service.ts +++ b/src/services/client-query.service.ts @@ -5,7 +5,7 @@ import { MAX_CONCURRENT_RELAY_CONNECTIONS, SEARCHABLE_RELAY_URLS } from '@/constants' -import { isStringifiedJsonObjectContentNostrEvent } from '@/lib/event-ingest-filter' +import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' import logger from '@/lib/logger' import { normalizeUrl } from '@/lib/url' import type { Filter, Event as NEvent } from 'nostr-tools' @@ -396,7 +396,7 @@ export class QueryService { const forwardOnevent = callbacks.onevent ? (evt: NEvent) => { - if (isStringifiedJsonObjectContentNostrEvent(evt)) return + if (shouldDropEventOnIngest(evt)) return callbacks.onevent!(evt) } : undefined diff --git a/src/services/client.service.ts b/src/services/client.service.ts index ca973984..0ce63eb8 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -17,7 +17,7 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { const { search: _search, ...rest } = f return rest as Filter } -import { isStringifiedJsonObjectContentNostrEvent } from '@/lib/event-ingest-filter' +import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import logger from '@/lib/logger' import { dispatchTombstonesUpdated } from '@/lib/tombstone-events' @@ -1380,7 +1380,7 @@ class ClientService extends EventTarget { const forwardOnevent = onevent ? (evt: NEvent) => { - if (isStringifiedJsonObjectContentNostrEvent(evt)) return + if (shouldDropEventOnIngest(evt)) return onevent(evt) } : undefined @@ -1521,7 +1521,7 @@ class ClientService extends EventTarget { const customEvent = data as CustomEvent const evt = customEvent.detail if (!matchFilters(filters, evt)) return - if (isStringifiedJsonObjectContentNostrEvent(evt)) return + if (shouldDropEventOnIngest(evt)) return const id = evt.id const have = _knownIds.has(id)