mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: word-based message filtering with quarantine approach (stored but hidden) (#4241)
This commit is contained in:
parent
ae65e64a37
commit
c0f8ed3503
32 changed files with 2187 additions and 115 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.first
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
@ -342,6 +343,129 @@ class PacketDaoTest {
|
|||
assertTrue(updated.sfpp_hash?.contentEquals(hash) == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_filteredMessages_excludedFromContactKeys(): Unit = runBlocking {
|
||||
// Create a new contact with only filtered messages
|
||||
val filteredContactKey = "0!filteredonly"
|
||||
|
||||
val filteredPacket =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
contact_key = filteredContactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, "Filtered message"),
|
||||
filtered = true,
|
||||
)
|
||||
packetDao.insert(filteredPacket)
|
||||
|
||||
// getContactKeys should not include contacts with only filtered messages
|
||||
val contactKeys = packetDao.getContactKeys().first()
|
||||
assertFalse(contactKeys.containsKey(filteredContactKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getFilteredCount_returnsCorrectCount(): Unit = runBlocking {
|
||||
val contactKey = "0${DataPacket.ID_BROADCAST}"
|
||||
|
||||
// Insert filtered messages
|
||||
repeat(3) { i ->
|
||||
val filteredPacket =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + i,
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, "Filtered $i"),
|
||||
filtered = true,
|
||||
)
|
||||
packetDao.insert(filteredPacket)
|
||||
}
|
||||
|
||||
val filteredCount = packetDao.getFilteredCount(contactKey)
|
||||
assertEquals(3, filteredCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_contactFilteringDisabled_persistence(): Unit = runBlocking {
|
||||
val contactKey = "0!testcontact"
|
||||
|
||||
// Initially should be null or false
|
||||
val initial = packetDao.getContactFilteringDisabled(contactKey)
|
||||
assertTrue(initial == null || initial == false)
|
||||
|
||||
// Set filtering disabled
|
||||
packetDao.setContactFilteringDisabled(contactKey, true)
|
||||
|
||||
val disabled = packetDao.getContactFilteringDisabled(contactKey)
|
||||
assertEquals(true, disabled)
|
||||
|
||||
// Re-enable filtering
|
||||
packetDao.setContactFilteringDisabled(contactKey, false)
|
||||
|
||||
val enabled = packetDao.getContactFilteringDisabled(contactKey)
|
||||
assertEquals(false, enabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getMessagesFrom_excludesFilteredMessages(): Unit = runBlocking {
|
||||
val contactKey = "0!notificationtest"
|
||||
|
||||
// Insert mix of filtered and non-filtered messages
|
||||
val normalMessages = listOf("Hello", "How are you?", "Good morning")
|
||||
val filteredMessages = listOf("Filtered message 1", "Filtered message 2")
|
||||
|
||||
normalMessages.forEachIndexed { index, text ->
|
||||
val packet =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + index,
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, text),
|
||||
filtered = false,
|
||||
)
|
||||
packetDao.insert(packet)
|
||||
}
|
||||
|
||||
filteredMessages.forEachIndexed { index, text ->
|
||||
val packet =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + normalMessages.size + index,
|
||||
read = true, // Filtered messages are marked as read
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, text),
|
||||
filtered = true,
|
||||
)
|
||||
packetDao.insert(packet)
|
||||
}
|
||||
|
||||
// Without filter - should return all messages
|
||||
val allMessages = packetDao.getMessagesFrom(contactKey).first()
|
||||
assertEquals(normalMessages.size + filteredMessages.size, allMessages.size)
|
||||
|
||||
// With includeFiltered = true - should return all messages
|
||||
val includingFiltered = packetDao.getMessagesFrom(contactKey, includeFiltered = true).first()
|
||||
assertEquals(normalMessages.size + filteredMessages.size, includingFiltered.size)
|
||||
|
||||
// With includeFiltered = false - should only return non-filtered messages
|
||||
val excludingFiltered = packetDao.getMessagesFrom(contactKey, includeFiltered = false).first()
|
||||
assertEquals(normalMessages.size, excludingFiltered.size)
|
||||
|
||||
// Verify none of the returned messages are filtered
|
||||
val hasFilteredMessages = excludingFiltered.any { it.packet.filtered }
|
||||
assertFalse(hasFilteredMessages)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SAMPLE_SIZE = 10
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,9 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
|
|||
AutoMigration(from = 29, to = 30, spec = AutoMigration29to30::class),
|
||||
AutoMigration(from = 30, to = 31),
|
||||
AutoMigration(from = 31, to = 32),
|
||||
AutoMigration(from = 32, to = 33),
|
||||
],
|
||||
version = 32,
|
||||
version = 33,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ interface PacketDao {
|
|||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1
|
||||
AND port_num = 1 AND filtered = 0
|
||||
ORDER BY received_time DESC
|
||||
""",
|
||||
)
|
||||
|
|
@ -69,11 +69,11 @@ interface PacketDao {
|
|||
SELECT contact_key, MAX(received_time) as max_time
|
||||
FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1
|
||||
AND port_num = 1 AND filtered = 0
|
||||
GROUP BY contact_key
|
||||
) latest ON p.contact_key = latest.contact_key AND p.received_time = latest.max_time
|
||||
WHERE (p.myNodeNum = 0 OR p.myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND p.port_num = 1
|
||||
AND p.port_num = 1 AND p.filtered = 0
|
||||
ORDER BY p.received_time DESC
|
||||
""",
|
||||
)
|
||||
|
|
@ -161,6 +161,18 @@ interface PacketDao {
|
|||
)
|
||||
fun getMessagesFrom(contact: String, limit: Int): Flow<List<PacketEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact
|
||||
AND (filtered = 0 OR :includeFiltered = 1)
|
||||
ORDER BY received_time DESC
|
||||
""",
|
||||
)
|
||||
fun getMessagesFrom(contact: String, includeFiltered: Boolean): Flow<List<PacketEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
|
|
@ -376,6 +388,47 @@ interface PacketDao {
|
|||
)
|
||||
suspend fun findReactionBySfppHash(hash: ByteArray): ReactionEntity?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact AND filtered = 1
|
||||
""",
|
||||
)
|
||||
suspend fun getFilteredCount(contact: String): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact AND filtered = 1
|
||||
""",
|
||||
)
|
||||
fun getFilteredCountFlow(contact: String): Flow<Int>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact
|
||||
AND (filtered = 0 OR :includeFiltered = 1)
|
||||
ORDER BY received_time DESC
|
||||
""",
|
||||
)
|
||||
fun getMessagesFromPaged(contact: String, includeFiltered: Boolean): PagingSource<Int, PacketEntity>
|
||||
|
||||
@Query("SELECT filtering_disabled FROM contact_settings WHERE contact_key = :contact")
|
||||
suspend fun getContactFilteringDisabled(contact: String): Boolean?
|
||||
|
||||
@Transaction
|
||||
suspend fun setContactFilteringDisabled(contact: String, disabled: Boolean) {
|
||||
val settings =
|
||||
getContactSettings(contact)?.copy(filteringDisabled = disabled)
|
||||
?: ContactSettings(contact_key = contact, filteringDisabled = disabled)
|
||||
upsertContactSettings(listOf(settings))
|
||||
}
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteAll() {
|
||||
deleteAllPackets()
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ data class PacketEntity(
|
|||
relayNode = data.relayNode,
|
||||
relays = data.relays,
|
||||
retryCount = data.retryCount,
|
||||
filtered = filtered,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +88,7 @@ data class Packet(
|
|||
@ColumnInfo(name = "rssi", defaultValue = "0") val rssi: Int = 0,
|
||||
@ColumnInfo(name = "hopsAway", defaultValue = "-1") val hopsAway: Int = -1,
|
||||
@ColumnInfo(name = "sfpp_hash") val sfpp_hash: ByteArray? = null,
|
||||
@ColumnInfo(name = "filtered", defaultValue = "0") val filtered: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
const val RELAY_NODE_SUFFIX_MASK = 0xFF
|
||||
|
|
@ -120,6 +122,7 @@ data class ContactSettings(
|
|||
val muteUntil: Long = 0L,
|
||||
@ColumnInfo(name = "last_read_message_uuid") val lastReadMessageUuid: Long? = null,
|
||||
@ColumnInfo(name = "last_read_message_timestamp") val lastReadMessageTimestamp: Long? = null,
|
||||
@ColumnInfo(name = "filtering_disabled", defaultValue = "0") val filteringDisabled: Boolean = false,
|
||||
) {
|
||||
val isMuted
|
||||
get() = System.currentTimeMillis() <= muteUntil
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ data class Message(
|
|||
val relayNode: Int? = null,
|
||||
val relays: Int = 0,
|
||||
val retryCount: Int = 0,
|
||||
val filtered: Boolean = false,
|
||||
) {
|
||||
fun getStatusStringRes(): Pair<StringResource, StringResource> {
|
||||
val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue