From 821e9bafdd29b9ea2240b968a5a77d5ebec648e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 20:50:33 +0000 Subject: [PATCH] fix: drop the "Mock Podcast" kind:10154 spam flood before consuming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Someone is flooding thousands of identical mock NIP-F4 show-metadata events. They share an exact fingerprint — title "Mock Podcast", description and content both "Headless test feed" — so match that and refuse to cache them. - PodcastMetadataEvent.isMockSpam() encodes the fingerprint (all three fields must match, so a real show sharing one field is never flagged); unit-tested. - LocalCache's PodcastMetadataEvent consume branch returns without storing when it matches, so the spam never reaches the cache, feeds, search, or the merged podcast list. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18 --- .../amethyst/model/LocalCache.kt | 7 +- .../metadata/PodcastMetadataEvent.kt | 13 ++++ .../nipF4Podcasts/PodcastMockSpamTest.kt | 64 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/PodcastMockSpamTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index b5a1d5596f..59fc275990 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -3754,7 +3754,12 @@ object LocalCache : ILocalCache, ICacheProvider { } is PodcastMetadataEvent -> { - consumeBaseReplaceable(event, relay, wasVerified) + // Drop the known "Mock Podcast" spam flood instead of caching thousands of them. + if (event.isMockSpam()) { + false + } else { + consumeBaseReplaceable(event, relay, wasVerified) + } } is AuthoredPodcastsEvent -> { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/metadata/PodcastMetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/metadata/PodcastMetadataEvent.kt index d2739b18eb..fa02e05ec0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/metadata/PodcastMetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/metadata/PodcastMetadataEvent.kt @@ -83,9 +83,22 @@ class PodcastMetadataEvent( */ fun claimedAuthors() = tags.mapNotNull(AuthorTag::parse) + /** + * Fingerprint of a known spam flood — thousands of identical headless-test "Mock Podcast" + * shows. Their structure is exactly `title="Mock Podcast"`, `description="Headless test feed"`, + * `content="Headless test feed"`. Matched so the client can drop them before consuming. + */ + fun isMockSpam(): Boolean = + content == MOCK_SPAM_CONTENT && + title() == MOCK_SPAM_TITLE && + description() == MOCK_SPAM_CONTENT + companion object { const val KIND = 10154 + private const val MOCK_SPAM_TITLE = "Mock Podcast" + private const val MOCK_SPAM_CONTENT = "Headless test feed" + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) fun createAddressATag(pubKey: HexKey) = ATag(KIND, pubKey, FIXED_D_TAG, null) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/PodcastMockSpamTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/PodcastMockSpamTest.kt new file mode 100644 index 0000000000..4013c30977 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipF4Podcasts/PodcastMockSpamTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipF4Podcasts + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** Verifies the fingerprint used to drop the "Mock Podcast" spam flood, and that it spares real shows. */ +class PodcastMockSpamTest { + private fun event( + title: String?, + description: String?, + content: String, + ): PodcastMetadataEvent { + val tags = + buildList { + title?.let { add(arrayOf("title", it)) } + description?.let { add(arrayOf("description", it)) } + }.toTypedArray() + return PodcastMetadataEvent(EMPTY_ID, EMPTY_ID, 0, tags, content, EMPTY_ID) + } + + @Test + fun `the exact mock spam structure matches`() { + assertTrue(event("Mock Podcast", "Headless test feed", "Headless test feed").isMockSpam()) + } + + @Test + fun `real shows are not flagged`() { + // A genuine show that happens to share none of the three fields. + assertFalse(event("My Real Podcast", "A show about things", "Welcome!").isMockSpam()) + // Same title only — not enough. + assertFalse(event("Mock Podcast", "A different description", "Real content").isMockSpam()) + // Same content only — not enough. + assertFalse(event("Another Show", "Another desc", "Headless test feed").isMockSpam()) + // Missing tags entirely. + assertFalse(event(null, null, "Headless test feed").isMockSpam()) + } + + companion object { + private val EMPTY_ID: HexKey = "0".repeat(64) + } +}