Ignores duplicated hashtags in different char cases when processing hashtag spam

This commit is contained in:
Vitor Pamplona
2026-05-16 11:34:11 -04:00
parent 3bf3157b17
commit 32b9a06612
5 changed files with 34 additions and 4 deletions
@@ -158,7 +158,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasMoreHashtagsThan
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
@@ -3035,7 +3035,7 @@ class Account(
private fun hasExcessiveHashtags(note: Note): Boolean { private fun hasExcessiveHashtags(note: Note): Boolean {
val limit = settings.syncedSettings.security.maxHashtagLimit.value val limit = settings.syncedSettings.security.maxHashtagLimit.value
return limit > 0 && (note.event?.countHashtags() ?: 0) > limit return limit > 0 && note.event?.hasMoreHashtagsThan(limit) == true
} }
override fun isAcceptable(note: Note): Boolean { override fun isAcceptable(note: Note): Boolean {
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasMoreHashtagsThan
import com.vitorpamplona.quartz.nip10Notes.threadRootIdOrSelf import com.vitorpamplona.quartz.nip10Notes.threadRootIdOrSelf
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
@@ -51,7 +51,7 @@ class FilterByListParams(
fun isNotInTheFuture(noteEvent: Event) = noteEvent.createdAt <= now fun isNotInTheFuture(noteEvent: Event) = noteEvent.createdAt <= now
fun hasExcessiveHashtags(noteEvent: Event) = hiddenLists.maxHashtagLimit > 0 && noteEvent.countHashtags() > hiddenLists.maxHashtagLimit fun hasExcessiveHashtags(noteEvent: Event) = hiddenLists.maxHashtagLimit > 0 && noteEvent.hasMoreHashtagsThan(hiddenLists.maxHashtagLimit)
fun isAuthorInFollows(author: HexKey): Boolean { fun isAuthorInFollows(author: HexKey): Boolean {
if (followLists == null) return false if (followLists == null) return false
@@ -26,6 +26,8 @@ fun Event.anyHashTag(onEach: (str: String) -> Boolean) = tags.anyHashTag(onEach)
fun Event.countHashtags() = tags.countHashtags() fun Event.countHashtags() = tags.countHashtags()
fun Event.hasMoreHashtagsThan(limit: Int) = tags.hasMoreHashtagsThan(limit)
fun Event.hasHashtags() = tags.hasHashtags() fun Event.hasHashtags() = tags.hasHashtags()
fun Event.hashtags() = tags.hashtags() fun Event.hashtags() = tags.hashtags()
@@ -46,6 +46,13 @@ class HashtagTag {
return tag[1] return tag[1]
} }
fun parseLowercase(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1].lowercase()
}
fun assemble(name: String) = arrayOf(TAG_NAME, name) fun assemble(name: String) = arrayOf(TAG_NAME, name)
fun assembleDualCase(name: String): List<Array<String>> { fun assembleDualCase(name: String): List<Array<String>> {
@@ -38,8 +38,29 @@ fun TagArray.hashtags() = this.mapNotNull(HashtagTag::parse)
fun TagArray.countHashtags() = this.count(HashtagTag::isTagged) fun TagArray.countHashtags() = this.count(HashtagTag::isTagged)
fun TagArray.hasMoreHashtagsThan(limit: Int): Boolean {
val count = this.count(HashtagTag::isTagged)
return count > limit && this.countUnique(count, HashtagTag::parseLowercase) > limit
}
fun TagArray.isTaggedHash(hashtag: String) = this.isTagged(HashtagTag.TAG_NAME, hashtag, true) fun TagArray.isTaggedHash(hashtag: String) = this.isTagged(HashtagTag.TAG_NAME, hashtag, true)
fun TagArray.isTaggedHashes(hashtags: Set<String>) = this.isAnyLowercaseTagged(HashtagTag.TAG_NAME, hashtags) fun TagArray.isTaggedHashes(hashtags: Set<String>) = this.isAnyLowercaseTagged(HashtagTag.TAG_NAME, hashtags)
fun TagArray.firstIsTaggedHashes(hashtags: Set<String>) = this.firstAnyLowercaseTaggedValue(HashtagTag.TAG_NAME, hashtags) fun TagArray.firstIsTaggedHashes(hashtags: Set<String>) = this.firstAnyLowercaseTaggedValue(HashtagTag.TAG_NAME, hashtags)
public inline fun <T, U> Array<out T>.countUnique(
size: Int,
transform: (T) -> U?,
): Int {
// Pre-allocate hash set to avoid frequent resizing for larger arrays
val seen = HashSet<U>(size)
var count = 0
for (element in this) {
val value = transform(element)
if (value != null && seen.add(value)) {
++count
}
}
return count
}