You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
4.8 KiB
124 lines
4.8 KiB
import { ExtendedKind } from '@/constants' |
|
import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata' |
|
import type { Event as NEvent } from 'nostr-tools' |
|
import { kinds } from 'nostr-tools' |
|
|
|
/** |
|
* 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. |
|
*/ |
|
function isStringifiedJsonObjectContentNostrEvent( |
|
event: Pick<NEvent, 'kind' | 'content'> |
|
): boolean { |
|
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 { |
|
const v = JSON.parse(c) as unknown |
|
return v !== null && typeof v === 'object' && !Array.isArray(v) |
|
} catch { |
|
return false |
|
} |
|
} |
|
|
|
/** |
|
* Kind-31987 noise: missing `d` (relay URL). Rating formats differ across clients; do not drop at ingest |
|
* (feeds and cards already treat unknown ratings as zero stars). |
|
*/ |
|
function isIncompleteRelayReviewIngest(event: NEvent): boolean { |
|
if (event.kind !== ExtendedKind.RELAY_REVIEW) return false |
|
return !getRelayUrlFromRelayReviewEvent(event) |
|
} |
|
|
|
/** |
|
* Kacti-style kind-1 “broadcast” payloads (non-human notes that flood index relays). Not valid Nostr discussion text. |
|
* Dropped from timelines, search, and prefetch; still loadable when the user opens that exact id (hex / note1 / nevent). |
|
*/ |
|
function isKactiBroadcastSpamKind1(event: Pick<NEvent, 'kind' | 'content'>): boolean { |
|
if (event.kind !== kinds.ShortTextNote) return false |
|
const c = typeof event.content === 'string' ? event.content.trimStart() : '' |
|
return c.startsWith('[broadcast:[#') |
|
} |
|
|
|
/** |
|
* drift.gits.net kind-1 payloads (`sp_<id>.….drift.gits.net` + `t` tag) — relay index noise, not discussion text. |
|
*/ |
|
/** Min length for kind-1 opaque blobs (base64-like relay noise). */ |
|
const OPAQUE_BLOB_KIND1_MIN_LEN = 80 |
|
/** Min length when there is no `=` padding but the whole note is still one opaque token. */ |
|
const OPAQUE_BLOB_KIND1_MIN_LEN_NO_PAD = 120 |
|
|
|
/** |
|
* Long single-token payloads (usually base64) with no readable text — relay index spam. |
|
*/ |
|
function isLongOpaqueRandomStringKind1(event: Pick<NEvent, 'kind' | 'content'>): boolean { |
|
if (event.kind !== kinds.ShortTextNote) return false |
|
const raw = typeof event.content === 'string' ? event.content : '' |
|
const compact = raw.trim().replace(/\s+/g, '') |
|
if (compact.length < OPAQUE_BLOB_KIND1_MIN_LEN) return false |
|
if (!/^[A-Za-z0-9+/]+=*$/.test(compact)) return false |
|
const bodyLen = compact.replace(/=+$/, '').length |
|
if (compact.endsWith('=')) { |
|
return bodyLen >= OPAQUE_BLOB_KIND1_MIN_LEN - 4 |
|
} |
|
return compact.length >= OPAQUE_BLOB_KIND1_MIN_LEN_NO_PAD |
|
} |
|
|
|
function isDriftGitsNetSpamKind1( |
|
event: Pick<NEvent, 'kind' | 'content' | 'tags'> |
|
): boolean { |
|
if (event.kind !== kinds.ShortTextNote) return false |
|
const c = typeof event.content === 'string' ? event.content.trim() : '' |
|
if (/\.drift\.gits\.net$/i.test(c) || /^sp_[0-9a-f]+\./i.test(c)) return true |
|
for (const tag of event.tags) { |
|
if (tag[0] === 't' && typeof tag[1] === 'string' && /^sp_[0-9a-f]+$/i.test(tag[1].trim())) { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
export type ShouldDropEventOnIngestOptions = { |
|
/** |
|
* When set to the same 64-char hex as {@link NEvent.id} (lowercase), kind-1 ingest spam filters do not apply |
|
* so `fetchEvent` / direct note views can still show the payload. |
|
*/ |
|
explicitNoteLookupHexId?: string |
|
} |
|
|
|
function explicitLookupMatchesEvent(eventId: string, lookup?: string): boolean { |
|
if (!lookup) return false |
|
const l = lookup.trim().toLowerCase() |
|
if (!/^[0-9a-f]{64}$/.test(l)) return false |
|
return eventId.toLowerCase() === l |
|
} |
|
|
|
/** NIP-71 addressable short video — dropped site-wide (relay spam). */ |
|
const DEPRECATED_NIP71_SHORT_VIDEO_ADDRESSABLE_KIND = 34236 |
|
|
|
/** |
|
* Single gate for subscribe/cache/IDB read paths: drop kind-1 JSON-object spam, Kacti broadcast spam, |
|
* drift.gits.net spam, long opaque random strings, and malformed relay reviews. Optional |
|
* {@link ShouldDropEventOnIngestOptions} relaxes |
|
* kind-1 spam drops for explicit id fetch. |
|
*/ |
|
export function shouldDropEventOnIngest( |
|
event: NEvent, |
|
options?: ShouldDropEventOnIngestOptions |
|
): boolean { |
|
if (event.kind === DEPRECATED_NIP71_SHORT_VIDEO_ADDRESSABLE_KIND) return true |
|
if (isIncompleteRelayReviewIngest(event)) return true |
|
if (isStringifiedJsonObjectContentNostrEvent(event)) return true |
|
const relaxKind1Spam = explicitLookupMatchesEvent(event.id, options?.explicitNoteLookupHexId) |
|
if (isKactiBroadcastSpamKind1(event)) { |
|
if (!relaxKind1Spam) return true |
|
} |
|
if (isDriftGitsNetSpamKind1(event)) { |
|
if (!relaxKind1Spam) return true |
|
} |
|
if (isLongOpaqueRandomStringKind1(event)) { |
|
if (!relaxKind1Spam) return true |
|
} |
|
return false |
|
}
|
|
|