mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge ec9d4abcaa7030b07edfad9ac8086aeddd7b5350 into 094657efa0aabbb6f71c9050149d1d01aee96400
This commit is contained in:
commit
389dd75c43
@ -12,7 +12,9 @@ import com.bitchat.android.sync.SyncDefaults
|
||||
data class RequestSyncPacket(
|
||||
val p: Int,
|
||||
val m: Long,
|
||||
val data: ByteArray
|
||||
val data: ByteArray,
|
||||
val wantedTypes: List<UByte>? = null,
|
||||
val minTimestamp: ULong? = null
|
||||
) {
|
||||
fun encode(): ByteArray {
|
||||
val out = ArrayList<Byte>()
|
||||
@ -38,6 +40,28 @@ data class RequestSyncPacket(
|
||||
)
|
||||
// data
|
||||
putTLV(0x03, data)
|
||||
// wantedTypes
|
||||
wantedTypes?.let { types ->
|
||||
if (types.isNotEmpty()) {
|
||||
val typesBytes = types.map { it.toByte() }.toByteArray()
|
||||
putTLV(0x04, typesBytes)
|
||||
}
|
||||
}
|
||||
// minTimestamp
|
||||
minTimestamp?.let { ts ->
|
||||
val tsLong = ts.toLong()
|
||||
val tsBytes = byteArrayOf(
|
||||
((tsLong ushr 56) and 0xFF).toByte(),
|
||||
((tsLong ushr 48) and 0xFF).toByte(),
|
||||
((tsLong ushr 40) and 0xFF).toByte(),
|
||||
((tsLong ushr 32) and 0xFF).toByte(),
|
||||
((tsLong ushr 24) and 0xFF).toByte(),
|
||||
((tsLong ushr 16) and 0xFF).toByte(),
|
||||
((tsLong ushr 8) and 0xFF).toByte(),
|
||||
(tsLong and 0xFF).toByte()
|
||||
)
|
||||
putTLV(0x05, tsBytes)
|
||||
}
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
@ -50,6 +74,8 @@ data class RequestSyncPacket(
|
||||
var p: Int? = null
|
||||
var m: Long? = null
|
||||
var payload: ByteArray? = null
|
||||
var wantedTypes: List<UByte>? = null
|
||||
var minTimestamp: ULong? = null
|
||||
|
||||
while (off + 3 <= data.size) {
|
||||
val t = (data[off].toInt() and 0xFF); off += 1
|
||||
@ -69,6 +95,22 @@ data class RequestSyncPacket(
|
||||
if (v.size > MAX_ACCEPT_FILTER_BYTES) return null
|
||||
payload = v
|
||||
}
|
||||
0x04 -> {
|
||||
val typesList = mutableListOf<UByte>()
|
||||
for (b in v) {
|
||||
typesList.add(b.toUByte())
|
||||
}
|
||||
wantedTypes = typesList
|
||||
}
|
||||
0x05 -> {
|
||||
if (len == 8) {
|
||||
var tsVal = 0L
|
||||
for (i in 0 until 8) {
|
||||
tsVal = (tsVal shl 8) or (v[i].toLong() and 0xFF)
|
||||
}
|
||||
minTimestamp = tsVal.toULong()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +118,7 @@ data class RequestSyncPacket(
|
||||
val mm = m ?: return null
|
||||
val dd = payload ?: return null
|
||||
if (pp < 1 || mm <= 0L) return null
|
||||
return RequestSyncPacket(pp, mm, dd)
|
||||
return RequestSyncPacket(pp, mm, dd, wantedTypes, minTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,8 +142,8 @@ class GossipSyncManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendRequestSync() {
|
||||
val payload = buildGcsPayload()
|
||||
fun sendRequestSync(wantedTypes: List<UByte>? = null, minTimestamp: ULong? = null) {
|
||||
val payload = buildGcsPayload(wantedTypes, minTimestamp)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.REQUEST_SYNC.value,
|
||||
@ -157,8 +157,8 @@ class GossipSyncManager(
|
||||
delegate?.sendPacket(signed)
|
||||
}
|
||||
|
||||
private fun sendRequestSyncToPeer(peerID: String) {
|
||||
val payload = buildGcsPayload()
|
||||
fun sendRequestSyncToPeer(peerID: String, wantedTypes: List<UByte>? = null, minTimestamp: ULong? = null) {
|
||||
val payload = buildGcsPayload(wantedTypes, minTimestamp)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.REQUEST_SYNC.value,
|
||||
@ -183,26 +183,40 @@ class GossipSyncManager(
|
||||
return GCSFilter.contains(sorted, nonZeroV)
|
||||
}
|
||||
|
||||
// Determine types to include (default: ANNOUNCE and MESSAGE if wantedTypes is null or empty)
|
||||
val targetTypes = if (request.wantedTypes == null || request.wantedTypes.isEmpty()) {
|
||||
listOf(MessageType.ANNOUNCE.value, MessageType.MESSAGE.value)
|
||||
} else {
|
||||
request.wantedTypes
|
||||
}
|
||||
val minTs = request.minTimestamp ?: 0uL
|
||||
|
||||
// 1) Announcements: send latest per peerID if remote doesn't have them
|
||||
for ((_, pair) in latestAnnouncementByPeer.entries) {
|
||||
val (id, pkt) = pair
|
||||
val idBytes = hexToBytes(id)
|
||||
if (!mightContain(idBytes)) {
|
||||
// Send original packet unchanged to requester only (keep local TTL)
|
||||
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
|
||||
delegate?.sendPacketToPeer(fromPeerID, toSend)
|
||||
Log.d(TAG, "Sent sync announce: Type ${toSend.type} from ${toSend.senderID.toHexString()} to $fromPeerID packet id ${idBytes.toHexString()}")
|
||||
if (targetTypes.contains(MessageType.ANNOUNCE.value)) {
|
||||
for ((_, pair) in latestAnnouncementByPeer.entries) {
|
||||
val (id, pkt) = pair
|
||||
if (pkt.timestamp < minTs) continue
|
||||
val idBytes = hexToBytes(id)
|
||||
if (!mightContain(idBytes)) {
|
||||
// Send original packet unchanged to requester only (keep local TTL)
|
||||
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
|
||||
delegate?.sendPacketToPeer(fromPeerID, toSend)
|
||||
Log.d(TAG, "Sent sync announce: Type ${toSend.type} from ${toSend.senderID.toHexString()} to $fromPeerID packet id ${idBytes.toHexString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Broadcast messages: send all they lack
|
||||
val toSendMsgs = synchronized(messages) { messages.values.toList() }
|
||||
for (pkt in toSendMsgs) {
|
||||
val idBytes = PacketIdUtil.computeIdBytes(pkt)
|
||||
if (!mightContain(idBytes)) {
|
||||
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
|
||||
delegate?.sendPacketToPeer(fromPeerID, toSend)
|
||||
Log.d(TAG, "Sent sync message: Type ${toSend.type} to $fromPeerID packet id ${idBytes.toHexString()}")
|
||||
if (targetTypes.contains(MessageType.MESSAGE.value)) {
|
||||
val toSendMsgs = synchronized(messages) { messages.values.toList() }
|
||||
for (pkt in toSendMsgs) {
|
||||
if (pkt.timestamp < minTs) continue
|
||||
val idBytes = PacketIdUtil.computeIdBytes(pkt)
|
||||
if (!mightContain(idBytes)) {
|
||||
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
|
||||
delegate?.sendPacketToPeer(fromPeerID, toSend)
|
||||
Log.d(TAG, "Sent sync message: Type ${toSend.type} to $fromPeerID packet id ${idBytes.toHexString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,16 +246,35 @@ class GossipSyncManager(
|
||||
return out
|
||||
}
|
||||
|
||||
private fun buildGcsPayload(): ByteArray {
|
||||
private fun buildGcsPayload(wantedTypes: List<UByte>? = null, minTimestamp: ULong? = null): ByteArray {
|
||||
// Collect candidates: latest announcement per peer + recent broadcast messages
|
||||
val list = ArrayList<BitchatPacket>()
|
||||
// Determine types to include (default: ANNOUNCE and MESSAGE if wantedTypes is null or empty)
|
||||
val targetTypes = if (wantedTypes == null || wantedTypes.isEmpty()) {
|
||||
listOf(MessageType.ANNOUNCE.value, MessageType.MESSAGE.value)
|
||||
} else {
|
||||
wantedTypes
|
||||
}
|
||||
val minTs = minTimestamp ?: 0uL
|
||||
|
||||
// announcements
|
||||
for ((_, pair) in latestAnnouncementByPeer) {
|
||||
list.add(pair.second)
|
||||
if (targetTypes.contains(MessageType.ANNOUNCE.value)) {
|
||||
for ((_, pair) in latestAnnouncementByPeer) {
|
||||
val pkt = pair.second
|
||||
if (pkt.timestamp >= minTs) {
|
||||
list.add(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
// messages
|
||||
synchronized(messages) {
|
||||
list.addAll(messages.values)
|
||||
if (targetTypes.contains(MessageType.MESSAGE.value)) {
|
||||
synchronized(messages) {
|
||||
for (pkt in messages.values) {
|
||||
if (pkt.timestamp >= minTs) {
|
||||
list.add(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort by timestamp desc, then take up to min(seenCapacity, fit capacity)
|
||||
list.sortByDescending { it.timestamp.toLong() }
|
||||
@ -254,12 +287,12 @@ class GossipSyncManager(
|
||||
val takeN = minOf(nMax, cap, list.size)
|
||||
if (takeN <= 0) {
|
||||
val p0 = GCSFilter.deriveP(fpr)
|
||||
return RequestSyncPacket(p = p0, m = 1, data = ByteArray(0)).encode()
|
||||
return RequestSyncPacket(p = p0, m = 1, data = ByteArray(0), wantedTypes = wantedTypes, minTimestamp = minTimestamp).encode()
|
||||
}
|
||||
val ids = list.take(takeN).map { pkt -> PacketIdUtil.computeIdBytes(pkt) }
|
||||
val params = GCSFilter.buildFilter(ids, maxBytes, fpr)
|
||||
val mVal = if (params.m <= 0L) 1 else params.m
|
||||
return RequestSyncPacket(p = params.p, m = mVal, data = params.data).encode()
|
||||
return RequestSyncPacket(p = params.p, m = mVal, data = params.data, wantedTypes = wantedTypes, minTimestamp = minTimestamp).encode()
|
||||
}
|
||||
|
||||
// Periodically remove stale announcements and all their messages
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
package com.bitchat.android.protocol
|
||||
|
||||
import com.bitchat.android.model.RequestSyncPacket
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class RequestSyncPacketTest {
|
||||
|
||||
@Test
|
||||
fun testBaseFieldsRoundTrip() {
|
||||
val original = RequestSyncPacket(
|
||||
p = 7,
|
||||
m = 12800L,
|
||||
data = byteArrayOf(1, 2, 3, 4, 5)
|
||||
)
|
||||
val encoded = original.encode()
|
||||
val decoded = RequestSyncPacket.decode(encoded)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals(7, decoded!!.p)
|
||||
assertEquals(12800L, decoded.m)
|
||||
assertTrue(original.data.contentEquals(decoded.data))
|
||||
assertNull(decoded.wantedTypes)
|
||||
assertNull(decoded.minTimestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpgradedFieldsRoundTrip() {
|
||||
val original = RequestSyncPacket(
|
||||
p = 8,
|
||||
m = 25600L,
|
||||
data = byteArrayOf(10, 20, 30),
|
||||
wantedTypes = listOf(0x01u, 0x02u),
|
||||
minTimestamp = 1700000000000uL
|
||||
)
|
||||
val encoded = original.encode()
|
||||
val decoded = RequestSyncPacket.decode(encoded)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals(8, decoded!!.p)
|
||||
assertEquals(25600L, decoded.m)
|
||||
assertTrue(original.data.contentEquals(decoded.data))
|
||||
assertNotNull(decoded.wantedTypes)
|
||||
assertEquals(2, decoded.wantedTypes!!.size)
|
||||
assertEquals(0x01u.toUByte(), decoded.wantedTypes!![0])
|
||||
assertEquals(0x02u.toUByte(), decoded.wantedTypes!![1])
|
||||
assertEquals(1700000000000uL, decoded.minTimestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLegacyCompatibleDecode() {
|
||||
// Construct a raw legacy payload manually without fields 0x04 or 0x05
|
||||
// Payload consists of:
|
||||
// Type 0x01: length 1, value P (7)
|
||||
// Type 0x02: length 4, value M (12800 -> 0x00003200)
|
||||
// Type 0x03: length 3, value data (1, 2, 3)
|
||||
val payload = byteArrayOf(
|
||||
0x01, 0x00, 0x01, 0x07, // P
|
||||
0x02, 0x00, 0x04, 0x00, 0x00, 0x32, 0x00, // M
|
||||
0x03, 0x00, 0x03, 0x01, 0x02, 0x03 // data
|
||||
)
|
||||
|
||||
val decoded = RequestSyncPacket.decode(payload)
|
||||
assertNotNull(decoded)
|
||||
assertEquals(7, decoded!!.p)
|
||||
assertEquals(12800L, decoded.m)
|
||||
assertTrue(byteArrayOf(1, 2, 3).contentEquals(decoded.data))
|
||||
assertNull(decoded.wantedTypes)
|
||||
assertNull(decoded.minTimestamp)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user