fix: exclude client tag from note text search matching

Searching for an app name (e.g. "Amethyst") was returning every event
published through that client, because the local-cache note search
matched the search term against all tag values including the NIP-89
["client", ...] tag. Skip the client tag when matching tag values in
findNotesStartingWith.

https://claude.ai/code/session_01YMs6aXuvs5NaYjzyPH6Zqj
This commit is contained in:
Claude
2026-06-12 15:07:13 +00:00
parent b37e62cd62
commit 7f39a18a02
3 changed files with 64 additions and 4 deletions
@@ -252,6 +252,7 @@ import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.ClientTag
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
@@ -2380,6 +2381,13 @@ object LocalCache : ILocalCache, ICacheProvider {
note.event is AppSpecificDataEvent
)
/**
* Tag names whose values should not match text searches. The `client` tag
* names the app that published the event, so searching for "Amethyst"
* would otherwise return every event posted through Amethyst.
*/
private val excludedTagNamesFromSearch = setOf(ClientTag.TAG_NAME)
fun findNotesStartingWith(
text: String,
hiddenUsers: HiddenUsersState,
@@ -2419,7 +2427,7 @@ object LocalCache : ILocalCache, ICacheProvider {
return@filter false
}
if (note.event?.tags?.tagValueContains(text, true) == true ||
if (note.event?.tags?.tagValueContains(text, true, excludedTagNamesFromSearch) == true ||
note.idHex.startsWith(text, true)
) {
return@filter !note.isHiddenFor(hiddenUsers.flow.value)
@@ -2440,7 +2448,7 @@ object LocalCache : ILocalCache, ICacheProvider {
return@filter false
}
if (addressable.event?.tags?.tagValueContains(text, true) == true ||
if (addressable.event?.tags?.tagValueContains(text, true, excludedTagNamesFromSearch) == true ||
addressable.idHex.startsWith(text, true)
) {
return@filter !addressable.isHiddenFor(hiddenUsers.flow.value)
@@ -171,12 +171,14 @@ fun TagArray.isAnyLowercaseTagged(
) = this.fastAny { it.size > 1 && it[0] == tagName && it[1].lowercase() in tagValues }
/**
* Returns `true` if at least one tag has value that contains [text]
* Returns `true` if at least one tag has value that contains [text].
* Tags whose name is in [exceptNames] are skipped.
*/
fun TagArray.tagValueContains(
text: String,
ignoreCase: Boolean = false,
) = this.fastAny { it.size > 1 && it[1].contains(text, ignoreCase) }
exceptNames: Set<String> = emptySet(),
) = this.fastAny { it.size > 1 && it[0] !in exceptNames && it[1].contains(text, ignoreCase) }
fun TagArray.containsAllTagNamesWithValues(names: Set<String>): Boolean {
val remaining = names.toMutableSet()
@@ -0,0 +1,50 @@
/*
* 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.nip01Core.core
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.ClientTag
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class TagArrayTest {
private val tags: TagArray =
arrayOf(
arrayOf("d", "30022"),
arrayOf("a", "31990:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:1685802317447", "wss://nostr.wine/", "android"),
arrayOf("alt", "App recommendations by the author"),
arrayOf("client", "Amethyst"),
)
@Test
fun matchesTagValues() {
assertTrue(tags.tagValueContains("recommendations", ignoreCase = true))
assertTrue(tags.tagValueContains("amethyst", ignoreCase = true))
assertFalse(tags.tagValueContains("missing", ignoreCase = true))
}
@Test
fun skipsExcludedTagNames() {
assertFalse(tags.tagValueContains("amethyst", ignoreCase = true, exceptNames = setOf(ClientTag.TAG_NAME)))
// other tags still match when the client tag is excluded
assertTrue(tags.tagValueContains("recommendations", ignoreCase = true, exceptNames = setOf(ClientTag.TAG_NAME)))
}
}