mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-28 21:53:01 +02:00
Only adds user to Hidden Users after 5 duplicated messages.
This commit is contained in:
@@ -420,13 +420,13 @@ class Account(
|
|||||||
reconnectIfRelaysHaveChanged()
|
reconnectIfRelaysHaveChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalCache.liveSpam.observeForever {
|
LocalCache.antiSpam.liveSpam.observeForever {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
LocalCache.spamMessages.snapshot().values.forEach {
|
it.cache.spamMessages.snapshot().values.forEach {
|
||||||
if (it !in hiddenUsers) {
|
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages > 5) {
|
||||||
val userToBlock = LocalCache.getOrCreateUser(it)
|
val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex)
|
||||||
if (userToBlock != userProfile() && userToBlock !in userProfile().follows) {
|
if (userToBlock != userProfile() && userToBlock !in userProfile().follows) {
|
||||||
transientHiddenUsers = transientHiddenUsers + it
|
transientHiddenUsers = transientHiddenUsers + it.pubkeyHex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,92 @@
|
|||||||
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.LruCache
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import nostr.postr.events.Event
|
||||||
|
import nostr.postr.toHex
|
||||||
|
|
||||||
|
class AntiSpamFilter {
|
||||||
|
val recentMessages = LruCache<Int, String>(1000)
|
||||||
|
val spamMessages = LruCache<Int, Spammer>(1000)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun isSpam(event: Event): Boolean {
|
||||||
|
val idHex = event.id.toHexKey()
|
||||||
|
|
||||||
|
// if already processed, ok
|
||||||
|
if (LocalCache.notes[idHex] != null) return false
|
||||||
|
|
||||||
|
// if short message, ok
|
||||||
|
if (event.content.length < 50) 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", "${event.id.toHex()} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
|
||||||
|
|
||||||
|
// Log down offenders
|
||||||
|
if (spamMessages.get(hash) == null) {
|
||||||
|
spamMessages.put(hash, Spammer(event.pubKey.toHexKey(), 2))
|
||||||
|
liveSpam.invalidateData()
|
||||||
|
} else {
|
||||||
|
spamMessages.get(hash).duplicatedMessages++
|
||||||
|
liveSpam.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
recentMessages.put(hash, idHex)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AntiSpamLiveData(val cache: AntiSpamFilter): LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||||
|
|
||||||
|
// Refreshes observers in batches.
|
||||||
|
var handlerWaiting = AtomicBoolean()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun invalidateData() {
|
||||||
|
if (!hasActiveObservers()) return
|
||||||
|
if (handlerWaiting.getAndSet(true)) return
|
||||||
|
|
||||||
|
handlerWaiting.set(true)
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
delay(100)
|
||||||
|
refresh()
|
||||||
|
} finally {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
handlerWaiting.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
postValue(AntiSpamState(cache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AntiSpamState(val cache: AntiSpamFilter) {
|
||||||
|
|
||||||
|
}
|
@@ -41,21 +41,22 @@ import nostr.postr.events.PrivateDmEvent
|
|||||||
import nostr.postr.events.RecommendRelayEvent
|
import nostr.postr.events.RecommendRelayEvent
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
import nostr.postr.toNpub
|
||||||
|
|
||||||
|
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Int)
|
||||||
|
|
||||||
object LocalCache {
|
object LocalCache {
|
||||||
val metadataParser = jacksonObjectMapper()
|
val metadataParser = jacksonObjectMapper()
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
.readerFor(UserMetadata::class.java)
|
.readerFor(UserMetadata::class.java)
|
||||||
|
|
||||||
|
val antiSpam = AntiSpamFilter()
|
||||||
|
|
||||||
val users = ConcurrentHashMap<HexKey, User>()
|
val users = ConcurrentHashMap<HexKey, User>()
|
||||||
val notes = ConcurrentHashMap<HexKey, Note>()
|
val notes = ConcurrentHashMap<HexKey, Note>()
|
||||||
val channels = ConcurrentHashMap<HexKey, Channel>()
|
val channels = ConcurrentHashMap<HexKey, Channel>()
|
||||||
|
|
||||||
val recentMessages = LruCache<Int, String>(1000)
|
fun checkGetOrCreateUser(key: String): User? {
|
||||||
val spamMessages = LruCache<Int, String>(1000)
|
|
||||||
|
|
||||||
fun checkGetOrCreateUser(key: HexKey): User? {
|
|
||||||
return try {
|
return try {
|
||||||
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
|
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
|
||||||
getOrCreateUser(key)
|
getOrCreateUser(key)
|
||||||
@@ -74,7 +75,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkGetOrCreateNote(key: HexKey): Note? {
|
fun checkGetOrCreateNote(key: String): Note? {
|
||||||
return try {
|
return try {
|
||||||
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
|
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
|
||||||
getOrCreateNote(key)
|
getOrCreateNote(key)
|
||||||
@@ -93,7 +94,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkGetOrCreateChannel(key: HexKey): Channel? {
|
fun checkGetOrCreateChannel(key: String): Channel? {
|
||||||
return try {
|
return try {
|
||||||
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
|
val checkHex = Hex.decode(key) // Checks if this is a valid Hex
|
||||||
getOrCreateChannel(key)
|
getOrCreateChannel(key)
|
||||||
@@ -141,29 +142,9 @@ object LocalCache {
|
|||||||
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun isRepeatedMessageSpam(event: Event): Boolean {
|
|
||||||
val idHex = event.id.toHexKey()
|
|
||||||
// if already processed, return
|
|
||||||
if (notes[idHex] != null) return false
|
|
||||||
|
|
||||||
// double list strategy:
|
|
||||||
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
|
|
||||||
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
|
||||||
if (event.content.length > 50 && ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null)) {
|
|
||||||
Log.w("Potential SPAM Message", "${event.id.toHex()} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
|
|
||||||
if (spamMessages.get(hash) == null) {
|
|
||||||
spamMessages.put(hash, event.pubKey.toHexKey())
|
|
||||||
liveSpam.invalidateData()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
recentMessages.put(hash, idHex)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun consume(event: TextNoteEvent, relay: Relay? = null) {
|
fun consume(event: TextNoteEvent, relay: Relay? = null) {
|
||||||
if (isRepeatedMessageSpam(event)) return
|
if (antiSpam.isSpam(event)) return
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
@@ -450,7 +431,7 @@ object LocalCache {
|
|||||||
|
|
||||||
fun consume(event: ChannelMessageEvent, relay: Relay?) {
|
fun consume(event: ChannelMessageEvent, relay: Relay?) {
|
||||||
if (event.channel.isNullOrBlank()) return
|
if (event.channel.isNullOrBlank()) return
|
||||||
if (isRepeatedMessageSpam(event)) return
|
if (antiSpam.isSpam(event)) return
|
||||||
|
|
||||||
val channel = checkGetOrCreateChannel(event.channel) ?: return
|
val channel = checkGetOrCreateChannel(event.channel) ?: return
|
||||||
|
|
||||||
@@ -667,7 +648,6 @@ object LocalCache {
|
|||||||
|
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val live: LocalCacheLiveData = LocalCacheLiveData(this)
|
val live: LocalCacheLiveData = LocalCacheLiveData(this)
|
||||||
val liveSpam: LocalCacheLiveData = LocalCacheLiveData(this)
|
|
||||||
|
|
||||||
private fun refreshObservers() {
|
private fun refreshObservers() {
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
|
Reference in New Issue
Block a user