Fix delivery acks and read receipts (#186)

* delivery ack and read receipt

* handle deliveryack

* fix uppercase id

* fix read receipts too
This commit is contained in:
callebtc
2025-07-25 17:54:12 +02:00
committed by GitHub
parent 534be7e613
commit 565e4ab33c
8 changed files with 274 additions and 92 deletions
@@ -46,7 +46,7 @@ sealed class DeliveryStatus : Parcelable {
*/
@Parcelize
data class BitchatMessage(
val id: String = UUID.randomUUID().toString(),
val id: String = UUID.randomUUID().toString().uppercase(),
val sender: String,
val content: String,
val timestamp: Date,
@@ -1,12 +1,13 @@
package com.bitchat.android.model
import android.os.Parcelable
import com.google.gson.GsonBuilder
import kotlinx.parcelize.Parcelize
import com.bitchat.android.util.*
import java.util.*
/**
* Delivery acknowledgment structure - exact same as iOS version
* Uses binary encoding for efficient protocol communication
*/
@Parcelize
data class DeliveryAck(
@@ -15,29 +16,91 @@ data class DeliveryAck(
val recipientID: String,
val recipientNickname: String,
val timestamp: Date = Date(),
val hopCount: UInt
val hopCount: UByte
) : Parcelable {
private val gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
.create()
fun encode(): ByteArray? {
return try {
gson.toJson(this).toByteArray(Charsets.UTF_8)
} catch (e: Exception) {
null
// Primary constructor for creating new acks
constructor(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UByte) : this(
originalMessageID = originalMessageID,
ackID = UUID.randomUUID().toString(),
recipientID = recipientID,
recipientNickname = recipientNickname,
timestamp = Date(),
hopCount = hopCount
)
/**
* Encode to binary data matching iOS toBinaryData implementation
*/
fun encode(): ByteArray {
val builder = BinaryDataBuilder()
// Append original message UUID
builder.appendUUID(originalMessageID)
// Append ack ID UUID
builder.appendUUID(ackID)
// Append recipient ID as 8-byte hex string
val recipientData = ByteArray(8) { 0 }
var tempID = recipientID
var index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
recipientData[index] = byte
}
tempID = tempID.substring(2)
index++
}
builder.buffer.addAll(recipientData.toList())
// Append hop count (UInt8)
builder.appendUInt8(hopCount)
// Append timestamp
builder.appendDate(timestamp)
// Append recipient nickname as string
builder.appendString(recipientNickname)
return builder.toByteArray()
}
companion object {
/**
* Decode from binary data matching iOS fromBinaryData implementation
*/
fun decode(data: ByteArray): DeliveryAck? {
return try {
val json = String(data, Charsets.UTF_8)
com.google.gson.Gson().fromJson(json, DeliveryAck::class.java)
} catch (e: Exception) {
null
}
// Create defensive copy
val dataCopy = data.copyOf()
// Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname
if (dataCopy.size < 50) return null
val offset = intArrayOf(0)
val originalMessageID = dataCopy.readUUID(offset) ?: return null
val ackID = dataCopy.readUUID(offset) ?: return null
val recipientIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
val recipientID = recipientIDData.hexEncodedString()
val hopCount = dataCopy.readUInt8(offset) ?: return null
val timestamp = dataCopy.readDate(offset) ?: return null
val recipientNickname = dataCopy.readString(offset) ?: return null
return DeliveryAck(
originalMessageID = originalMessageID,
ackID = ackID,
recipientID = recipientID,
recipientNickname = recipientNickname,
timestamp = timestamp,
hopCount = hopCount
)
}
}
}
@@ -2,10 +2,12 @@ package com.bitchat.android.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import com.bitchat.android.util.*
import java.util.*
/**
* Read receipt structure - exact same as iOS version
* Uses binary encoding for efficient protocol communication
*/
@Parcelize
data class ReadReceipt(
@@ -15,23 +17,83 @@ data class ReadReceipt(
val readerNickname: String,
val timestamp: Date = Date()
) : Parcelable {
// Primary constructor for creating new read receipts
constructor(originalMessageID: String, readerID: String, readerNickname: String) : this(
originalMessageID = originalMessageID,
receiptID = UUID.randomUUID().toString(),
readerID = readerID,
readerNickname = readerNickname,
timestamp = Date()
)
fun encode(): ByteArray? {
return try {
com.google.gson.Gson().toJson(this).toByteArray(Charsets.UTF_8)
} catch (e: Exception) {
null
/**
* Encode to binary data matching iOS toBinaryData implementation
*/
fun encode(): ByteArray {
val builder = BinaryDataBuilder()
// Append original message UUID
builder.appendUUID(originalMessageID)
// Append receipt ID UUID
builder.appendUUID(receiptID)
// Append reader ID as 8-byte hex string
val readerData = ByteArray(8) { 0 }
var tempID = readerID
var index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
readerData[index] = byte
}
tempID = tempID.substring(2)
index++
}
builder.buffer.addAll(readerData.toList())
// Append timestamp
builder.appendDate(timestamp)
// Append reader nickname as string
builder.appendString(readerNickname)
return builder.toByteArray()
}
companion object {
/**
* Decode from binary data matching iOS fromBinaryData implementation
*/
fun decode(data: ByteArray): ReadReceipt? {
return try {
val json = String(data, Charsets.UTF_8)
com.google.gson.Gson().fromJson(json, ReadReceipt::class.java)
} catch (e: Exception) {
null
}
// Create defensive copy
val dataCopy = data.copyOf()
// Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname
if (dataCopy.size < 49) return null
val offset = intArrayOf(0)
val originalMessageID = dataCopy.readUUID(offset) ?: return null
val receiptID = dataCopy.readUUID(offset) ?: return null
val readerIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
val readerID = readerIDData.hexEncodedString()
val timestamp = dataCopy.readDate(offset) ?: return null
val readerNickname = dataCopy.readString(offset) ?: return null
return ReadReceipt(
originalMessageID = originalMessageID,
receiptID = receiptID,
readerID = readerID,
readerNickname = readerNickname,
timestamp = timestamp
)
}
}
}