mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 11:25:21 +00:00
Changes bitchat protocol (#265)
* create payload * compiles and can send messages * identityannouncement * DMs work, read receipt not sent yet * works * delete old code * simplify * working * fragment wip * compression wip * use zlib compression * clean * nice * mesh * remove comments
This commit is contained in:
@@ -13,22 +13,22 @@ import java.util.*
|
||||
sealed class DeliveryStatus : Parcelable {
|
||||
@Parcelize
|
||||
object Sending : DeliveryStatus()
|
||||
|
||||
|
||||
@Parcelize
|
||||
object Sent : DeliveryStatus()
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class Delivered(val to: String, val at: Date) : DeliveryStatus()
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class Read(val by: String, val at: Date) : DeliveryStatus()
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class Failed(val reason: String) : DeliveryStatus()
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class PartiallyDelivered(val reached: Int, val total: Int) : DeliveryStatus()
|
||||
|
||||
|
||||
fun getDisplayText(): String {
|
||||
return when (this) {
|
||||
is Sending -> "Sending..."
|
||||
@@ -68,7 +68,7 @@ data class BitchatMessage(
|
||||
fun toBinaryPayload(): ByteArray? {
|
||||
try {
|
||||
val buffer = ByteBuffer.allocate(4096).apply { order(ByteOrder.BIG_ENDIAN) }
|
||||
|
||||
|
||||
// Message format:
|
||||
// - Flags: 1 byte (bit flags for optional fields)
|
||||
// - Timestamp: 8 bytes (milliseconds since epoch, big-endian)
|
||||
@@ -76,7 +76,7 @@ data class BitchatMessage(
|
||||
// - Sender length: 1 byte + sender data
|
||||
// - Content length: 2 bytes + content data (or encrypted content)
|
||||
// Optional fields based on flags...
|
||||
|
||||
|
||||
var flags: UByte = 0u
|
||||
if (isRelay) flags = flags or 0x01u
|
||||
if (isPrivate) flags = flags or 0x02u
|
||||
@@ -86,23 +86,23 @@ data class BitchatMessage(
|
||||
if (mentions != null && mentions.isNotEmpty()) flags = flags or 0x20u
|
||||
if (channel != null) flags = flags or 0x40u
|
||||
if (isEncrypted) flags = flags or 0x80u
|
||||
|
||||
|
||||
buffer.put(flags.toByte())
|
||||
|
||||
|
||||
// Timestamp (in milliseconds, 8 bytes big-endian)
|
||||
val timestampMillis = timestamp.time
|
||||
buffer.putLong(timestampMillis)
|
||||
|
||||
|
||||
// ID
|
||||
val idBytes = id.toByteArray(Charsets.UTF_8)
|
||||
buffer.put(minOf(idBytes.size, 255).toByte())
|
||||
buffer.put(idBytes.take(255).toByteArray())
|
||||
|
||||
|
||||
// Sender
|
||||
val senderBytes = sender.toByteArray(Charsets.UTF_8)
|
||||
buffer.put(minOf(senderBytes.size, 255).toByte())
|
||||
buffer.put(senderBytes.take(255).toByteArray())
|
||||
|
||||
|
||||
// Content or encrypted content
|
||||
if (isEncrypted && encryptedContent != null) {
|
||||
val length = minOf(encryptedContent.size, 65535)
|
||||
@@ -114,26 +114,26 @@ data class BitchatMessage(
|
||||
buffer.putShort(length.toShort())
|
||||
buffer.put(contentBytes.take(length).toByteArray())
|
||||
}
|
||||
|
||||
|
||||
// Optional fields
|
||||
originalSender?.let { origSender ->
|
||||
val origBytes = origSender.toByteArray(Charsets.UTF_8)
|
||||
buffer.put(minOf(origBytes.size, 255).toByte())
|
||||
buffer.put(origBytes.take(255).toByteArray())
|
||||
}
|
||||
|
||||
|
||||
recipientNickname?.let { recipient ->
|
||||
val recipBytes = recipient.toByteArray(Charsets.UTF_8)
|
||||
buffer.put(minOf(recipBytes.size, 255).toByte())
|
||||
buffer.put(recipBytes.take(255).toByteArray())
|
||||
}
|
||||
|
||||
|
||||
senderPeerID?.let { peerID ->
|
||||
val peerBytes = peerID.toByteArray(Charsets.UTF_8)
|
||||
buffer.put(minOf(peerBytes.size, 255).toByte())
|
||||
buffer.put(peerBytes.take(255).toByteArray())
|
||||
}
|
||||
|
||||
|
||||
// Mentions array
|
||||
mentions?.let { mentionList ->
|
||||
buffer.put(minOf(mentionList.size, 255).toByte())
|
||||
@@ -143,24 +143,24 @@ data class BitchatMessage(
|
||||
buffer.put(mentionBytes.take(255).toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Channel hashtag
|
||||
channel?.let { channelName ->
|
||||
val channelBytes = channelName.toByteArray(Charsets.UTF_8)
|
||||
buffer.put(minOf(channelBytes.size, 255).toByte())
|
||||
buffer.put(channelBytes.take(255).toByteArray())
|
||||
}
|
||||
|
||||
|
||||
val result = ByteArray(buffer.position())
|
||||
buffer.rewind()
|
||||
buffer.get(result)
|
||||
return result
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Parse message from binary payload - exactly same logic as iOS version
|
||||
@@ -168,9 +168,9 @@ data class BitchatMessage(
|
||||
fun fromBinaryPayload(data: ByteArray): BitchatMessage? {
|
||||
try {
|
||||
if (data.size < 13) return null
|
||||
|
||||
|
||||
val buffer = ByteBuffer.wrap(data).apply { order(ByteOrder.BIG_ENDIAN) }
|
||||
|
||||
|
||||
// Flags
|
||||
val flags = buffer.get().toUByte()
|
||||
val isRelay = (flags and 0x01u) != 0u.toUByte()
|
||||
@@ -181,32 +181,32 @@ data class BitchatMessage(
|
||||
val hasMentions = (flags and 0x20u) != 0u.toUByte()
|
||||
val hasChannel = (flags and 0x40u) != 0u.toUByte()
|
||||
val isEncrypted = (flags and 0x80u) != 0u.toUByte()
|
||||
|
||||
|
||||
// Timestamp
|
||||
val timestampMillis = buffer.getLong()
|
||||
val timestamp = Date(timestampMillis)
|
||||
|
||||
|
||||
// ID
|
||||
val idLength = buffer.get().toInt() and 0xFF
|
||||
if (buffer.remaining() < idLength) return null
|
||||
val idBytes = ByteArray(idLength)
|
||||
buffer.get(idBytes)
|
||||
val id = String(idBytes, Charsets.UTF_8)
|
||||
|
||||
|
||||
// Sender
|
||||
val senderLength = buffer.get().toInt() and 0xFF
|
||||
if (buffer.remaining() < senderLength) return null
|
||||
val senderBytes = ByteArray(senderLength)
|
||||
buffer.get(senderBytes)
|
||||
val sender = String(senderBytes, Charsets.UTF_8)
|
||||
|
||||
|
||||
// Content
|
||||
val contentLength = buffer.getShort().toInt() and 0xFFFF
|
||||
if (buffer.remaining() < contentLength) return null
|
||||
|
||||
|
||||
val content: String
|
||||
val encryptedContent: ByteArray?
|
||||
|
||||
|
||||
if (isEncrypted) {
|
||||
val encryptedBytes = ByteArray(contentLength)
|
||||
buffer.get(encryptedBytes)
|
||||
@@ -218,7 +218,7 @@ data class BitchatMessage(
|
||||
content = String(contentBytes, Charsets.UTF_8)
|
||||
encryptedContent = null
|
||||
}
|
||||
|
||||
|
||||
// Optional fields
|
||||
val originalSender = if (hasOriginalSender && buffer.hasRemaining()) {
|
||||
val length = buffer.get().toInt() and 0xFF
|
||||
@@ -228,7 +228,7 @@ data class BitchatMessage(
|
||||
String(bytes, Charsets.UTF_8)
|
||||
} else null
|
||||
} else null
|
||||
|
||||
|
||||
val recipientNickname = if (hasRecipientNickname && buffer.hasRemaining()) {
|
||||
val length = buffer.get().toInt() and 0xFF
|
||||
if (buffer.remaining() >= length) {
|
||||
@@ -237,7 +237,7 @@ data class BitchatMessage(
|
||||
String(bytes, Charsets.UTF_8)
|
||||
} else null
|
||||
} else null
|
||||
|
||||
|
||||
val senderPeerID = if (hasSenderPeerID && buffer.hasRemaining()) {
|
||||
val length = buffer.get().toInt() and 0xFF
|
||||
if (buffer.remaining() >= length) {
|
||||
@@ -246,7 +246,7 @@ data class BitchatMessage(
|
||||
String(bytes, Charsets.UTF_8)
|
||||
} else null
|
||||
} else null
|
||||
|
||||
|
||||
// Mentions array
|
||||
val mentions = if (hasMentions && buffer.hasRemaining()) {
|
||||
val mentionCount = buffer.get().toInt() and 0xFF
|
||||
@@ -263,7 +263,7 @@ data class BitchatMessage(
|
||||
}
|
||||
if (mentionList.isNotEmpty()) mentionList else null
|
||||
} else null
|
||||
|
||||
|
||||
// Channel
|
||||
val channel = if (hasChannel && buffer.hasRemaining()) {
|
||||
val length = buffer.get().toInt() and 0xFF
|
||||
@@ -273,7 +273,7 @@ data class BitchatMessage(
|
||||
String(bytes, Charsets.UTF_8)
|
||||
} else null
|
||||
} else null
|
||||
|
||||
|
||||
return BitchatMessage(
|
||||
id = id,
|
||||
sender = sender,
|
||||
@@ -289,19 +289,19 @@ data class BitchatMessage(
|
||||
encryptedContent = encryptedContent,
|
||||
isEncrypted = isEncrypted
|
||||
)
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
|
||||
other as BitchatMessage
|
||||
|
||||
|
||||
if (id != other.id) return false
|
||||
if (sender != other.sender) return false
|
||||
if (content != other.content) return false
|
||||
@@ -319,10 +319,10 @@ data class BitchatMessage(
|
||||
} else if (other.encryptedContent != null) return false
|
||||
if (isEncrypted != other.isEncrypted) return false
|
||||
if (deliveryStatus != other.deliveryStatus) return false
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + sender.hashCode()
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
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(
|
||||
val originalMessageID: String,
|
||||
val ackID: String = UUID.randomUUID().toString(),
|
||||
val recipientID: String,
|
||||
val recipientNickname: String,
|
||||
val timestamp: Date = Date(),
|
||||
val hopCount: UByte
|
||||
) : Parcelable {
|
||||
|
||||
// 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? {
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
|
||||
/**
|
||||
* FragmentPayload - 100% iOS-compatible fragment payload structure
|
||||
*
|
||||
* This class handles the encoding and decoding of fragment payloads exactly
|
||||
* as implemented in iOS bitchat SimplifiedBluetoothService.
|
||||
*
|
||||
* Fragment payload structure (matching iOS):
|
||||
* - 8 bytes: Fragment ID (random bytes)
|
||||
* - 2 bytes: Index (big-endian)
|
||||
* - 2 bytes: Total count (big-endian)
|
||||
* - 1 byte: Original message type
|
||||
* - Variable: Fragment data
|
||||
*
|
||||
* Total header size: 13 bytes
|
||||
*/
|
||||
data class FragmentPayload(
|
||||
val fragmentID: ByteArray, // 8 bytes - random fragment identifier
|
||||
val index: Int, // Fragment index (0-based)
|
||||
val total: Int, // Total number of fragments
|
||||
val originalType: UByte, // Original message type before fragmentation
|
||||
val data: ByteArray // Fragment data
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val HEADER_SIZE = 13
|
||||
const val FRAGMENT_ID_SIZE = 8
|
||||
|
||||
/**
|
||||
* Decode fragment payload from binary data
|
||||
* Matches iOS implementation exactly
|
||||
*/
|
||||
fun decode(payloadData: ByteArray): FragmentPayload? {
|
||||
if (payloadData.size < HEADER_SIZE) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract fragment ID (8 bytes)
|
||||
val fragmentID = payloadData.sliceArray(0..<FRAGMENT_ID_SIZE)
|
||||
|
||||
// Extract index (2 bytes, big-endian) - matching iOS
|
||||
val index = ((payloadData[8].toInt() and 0xFF) shl 8) or
|
||||
(payloadData[9].toInt() and 0xFF)
|
||||
|
||||
// Extract total (2 bytes, big-endian) - matching iOS
|
||||
val total = ((payloadData[10].toInt() and 0xFF) shl 8) or
|
||||
(payloadData[11].toInt() and 0xFF)
|
||||
|
||||
// Extract original type (1 byte)
|
||||
val originalType = payloadData[12].toUByte()
|
||||
|
||||
// Extract fragment data (remaining bytes)
|
||||
val data = if (payloadData.size > HEADER_SIZE) {
|
||||
payloadData.sliceArray(HEADER_SIZE..<payloadData.size)
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
|
||||
return FragmentPayload(fragmentID, index, total, originalType, data)
|
||||
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random fragment ID (8 bytes)
|
||||
* Matches iOS implementation: Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||
*/
|
||||
fun generateFragmentID(): ByteArray {
|
||||
val fragmentID = ByteArray(FRAGMENT_ID_SIZE)
|
||||
kotlin.random.Random.nextBytes(fragmentID)
|
||||
return fragmentID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode fragment payload to binary data
|
||||
* Matches iOS implementation exactly
|
||||
*/
|
||||
fun encode(): ByteArray {
|
||||
val payload = ByteArray(HEADER_SIZE + data.size)
|
||||
|
||||
// Fragment ID (8 bytes)
|
||||
System.arraycopy(fragmentID, 0, payload, 0, FRAGMENT_ID_SIZE)
|
||||
|
||||
// Index (2 bytes, big-endian) - matching iOS withUnsafeBytes(of: UInt16(index).bigEndian)
|
||||
payload[8] = ((index shr 8) and 0xFF).toByte()
|
||||
payload[9] = (index and 0xFF).toByte()
|
||||
|
||||
// Total (2 bytes, big-endian) - matching iOS withUnsafeBytes(of: UInt16(fragments.count).bigEndian)
|
||||
payload[10] = ((total shr 8) and 0xFF).toByte()
|
||||
payload[11] = (total and 0xFF).toByte()
|
||||
|
||||
// Original type (1 byte)
|
||||
payload[12] = originalType.toByte()
|
||||
|
||||
// Fragment data
|
||||
if (data.isNotEmpty()) {
|
||||
System.arraycopy(data, 0, payload, HEADER_SIZE, data.size)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fragment ID as hex string for logging/debugging
|
||||
*/
|
||||
fun getFragmentIDString(): String {
|
||||
return fragmentID.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate fragment payload constraints
|
||||
*/
|
||||
fun isValid(): Boolean {
|
||||
return fragmentID.size == FRAGMENT_ID_SIZE &&
|
||||
index >= 0 &&
|
||||
total > 0 &&
|
||||
index < total &&
|
||||
data.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FragmentPayload
|
||||
|
||||
if (!fragmentID.contentEquals(other.fragmentID)) return false
|
||||
if (index != other.index) return false
|
||||
if (total != other.total) return false
|
||||
if (originalType != other.originalType) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = fragmentID.contentHashCode()
|
||||
result = 31 * result + index
|
||||
result = 31 * result + total
|
||||
result = 31 * result + originalType.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "FragmentPayload(fragmentID=${getFragmentIDString()}, index=$index, total=$total, originalType=$originalType, dataSize=${data.size})"
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.bitchat.android.util.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Handshake request for pending messages - exact same as iOS version
|
||||
* Uses binary encoding for efficient protocol communication
|
||||
*/
|
||||
@Parcelize
|
||||
data class HandshakeRequest(
|
||||
val requestID: String,
|
||||
val requesterID: String, // Who needs the handshake
|
||||
val requesterNickname: String, // Nickname of requester
|
||||
val targetID: String, // Who should initiate handshake
|
||||
val pendingMessageCount: UByte, // Number of messages queued
|
||||
val timestamp: Date
|
||||
) : Parcelable {
|
||||
|
||||
// Primary constructor for creating new requests
|
||||
constructor(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UByte) : this(
|
||||
requestID = UUID.randomUUID().toString(),
|
||||
requesterID = requesterID,
|
||||
requesterNickname = requesterNickname,
|
||||
targetID = targetID,
|
||||
pendingMessageCount = pendingMessageCount,
|
||||
timestamp = Date()
|
||||
)
|
||||
|
||||
/**
|
||||
* Encode to binary data matching iOS toBinaryData implementation
|
||||
*/
|
||||
fun toBinaryData(): ByteArray {
|
||||
val builder = BinaryDataBuilder()
|
||||
|
||||
// Append request ID UUID
|
||||
builder.appendUUID(requestID)
|
||||
|
||||
// RequesterID as 8-byte hex string
|
||||
val requesterData = ByteArray(8) { 0 }
|
||||
var tempID = requesterID
|
||||
var index = 0
|
||||
|
||||
while (tempID.length >= 2 && index < 8) {
|
||||
val hexByte = tempID.substring(0, 2)
|
||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
||||
if (byte != null) {
|
||||
requesterData[index] = byte
|
||||
}
|
||||
tempID = tempID.substring(2)
|
||||
index++
|
||||
}
|
||||
|
||||
builder.buffer.addAll(requesterData.toList())
|
||||
|
||||
// TargetID as 8-byte hex string
|
||||
val targetData = ByteArray(8) { 0 }
|
||||
tempID = targetID
|
||||
index = 0
|
||||
|
||||
while (tempID.length >= 2 && index < 8) {
|
||||
val hexByte = tempID.substring(0, 2)
|
||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
||||
if (byte != null) {
|
||||
targetData[index] = byte
|
||||
}
|
||||
tempID = tempID.substring(2)
|
||||
index++
|
||||
}
|
||||
|
||||
builder.buffer.addAll(targetData.toList())
|
||||
|
||||
// Append pending message count (UInt8)
|
||||
builder.appendUInt8(pendingMessageCount)
|
||||
|
||||
// Append timestamp
|
||||
builder.appendDate(timestamp)
|
||||
|
||||
// Append requester nickname as string
|
||||
builder.appendString(requesterNickname)
|
||||
|
||||
return builder.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Decode from binary data matching iOS fromBinaryData implementation
|
||||
*/
|
||||
fun fromBinaryData(data: ByteArray): HandshakeRequest? {
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
// Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname
|
||||
if (dataCopy.size < 42) return null
|
||||
|
||||
val offset = intArrayOf(0)
|
||||
|
||||
val requestID = dataCopy.readUUID(offset) ?: return null
|
||||
|
||||
val requesterIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
||||
val requesterID = requesterIDData.hexEncodedString()
|
||||
|
||||
val targetIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
||||
val targetID = targetIDData.hexEncodedString()
|
||||
|
||||
val pendingMessageCount = dataCopy.readUInt8(offset) ?: return null
|
||||
val timestamp = dataCopy.readDate(offset) ?: return null
|
||||
val requesterNickname = dataCopy.readString(offset) ?: return null
|
||||
|
||||
return HandshakeRequest(
|
||||
requestID = requestID,
|
||||
requesterID = requesterID,
|
||||
requesterNickname = requesterNickname,
|
||||
targetID = targetID,
|
||||
pendingMessageCount = pendingMessageCount,
|
||||
timestamp = timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.bitchat.android.util.*
|
||||
|
||||
/**
|
||||
* Identity announcement structure with TLV encoding
|
||||
* Compatible with iOS AnnouncementPacket TLV format
|
||||
*/
|
||||
@Parcelize
|
||||
data class IdentityAnnouncement(
|
||||
val nickname: String,
|
||||
val publicKey: ByteArray // FIXED: Made non-nullable to match iOS and AnnouncementPacket
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* TLV types matching iOS implementation
|
||||
*/
|
||||
private enum class TLVType(val value: UByte) {
|
||||
NICKNAME(0x01u),
|
||||
NOISE_PUBLIC_KEY(0x02u);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: UByte): TLVType? {
|
||||
return values().find { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode to TLV binary data matching iOS implementation
|
||||
*/
|
||||
fun encode(): ByteArray? {
|
||||
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Check size limits
|
||||
if (nicknameData.size > 255 || publicKey.size > 255) {
|
||||
return null
|
||||
}
|
||||
|
||||
val result = mutableListOf<Byte>()
|
||||
|
||||
// TLV for nickname
|
||||
result.add(TLVType.NICKNAME.value.toByte())
|
||||
result.add(nicknameData.size.toByte())
|
||||
result.addAll(nicknameData.toList())
|
||||
|
||||
// TLV for public key
|
||||
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
||||
result.add(publicKey.size.toByte())
|
||||
result.addAll(publicKey.toList())
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Decode from TLV binary data matching iOS implementation
|
||||
*/
|
||||
fun decode(data: ByteArray): IdentityAnnouncement? {
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
var offset = 0
|
||||
var nickname: String? = null
|
||||
var publicKey: ByteArray? = null
|
||||
|
||||
while (offset + 2 <= dataCopy.size) {
|
||||
// Read TLV type
|
||||
val typeValue = dataCopy[offset].toUByte()
|
||||
val type = TLVType.fromValue(typeValue) ?: return null
|
||||
offset += 1
|
||||
|
||||
// Read TLV length
|
||||
val length = dataCopy[offset].toUByte().toInt()
|
||||
offset += 1
|
||||
|
||||
// Check bounds
|
||||
if (offset + length > dataCopy.size) return null
|
||||
|
||||
// Read TLV value
|
||||
val value = dataCopy.sliceArray(offset until offset + length)
|
||||
offset += length
|
||||
|
||||
when (type) {
|
||||
TLVType.NICKNAME -> {
|
||||
nickname = String(value, Charsets.UTF_8)
|
||||
}
|
||||
TLVType.NOISE_PUBLIC_KEY -> {
|
||||
publicKey = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both fields are required
|
||||
return if (nickname != null && publicKey != null) {
|
||||
IdentityAnnouncement(nickname, publicKey)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override equals and hashCode since we use ByteArray
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as IdentityAnnouncement
|
||||
|
||||
if (nickname != other.nickname) return false
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = nickname.hashCode()
|
||||
result = 31 * result + publicKey.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "IdentityAnnouncement(nickname='$nickname', publicKey=${publicKey.joinToString("") { "%02x".format(it) }.take(16)}...)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Noise encrypted payload types and handling - 100% compatible with iOS SimplifiedBluetoothService
|
||||
*
|
||||
* This handles all encrypted content that goes through noiseEncrypted packets:
|
||||
* - Private messages with TLV encoding
|
||||
* - Delivery acknowledgments
|
||||
* - Read receipts
|
||||
* - Future encrypted payload types
|
||||
*/
|
||||
|
||||
/**
|
||||
* Types of payloads embedded within noiseEncrypted messages
|
||||
* Matches iOS NoisePayloadType exactly
|
||||
*/
|
||||
enum class NoisePayloadType(val value: UByte) {
|
||||
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
|
||||
READ_RECEIPT(0x02u), // Message was read
|
||||
DELIVERED(0x03u); // Message was delivered
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: UByte): NoisePayloadType? {
|
||||
return values().find { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for creating and parsing Noise payloads
|
||||
* Matches iOS NoisePayload helper exactly
|
||||
*/
|
||||
@Parcelize
|
||||
data class NoisePayload(
|
||||
val type: NoisePayloadType,
|
||||
val data: ByteArray
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Encode payload with type prefix - exactly like iOS
|
||||
* Format: [type_byte][payload_data]
|
||||
*/
|
||||
fun encode(): ByteArray {
|
||||
val result = ByteArray(1 + data.size)
|
||||
result[0] = type.value.toByte()
|
||||
data.copyInto(result, destinationOffset = 1)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Decode payload from data - exactly like iOS
|
||||
* Expects: [type_byte][payload_data]
|
||||
*/
|
||||
fun decode(data: ByteArray): NoisePayload? {
|
||||
if (data.isEmpty()) return null
|
||||
|
||||
val typeValue = data[0].toUByte()
|
||||
val type = NoisePayloadType.fromValue(typeValue) ?: return null
|
||||
|
||||
// Extract payload data (remaining bytes after type byte)
|
||||
val payloadData = if (data.size > 1) {
|
||||
data.copyOfRange(1, data.size)
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
|
||||
return NoisePayload(type, payloadData)
|
||||
}
|
||||
}
|
||||
|
||||
// Override equals and hashCode since we use ByteArray
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as NoisePayload
|
||||
|
||||
if (type != other.type) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = type.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private message packet with TLV encoding - matches iOS PrivateMessagePacket exactly
|
||||
*/
|
||||
@Parcelize
|
||||
data class PrivateMessagePacket(
|
||||
val messageID: String,
|
||||
val content: String
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* TLV types matching iOS implementation exactly
|
||||
*/
|
||||
private enum class TLVType(val value: UByte) {
|
||||
MESSAGE_ID(0x00u),
|
||||
CONTENT(0x01u);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: UByte): TLVType? {
|
||||
return values().find { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode to TLV binary data - exactly like iOS
|
||||
* Format: [type][length][value] for each field
|
||||
*/
|
||||
fun encode(): ByteArray? {
|
||||
val messageIDData = messageID.toByteArray(Charsets.UTF_8)
|
||||
val contentData = content.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Check size limits (TLV length field is 1 byte = max 255)
|
||||
if (messageIDData.size > 255 || contentData.size > 255) {
|
||||
return null
|
||||
}
|
||||
|
||||
val result = mutableListOf<Byte>()
|
||||
|
||||
// TLV for messageID
|
||||
result.add(TLVType.MESSAGE_ID.value.toByte())
|
||||
result.add(messageIDData.size.toByte())
|
||||
result.addAll(messageIDData.toList())
|
||||
|
||||
// TLV for content
|
||||
result.add(TLVType.CONTENT.value.toByte())
|
||||
result.add(contentData.size.toByte())
|
||||
result.addAll(contentData.toList())
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Decode from TLV binary data - exactly like iOS
|
||||
*/
|
||||
fun decode(data: ByteArray): PrivateMessagePacket? {
|
||||
var offset = 0
|
||||
var messageID: String? = null
|
||||
var content: String? = null
|
||||
|
||||
while (offset + 2 <= data.size) {
|
||||
// Read TLV type
|
||||
val typeValue = data[offset].toUByte()
|
||||
val type = TLVType.fromValue(typeValue) ?: return null
|
||||
offset += 1
|
||||
|
||||
// Read TLV length
|
||||
val length = data[offset].toUByte().toInt()
|
||||
offset += 1
|
||||
|
||||
// Check bounds
|
||||
if (offset + length > data.size) return null
|
||||
|
||||
// Read TLV value
|
||||
val value = data.copyOfRange(offset, offset + length)
|
||||
offset += length
|
||||
|
||||
when (type) {
|
||||
TLVType.MESSAGE_ID -> {
|
||||
messageID = String(value, Charsets.UTF_8)
|
||||
}
|
||||
TLVType.CONTENT -> {
|
||||
content = String(value, Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (messageID != null && content != null) {
|
||||
PrivateMessagePacket(messageID, content)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "PrivateMessagePacket(messageID='$messageID', content='${content.take(50)}${if (content.length > 50) "..." else ""}')"
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.*
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Noise Identity Announcement data class (compatible with iOS version)
|
||||
* Enhanced identity announcement with rotation support and binary protocol
|
||||
*/
|
||||
data class NoiseIdentityAnnouncement(
|
||||
val peerID: String, // Current ephemeral peer ID
|
||||
val publicKey: ByteArray, // Noise static public key
|
||||
val signingPublicKey: ByteArray, // Ed25519 signing public key
|
||||
val nickname: String, // Current nickname
|
||||
val timestamp: Date, // When this binding was created
|
||||
val previousPeerID: String?, // Previous peer ID (for smooth transition)
|
||||
val signature: ByteArray // Signature proving ownership
|
||||
) {
|
||||
|
||||
// Computed fingerprint from public key
|
||||
val fingerprint: String by lazy {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKey)
|
||||
hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as NoiseIdentityAnnouncement
|
||||
|
||||
if (peerID != other.peerID) return false
|
||||
if (nickname != other.nickname) return false
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (!signature.contentEquals(other.signature)) return false
|
||||
if (previousPeerID != other.previousPeerID) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = peerID.hashCode()
|
||||
result = 31 * result + nickname.hashCode()
|
||||
result = 31 * result + publicKey.contentHashCode()
|
||||
result = 31 * result + signingPublicKey.contentHashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + signature.contentHashCode()
|
||||
result = 31 * result + (previousPeerID?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Binary Encoding
|
||||
|
||||
fun toBinaryData(): ByteArray {
|
||||
val builder = BinaryDataBuilder()
|
||||
|
||||
// Flags byte: bit 0 = hasPreviousPeerID
|
||||
var flags: UByte = 0u
|
||||
if (previousPeerID != null) flags = flags or 0x01u
|
||||
builder.appendUInt8(flags)
|
||||
|
||||
// PeerID as 8-byte hex string
|
||||
val peerData = hexStringToByteArray(peerID)
|
||||
// Directly append the 8 bytes without length prefix since this is a fixed field
|
||||
for (byte in peerData) {
|
||||
builder.buffer.add(byte)
|
||||
}
|
||||
|
||||
builder.appendData(publicKey)
|
||||
builder.appendData(signingPublicKey)
|
||||
builder.appendString(nickname)
|
||||
builder.appendDate(timestamp)
|
||||
|
||||
if (previousPeerID != null) {
|
||||
// Previous PeerID as 8-byte hex string
|
||||
val prevData = hexStringToByteArray(previousPeerID)
|
||||
// Directly append the 8 bytes without length prefix since this is a fixed field
|
||||
for (byte in prevData) {
|
||||
builder.buffer.add(byte)
|
||||
}
|
||||
}
|
||||
|
||||
builder.appendData(signature)
|
||||
|
||||
return builder.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NoiseIdentityAnnouncement"
|
||||
|
||||
/**
|
||||
* Parse Noise identity announcement from binary payload with proper iOS compatibility
|
||||
*/
|
||||
fun fromBinaryData(data: ByteArray): NoiseIdentityAnnouncement? {
|
||||
return try {
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
// Minimum size check: flags(1) + peerID(8) + min data lengths
|
||||
if (dataCopy.size < 20) {
|
||||
Log.w(TAG, "Data too small for NoiseIdentityAnnouncement: ${dataCopy.size} bytes")
|
||||
return null
|
||||
}
|
||||
|
||||
val offsetArray = intArrayOf(0)
|
||||
|
||||
val flags = dataCopy.readUInt8(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read flags")
|
||||
return null
|
||||
}
|
||||
val hasPreviousPeerID = (flags.toInt() and 0x01) != 0
|
||||
|
||||
// Read peerID using safe method
|
||||
val peerIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
||||
Log.w(TAG, "Failed to read peerID bytes")
|
||||
return null
|
||||
}
|
||||
val peerID = peerIDBytes.hexEncodedString()
|
||||
|
||||
val publicKey = dataCopy.readData(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read public key")
|
||||
return null
|
||||
}
|
||||
|
||||
val signingPublicKey = dataCopy.readData(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read signing public key")
|
||||
return null
|
||||
}
|
||||
|
||||
val nickname = dataCopy.readString(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read nickname")
|
||||
return null
|
||||
}
|
||||
|
||||
val timestamp = dataCopy.readDate(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read timestamp")
|
||||
return null
|
||||
}
|
||||
|
||||
var previousPeerID: String? = null
|
||||
if (hasPreviousPeerID) {
|
||||
// Read previousPeerID using safe method
|
||||
val prevIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
||||
Log.w(TAG, "Failed to read previousPeerID bytes")
|
||||
return null
|
||||
}
|
||||
previousPeerID = prevIDBytes.hexEncodedString()
|
||||
}
|
||||
|
||||
val signature = dataCopy.readData(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read signature")
|
||||
return null
|
||||
}
|
||||
|
||||
Log.d(TAG, "Successfully parsed NoiseIdentityAnnouncement: peerID=$peerID, nickname=$nickname")
|
||||
|
||||
return NoiseIdentityAnnouncement(
|
||||
peerID = peerID,
|
||||
publicKey = publicKey,
|
||||
signingPublicKey = signingPublicKey,
|
||||
nickname = nickname,
|
||||
timestamp = timestamp,
|
||||
previousPeerID = previousPeerID,
|
||||
signature = signature
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
|
||||
*/
|
||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
||||
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
|
||||
var tempID = hexString
|
||||
var index = 0
|
||||
|
||||
while (tempID.length >= 2 && index < 8) {
|
||||
val hexByte = tempID.substring(0, 2)
|
||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
||||
if (byte != null) {
|
||||
result[index] = byte
|
||||
}
|
||||
tempID = tempID.substring(2)
|
||||
index++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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(
|
||||
val originalMessageID: String,
|
||||
val receiptID: String = UUID.randomUUID().toString(),
|
||||
val readerID: String,
|
||||
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()
|
||||
)
|
||||
|
||||
/**
|
||||
* 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? {
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user