Browse Source

fix stringified json spam filter

imwald
Silberengel 1 month ago
parent
commit
368f1a7336
  1. 32
      src/lib/event-ingest-filter.ts
  2. 24
      src/services/client-events.service.ts
  3. 4
      src/services/client-query.service.ts
  4. 6
      src/services/client.service.ts

32
src/lib/event-ingest-filter.ts

@ -1,19 +1,17 @@ @@ -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<number>([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<NEvent, 'kind' | 'content'>
): 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( @@ -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)
}

24
src/services/client-events.service.ts

@ -7,7 +7,7 @@ import indexedDb from './indexed-db.service' @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -561,7 +561,7 @@ export class EventService {
const eventsMap = new Map<string, NEvent>()
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,

4
src/services/client-query.service.ts

@ -5,7 +5,7 @@ import { @@ -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 { @@ -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

6
src/services/client.service.ts

@ -17,7 +17,7 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { @@ -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 { @@ -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 { @@ -1521,7 +1521,7 @@ class ClientService extends EventTarget {
const customEvent = data as CustomEvent<NEvent>
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)

Loading…
Cancel
Save