kopia lustrzana https://github.com/vitorpamplona/amethyst
93 wiersze
3.3 KiB
Kotlin
93 wiersze
3.3 KiB
Kotlin
package com.vitorpamplona.amethyst.model
|
|
|
|
import android.util.Log
|
|
import android.util.LruCache
|
|
import androidx.compose.runtime.Stable
|
|
import androidx.lifecycle.LiveData
|
|
import com.vitorpamplona.amethyst.OptOutFromFilters
|
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
|
import com.vitorpamplona.quartz.encoders.HexKey
|
|
import com.vitorpamplona.quartz.encoders.Nip19
|
|
import com.vitorpamplona.quartz.events.Event
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>)
|
|
|
|
class AntiSpamFilter {
|
|
val recentMessages = LruCache<Int, String>(1000)
|
|
val spamMessages = LruCache<Int, Spammer>(1000)
|
|
|
|
fun isSpam(event: Event, relay: Relay?): Boolean {
|
|
checkNotInMainThread()
|
|
|
|
if (!OptOutFromFilters.filterSpamFromStrangers) return false
|
|
|
|
val idHex = event.id
|
|
|
|
// if short message, ok
|
|
// The idea here is to avoid considering repeated "GM" messages spam.
|
|
if (event.content.length < 50) return false
|
|
|
|
// if the message is actually short but because it cites a user/event, the nostr: string is really long, make it ok.
|
|
// The idea here is to avoid considering repeated "@Bot, command" messages spam, while still blocking repeated "lnbc..." invoices or fishing urls
|
|
if (event.content.length < 180 && Nip19.nip19regex.matcher(event.content).find()) return false
|
|
|
|
// double list strategy:
|
|
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
|
|
|
|
// Considers tags so that same replies to different people don't count.
|
|
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
|
|
|
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
|
|
Log.w("Potential SPAM Message for sharing", "${Nip19.createNEvent(event.id, event.pubKey, event.kind, null)}")
|
|
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}")
|
|
|
|
// Log down offenders
|
|
logOffender(hash, event)
|
|
|
|
liveSpam.invalidateData()
|
|
|
|
return true
|
|
}
|
|
|
|
recentMessages.put(hash, idHex)
|
|
|
|
return false
|
|
}
|
|
|
|
@Synchronized
|
|
private fun logOffender(hashCode: Int, event: Event) {
|
|
if (spamMessages.get(hashCode) == null) {
|
|
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
|
|
} else {
|
|
val spammer = spamMessages.get(hashCode)
|
|
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
|
|
}
|
|
}
|
|
|
|
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
|
|
}
|
|
|
|
@Stable
|
|
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
|
|
|
// Refreshes observers in batches.
|
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
|
|
|
fun invalidateData() {
|
|
checkNotInMainThread()
|
|
|
|
bundler.invalidate() {
|
|
checkNotInMainThread()
|
|
|
|
if (hasActiveObservers()) {
|
|
postValue(AntiSpamState(cache))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class AntiSpamState(val cache: AntiSpamFilter)
|