mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-11-08 08:01:09 +00:00
Make Mesh Lab receive hooks tolerate device clock skew
dm_recv and msg_recv compared the sender's message timestamp with the receiver's clock at command start, so a receiver whose clock ran ahead ignored messages that had genuinely arrived. Decide "new" by local arrival instead: snapshot the message IDs present when the command starts and match only unseen IDs. Mirrored in the Wear debug driver.
This commit is contained in:
parent
95c0852c2b
commit
0c0b16cfed
@ -0,0 +1,20 @@
|
||||
package com.bitchat.android.testhook
|
||||
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
|
||||
/**
|
||||
* Finds messages that arrived after a receive command started.
|
||||
*
|
||||
* "Arrived" is decided locally: a message is new when its ID was absent from the
|
||||
* snapshot taken at command start. The sender's timestamp is never compared with the
|
||||
* receiver's clock, so receive hooks keep working when the two devices' clocks disagree.
|
||||
*/
|
||||
internal class ArrivedMessageMatcher(
|
||||
alreadyPresent: Iterable<BitchatMessage>,
|
||||
private val accepts: (BitchatMessage) -> Boolean
|
||||
) {
|
||||
private val seenIDs: Set<String> = alreadyPresent.mapTo(HashSet()) { it.id }
|
||||
|
||||
fun firstArrived(messages: Iterable<BitchatMessage>): BitchatMessage? =
|
||||
messages.firstOrNull { it.id !in seenIDs && accepts(it) }
|
||||
}
|
||||
@ -31,6 +31,7 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
@ -247,24 +248,17 @@ object TestHookDriver {
|
||||
val timeoutMs = intent.getLongExtra("timeout_ms", DEFAULT_RECV_TIMEOUT_MS)
|
||||
val fromPeer = intent.getStringExtra("peer")
|
||||
val contains = intent.getStringExtra("contains")
|
||||
val startTime = System.currentTimeMillis()
|
||||
val mesh = mesh(context)
|
||||
val match = withTimeoutOrNull(timeoutMs) {
|
||||
AppStateStore.privateMessages.first { conversations ->
|
||||
conversations.values.flatten().any { msg ->
|
||||
msg.timestamp.time >= startTime &&
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
(fromPeer == null || msg.senderPeerID == fromPeer) &&
|
||||
(contains == null || msg.content.contains(contains))
|
||||
}
|
||||
}
|
||||
} ?: return err("dm_recv", "timeout after ${timeoutMs}ms")
|
||||
val msg = match.values.flatten().first { msg ->
|
||||
msg.timestamp.time >= startTime &&
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
val matcher = ArrivedMessageMatcher(AppStateStore.privateMessages.value.values.flatten()) { msg ->
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
(fromPeer == null || msg.senderPeerID == fromPeer) &&
|
||||
(contains == null || msg.content.contains(contains))
|
||||
}
|
||||
val msg = withTimeoutOrNull(timeoutMs) {
|
||||
AppStateStore.privateMessages
|
||||
.mapNotNull { conversations -> matcher.firstArrived(conversations.values.flatten()) }
|
||||
.first()
|
||||
} ?: return err("dm_recv", "timeout after ${timeoutMs}ms")
|
||||
return ok("dm_recv")
|
||||
.put("from", msg.senderPeerID)
|
||||
.put("sender", msg.sender)
|
||||
@ -276,20 +270,19 @@ object TestHookDriver {
|
||||
val timeoutMs = intent.getLongExtra("timeout_ms", DEFAULT_RECV_TIMEOUT_MS)
|
||||
val contains = intent.getStringExtra("contains")
|
||||
val channel = intent.getStringExtra("channel")
|
||||
val startTime = System.currentTimeMillis()
|
||||
val mesh = mesh(context)
|
||||
val matches: (com.bitchat.android.model.BitchatMessage) -> Boolean = { msg ->
|
||||
msg.timestamp.time >= startTime &&
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
val accepts: (com.bitchat.android.model.BitchatMessage) -> Boolean = { msg ->
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
(contains == null || msg.content.contains(contains)) &&
|
||||
(channel == null || msg.channel == channel)
|
||||
}
|
||||
val found = withTimeoutOrNull(timeoutMs) {
|
||||
if (channel != null) {
|
||||
AppStateStore.channelMessages.first { m -> m.values.flatten().any(matches) }
|
||||
.values.flatten().first(matches)
|
||||
val matcher = ArrivedMessageMatcher(AppStateStore.channelMessages.value.values.flatten(), accepts)
|
||||
AppStateStore.channelMessages.mapNotNull { m -> matcher.firstArrived(m.values.flatten()) }.first()
|
||||
} else {
|
||||
AppStateStore.publicMessages.first { l -> l.any(matches) }.first(matches)
|
||||
val matcher = ArrivedMessageMatcher(AppStateStore.publicMessages.value, accepts)
|
||||
AppStateStore.publicMessages.mapNotNull(matcher::firstArrived).first()
|
||||
}
|
||||
} ?: return err("msg_recv", "timeout after ${timeoutMs}ms")
|
||||
return ok("msg_recv")
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
package com.bitchat.android.testhook
|
||||
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
class ArrivedMessageMatcherTest {
|
||||
|
||||
private val receiverClockAtStart = 1_700_000_000_000L
|
||||
|
||||
private fun message(id: String, content: String, timestampMs: Long) = BitchatMessage(
|
||||
id = id,
|
||||
sender = "peer",
|
||||
content = content,
|
||||
timestamp = Date(timestampMs),
|
||||
senderPeerID = "0102030405060708"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun messageStampedBySlowerSenderClockStillCountsAsArrived() {
|
||||
val matcher = ArrivedMessageMatcher(emptyList()) { it.content.contains("token") }
|
||||
val behindReceiver = message("new", "hello token", receiverClockAtStart - 140_000)
|
||||
|
||||
assertEquals(behindReceiver, matcher.firstArrived(listOf(behindReceiver)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messagePresentAtStartIsIgnoredEvenWhenSenderClockIsAhead() {
|
||||
val preexisting = message("old", "hello token", receiverClockAtStart + 140_000)
|
||||
val matcher = ArrivedMessageMatcher(listOf(preexisting)) { it.content.contains("token") }
|
||||
|
||||
assertNull(matcher.firstArrived(listOf(preexisting)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun arrivedMessageMustStillPassFilter() {
|
||||
val matcher = ArrivedMessageMatcher(emptyList()) { it.content.contains("token") }
|
||||
val unrelated = message("a", "other", receiverClockAtStart)
|
||||
val wanted = message("b", "hello token", receiverClockAtStart)
|
||||
|
||||
assertEquals(wanted, matcher.firstArrived(listOf(unrelated, wanted)))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.bitchat.watch.testhook
|
||||
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
|
||||
/**
|
||||
* Finds messages that arrived after a receive command started.
|
||||
*
|
||||
* "Arrived" is decided locally: a message is new when its ID was absent from the
|
||||
* snapshot taken at command start. The sender's timestamp is never compared with the
|
||||
* receiver's clock, so receive hooks keep working when the two devices' clocks disagree.
|
||||
*/
|
||||
internal class ArrivedMessageMatcher(
|
||||
alreadyPresent: Iterable<BitchatMessage>,
|
||||
private val accepts: (BitchatMessage) -> Boolean
|
||||
) {
|
||||
private val seenIDs: Set<String> = alreadyPresent.mapTo(HashSet()) { it.id }
|
||||
|
||||
fun firstArrived(messages: Iterable<BitchatMessage>): BitchatMessage? =
|
||||
messages.firstOrNull { it.id !in seenIDs && accepts(it) }
|
||||
}
|
||||
@ -20,6 +20,7 @@ import com.bitchat.watch.service.WearMeshForegroundService
|
||||
import com.bitchat.watch.ui.WearPeerIdentityState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
@ -229,24 +230,17 @@ object WearTestHookDriver {
|
||||
val timeoutMs = intent.getLongExtra("timeout_ms", DEFAULT_RECV_TIMEOUT_MS)
|
||||
val fromPeer = intent.getStringExtra("peer")
|
||||
val contains = intent.getStringExtra("contains")
|
||||
val startTime = System.currentTimeMillis()
|
||||
val mesh = mesh(context)
|
||||
val match = withTimeoutOrNull(timeoutMs) {
|
||||
AppStateStore.privateMessages.first { conversations ->
|
||||
conversations.values.flatten().any { msg ->
|
||||
msg.timestamp.time >= startTime &&
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
(fromPeer == null || msg.senderPeerID == fromPeer) &&
|
||||
(contains == null || msg.content.contains(contains))
|
||||
}
|
||||
}
|
||||
} ?: return err("dm_recv", "timeout after ${timeoutMs}ms")
|
||||
val msg = match.values.flatten().first { msg ->
|
||||
msg.timestamp.time >= startTime &&
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
val matcher = ArrivedMessageMatcher(AppStateStore.privateMessages.value.values.flatten()) { msg ->
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
(fromPeer == null || msg.senderPeerID == fromPeer) &&
|
||||
(contains == null || msg.content.contains(contains))
|
||||
}
|
||||
val msg = withTimeoutOrNull(timeoutMs) {
|
||||
AppStateStore.privateMessages
|
||||
.mapNotNull { conversations -> matcher.firstArrived(conversations.values.flatten()) }
|
||||
.first()
|
||||
} ?: return err("dm_recv", "timeout after ${timeoutMs}ms")
|
||||
return ok("dm_recv")
|
||||
.put("from", msg.senderPeerID)
|
||||
.put("sender", msg.sender)
|
||||
@ -257,15 +251,13 @@ object WearTestHookDriver {
|
||||
private suspend fun msgRecv(context: Context, intent: Intent): JSONObject {
|
||||
val timeoutMs = intent.getLongExtra("timeout_ms", DEFAULT_RECV_TIMEOUT_MS)
|
||||
val contains = intent.getStringExtra("contains")
|
||||
val startTime = System.currentTimeMillis()
|
||||
val mesh = mesh(context)
|
||||
val matches: (com.bitchat.android.model.BitchatMessage) -> Boolean = { msg ->
|
||||
msg.timestamp.time >= startTime &&
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
val matcher = ArrivedMessageMatcher(AppStateStore.publicMessages.value) { msg ->
|
||||
msg.senderPeerID != mesh.myPeerID &&
|
||||
(contains == null || msg.content.contains(contains))
|
||||
}
|
||||
val found = withTimeoutOrNull(timeoutMs) {
|
||||
AppStateStore.publicMessages.first { l -> l.any(matches) }.first(matches)
|
||||
AppStateStore.publicMessages.mapNotNull(matcher::firstArrived).first()
|
||||
} ?: return err("msg_recv", "timeout after ${timeoutMs}ms")
|
||||
return ok("msg_recv")
|
||||
.put("from", found.senderPeerID)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user