Carry channel messages in the BitchatMessage binary envelope

Mesh public chat stays UTF-8 for iOS interop. Channel sends previously
ignored the channel argument and broadcast raw text, so every #room
landed in the Mesh feed. Encode channel traffic with toBinaryPayload
and decode it on receive so IncomingMessageAdmission can route by
channel.
This commit is contained in:
Taksh 2026-08-29 18:32:10 +05:30
parent 4f828b5643
commit 371c123910
4 changed files with 108 additions and 6 deletions

View File

@ -852,6 +852,34 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
return reusable
}
/**
* Mesh timeline stays UTF-8 for iOS interop. Channel messages use the shared
* [BitchatMessage] binary envelope so receivers can route by `channel`.
*/
private fun encodePublicOrChannelPayload(
content: String,
mentions: List<String>,
channel: String?
): ByteArray {
if (channel == null) return content.toByteArray(Charsets.UTF_8)
val nickname = try {
com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID)
} catch (_: Exception) {
myPeerID
}
val encoded = BitchatMessage(
sender = nickname,
content = content,
timestamp = java.util.Date(),
isRelay = false,
senderPeerID = myPeerID,
mentions = mentions.takeIf { it.isNotEmpty() },
channel = channel
).toBinaryPayload()
return encoded ?: content.toByteArray(Charsets.UTF_8)
}
/**
* Send public message
*/
@ -859,13 +887,14 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
if (content.isEmpty()) return
serviceScope.launch {
val payloadBytes = encodePublicOrChannelPayload(content, mentions, channel)
val packet = BitchatPacket(
version = 1u,
type = MessageType.MESSAGE.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = SpecialRecipients.BROADCAST,
timestamp = System.currentTimeMillis().toULong(),
payload = content.toByteArray(Charsets.UTF_8),
payload = payloadBytes,
signature = null,
ttl = MAX_TTL
)

View File

@ -530,16 +530,42 @@ class MeshCore(
}
}
/**
* Mesh timeline stays UTF-8 for iOS interop. Channel messages use the shared
* [BitchatMessage] binary envelope so receivers can route by `channel` instead
* of dumping every #room into the public Mesh feed.
*/
private fun encodePublicOrChannelPayload(
content: String,
mentions: List<String>,
channel: String?
): ByteArray {
if (channel == null) return content.toByteArray(Charsets.UTF_8)
val nickname = hooks.announcementNicknameProvider?.invoke() ?: myPeerID
val encoded = BitchatMessage(
sender = nickname,
content = content,
timestamp = java.util.Date(),
isRelay = false,
senderPeerID = myPeerID,
mentions = mentions.takeIf { it.isNotEmpty() },
channel = channel
).toBinaryPayload()
return encoded ?: content.toByteArray(Charsets.UTF_8)
}
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null) {
if (content.isEmpty()) return
scope.launch {
val payloadBytes = encodePublicOrChannelPayload(content, mentions, channel)
val packet = BitchatPacket(
version = 1u,
type = MessageType.MESSAGE.value,
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
recipientID = SpecialRecipients.BROADCAST,
timestamp = System.currentTimeMillis().toULong(),
payload = content.toByteArray(Charsets.UTF_8),
payload = payloadBytes,
signature = null,
ttl = maxTtl
)

View File

@ -484,10 +484,20 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
Log.w(TAG, "FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)}")
}
// Fallback: plain text
val message = BitchatMessage(
id = PacketIdUtil.computeIdHex(packet).uppercase(),
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
// Prefer the shared binary envelope (channel / mentions). Fall back to
// legacy UTF-8 mesh text for older peers and for iOS public broadcasts.
val packetId = PacketIdUtil.computeIdHex(packet).uppercase()
val peerNickname = delegate?.getPeerNickname(peerID) ?: "unknown"
val message = BitchatMessage.fromBinaryPayload(packet.payload)?.let { decoded ->
decoded.copy(
id = packetId,
sender = decoded.sender.ifBlank { peerNickname },
senderPeerID = peerID,
timestamp = Date(packet.timestamp.toLong())
)
} ?: BitchatMessage(
id = packetId,
sender = peerNickname,
content = String(packet.payload, Charsets.UTF_8),
senderPeerID = peerID,
timestamp = Date(packet.timestamp.toLong())

View File

@ -0,0 +1,37 @@
package com.bitchat.android.model
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.Date
class ChannelWireFormatTest {
@Test
fun channelFlagSurvivesBinaryRoundTrip() {
val original = BitchatMessage(
sender = "alice",
content = "hello channel",
timestamp = Date(1_700_000_000_000L),
senderPeerID = "aabbccddeeff0011",
channel = "#general"
)
val encoded = original.toBinaryPayload()
assertNotNull(encoded)
// Must not be plain UTF-8 of the content alone — that is the mesh leak.
assertTrue(encoded!!.size > original.content.toByteArray(Charsets.UTF_8).size)
val decoded = BitchatMessage.fromBinaryPayload(encoded)
assertNotNull(decoded)
assertEquals("#general", decoded!!.channel)
assertEquals("hello channel", decoded.content)
assertEquals("alice", decoded.sender)
}
@Test
fun plainUtf8MeshPayloadDoesNotParseAsChannelEnvelope() {
val plain = "just a mesh hello".toByteArray(Charsets.UTF_8)
assertNull(BitchatMessage.fromBinaryPayload(plain))
}
}