mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 11:05:20 +00:00
new files
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.model.RoutedPacket
|
||||||
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
|
import com.bitchat.android.protocol.MessageType
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared transport send wrapper that applies bitchat packet fragmentation and
|
||||||
|
* transfer progress before a transport writes packets to its concrete medium.
|
||||||
|
*/
|
||||||
|
class FragmentingPacketSender(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val fragmentManager: FragmentManager?,
|
||||||
|
private val logTag: String,
|
||||||
|
private val interFragmentDelayMs: Long = 20L
|
||||||
|
) {
|
||||||
|
private val transferJobs = ConcurrentHashMap<String, Job>()
|
||||||
|
|
||||||
|
fun send(
|
||||||
|
routed: RoutedPacket,
|
||||||
|
description: String,
|
||||||
|
sendSingle: (RoutedPacket) -> Boolean
|
||||||
|
): Boolean {
|
||||||
|
val transferId = transferIdFor(routed)
|
||||||
|
val packets = packetsForTransport(routed.packet) ?: return false
|
||||||
|
val total = packets.size
|
||||||
|
|
||||||
|
if (total <= 1) {
|
||||||
|
if (transferId != null) {
|
||||||
|
TransferProgressManager.start(transferId, 1)
|
||||||
|
}
|
||||||
|
val sent = sendSingle(routed.copy(packet = packets.first(), transferId = transferId))
|
||||||
|
if (sent && transferId != null) {
|
||||||
|
TransferProgressManager.progress(transferId, 1, 1)
|
||||||
|
TransferProgressManager.complete(transferId, 1)
|
||||||
|
}
|
||||||
|
return sent
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(logTag, "Fragmenting packet type ${routed.packet.type} into $total fragments for $description")
|
||||||
|
if (transferId != null) {
|
||||||
|
TransferProgressManager.start(transferId, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
val job = scope.launch(start = CoroutineStart.LAZY) {
|
||||||
|
var sent = 0
|
||||||
|
for (packet in packets) {
|
||||||
|
if (!isActive) return@launch
|
||||||
|
if (transferId != null && transferJobs[transferId]?.isCancelled == true) return@launch
|
||||||
|
|
||||||
|
val fragment = routed.copy(packet = packet, transferId = transferId)
|
||||||
|
val delivered = try {
|
||||||
|
sendSingle(fragment)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Fragment send failed for $description: ${e.message}", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!delivered) {
|
||||||
|
Log.w(logTag, "Stopping fragmented send for $description after $sent/$total fragments")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
sent += 1
|
||||||
|
if (transferId != null) {
|
||||||
|
TransferProgressManager.progress(transferId, sent, total)
|
||||||
|
}
|
||||||
|
if (sent < total) {
|
||||||
|
delay(interFragmentDelayMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferId != null) {
|
||||||
|
TransferProgressManager.complete(transferId, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferId != null) {
|
||||||
|
transferJobs[transferId] = job
|
||||||
|
job.invokeOnCompletion { transferJobs.remove(transferId, job) }
|
||||||
|
}
|
||||||
|
job.start()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTransfer(transferId: String): Boolean {
|
||||||
|
val job = transferJobs.remove(transferId) ?: return false
|
||||||
|
job.cancel()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun packetsForTransport(packet: BitchatPacket): List<BitchatPacket>? {
|
||||||
|
if (packet.type == MessageType.FRAGMENT.value) {
|
||||||
|
return listOf(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
val manager = fragmentManager ?: return listOf(packet)
|
||||||
|
return try {
|
||||||
|
val fragments = manager.createFragments(packet)
|
||||||
|
if (fragments.isEmpty()) {
|
||||||
|
Log.e(logTag, "Fragment manager returned no packets for packet type ${packet.type}")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
fragments
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Fragment creation failed for packet type ${packet.type}: ${e.message}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transferIdFor(routed: RoutedPacket): String? {
|
||||||
|
routed.transferId?.let { return it }
|
||||||
|
val packet = routed.packet
|
||||||
|
return if (packet.type == MessageType.FILE_TRANSFER.value) {
|
||||||
|
sha256Hex(packet.payload)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sha256Hex(bytes: ByteArray): String = try {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
md.update(bytes)
|
||||||
|
md.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
bytes.size.toString(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -636,7 +636,7 @@ private fun PeerItem(
|
|||||||
else -> "Routed"
|
else -> "Routed"
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = if (isWifiAware) Color(0xFF9C27B0) else colorScheme.onSurface.copy(alpha = 0.6f)
|
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,7 +673,7 @@ private fun PeerItem(
|
|||||||
imageVector = Icons.Filled.Wifi,
|
imageVector = Icons.Filled.Wifi,
|
||||||
contentDescription = "Direct Wi-Fi Aware",
|
contentDescription = "Direct Wi-Fi Aware",
|
||||||
modifier = Modifier.size(13.dp),
|
modifier = Modifier.size(13.dp),
|
||||||
tint = Color(0xFF9C27B0).copy(alpha = 0.8f)
|
tint = colorScheme.onSurface.copy(alpha = 0.8f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
|||||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MeshTopologySection(localPeerID: String? = null) {
|
fun MeshTopologySection(
|
||||||
|
localPeerID: String? = null,
|
||||||
|
blePeerIDs: Set<String> = emptySet(),
|
||||||
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val graphService = remember { MeshGraphService.getInstance() }
|
val graphService = remember { MeshGraphService.getInstance() }
|
||||||
val snapshot by graphService.graphState.collectAsState()
|
val snapshot by graphService.graphState.collectAsState()
|
||||||
@@ -65,6 +68,7 @@ fun MeshTopologySection(localPeerID: String? = null) {
|
|||||||
nodes = nodes,
|
nodes = nodes,
|
||||||
edges = edges,
|
edges = edges,
|
||||||
wifiAwarePeerIDs = wifiAwarePeerIDs,
|
wifiAwarePeerIDs = wifiAwarePeerIDs,
|
||||||
|
blePeerIDs = blePeerIDs,
|
||||||
localPeerID = localPeerID,
|
localPeerID = localPeerID,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -225,7 +229,13 @@ fun DebugSettingsSheet(
|
|||||||
|
|
||||||
// Mesh topology visualization (moved below verbose logging)
|
// Mesh topology visualization (moved below verbose logging)
|
||||||
item {
|
item {
|
||||||
MeshTopologySection(localPeerID = meshService.myPeerID)
|
val blePeerIDs = remember(connectedDevices) {
|
||||||
|
connectedDevices.mapNotNull { it.peerID }.toSet()
|
||||||
|
}
|
||||||
|
MeshTopologySection(
|
||||||
|
localPeerID = meshService.myPeerID,
|
||||||
|
blePeerIDs = blePeerIDs,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GATT controls
|
// GATT controls
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.offset
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Wifi
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
|
import androidx.compose.material.icons.outlined.Bluetooth
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -37,6 +38,44 @@ private const val DAMPING = 0.85f
|
|||||||
private const val MAX_VELOCITY = 30f
|
private const val MAX_VELOCITY = 30f
|
||||||
private const val PULSE_DECAY = 0.05f
|
private const val PULSE_DECAY = 0.05f
|
||||||
private const val ROUTE_DECAY = 0.02f
|
private const val ROUTE_DECAY = 0.02f
|
||||||
|
private val EDGE_ICON_OFFSET = 14.dp
|
||||||
|
|
||||||
|
private enum class EdgeTransport { WIFI, BLE }
|
||||||
|
|
||||||
|
private data class EdgeMarker(val x: Float, val y: Float, val transport: EdgeTransport)
|
||||||
|
|
||||||
|
private fun shouldMarkDirectEdge(
|
||||||
|
edge: MeshGraphService.GraphEdge,
|
||||||
|
transportPeerIDs: Set<String>,
|
||||||
|
localID: String?
|
||||||
|
): Boolean {
|
||||||
|
if (transportPeerIDs.isEmpty()) return false
|
||||||
|
return if (localID != null) {
|
||||||
|
(edge.a == localID && edge.b in transportPeerIDs) ||
|
||||||
|
(edge.b == localID && edge.a in transportPeerIDs)
|
||||||
|
} else {
|
||||||
|
edge.a in transportPeerIDs || edge.b in transportPeerIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun edgeMarkerPosition(
|
||||||
|
x1: Float,
|
||||||
|
y1: Float,
|
||||||
|
x2: Float,
|
||||||
|
y2: Float,
|
||||||
|
offsetPx: Float,
|
||||||
|
sideSign: Float
|
||||||
|
): Pair<Float, Float> {
|
||||||
|
val midX = (x1 + x2) / 2f
|
||||||
|
val midY = (y1 + y2) / 2f
|
||||||
|
val dx = x2 - x1
|
||||||
|
val dy = y2 - y1
|
||||||
|
val len = sqrt(dx * dx + dy * dy)
|
||||||
|
if (len < 0.1f) return midX to midY
|
||||||
|
val px = -dy / len * offsetPx * sideSign
|
||||||
|
val py = dx / len * offsetPx * sideSign
|
||||||
|
return (midX + px) to (midY + py)
|
||||||
|
}
|
||||||
|
|
||||||
private class GraphNodeState(
|
private class GraphNodeState(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -219,6 +258,7 @@ fun ForceDirectedMeshGraph(
|
|||||||
nodes: List<MeshGraphService.GraphNode>,
|
nodes: List<MeshGraphService.GraphNode>,
|
||||||
edges: List<MeshGraphService.GraphEdge>,
|
edges: List<MeshGraphService.GraphEdge>,
|
||||||
wifiAwarePeerIDs: Set<String> = emptySet(),
|
wifiAwarePeerIDs: Set<String> = emptySet(),
|
||||||
|
blePeerIDs: Set<String> = emptySet(),
|
||||||
localPeerID: String? = null,
|
localPeerID: String? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -417,38 +457,46 @@ fun ForceDirectedMeshGraph(
|
|||||||
val iconSize = 16.dp
|
val iconSize = 16.dp
|
||||||
val iconSizePx = with(density) { iconSize.toPx() }
|
val iconSizePx = with(density) { iconSize.toPx() }
|
||||||
val halfIconSizePx = iconSizePx / 2f
|
val halfIconSizePx = iconSizePx / 2f
|
||||||
|
val iconOffsetPx = with(density) { EDGE_ICON_OFFSET.toPx() }
|
||||||
val localID = localPeerID
|
val localID = localPeerID
|
||||||
val shouldMarkWifiEdge: (MeshGraphService.GraphEdge) -> Boolean = { edge ->
|
|
||||||
if (localID != null) {
|
|
||||||
(edge.a == localID && edge.b in wifiAwarePeerIDs) ||
|
|
||||||
(edge.b == localID && edge.a in wifiAwarePeerIDs)
|
|
||||||
} else {
|
|
||||||
edge.a in wifiAwarePeerIDs || edge.b in wifiAwarePeerIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val wifiEdgeMidpoints = tick.let {
|
val edgeMarkers = tick.let {
|
||||||
simulation.edges.mapNotNull { edge ->
|
simulation.edges.flatMap { edge ->
|
||||||
val n1 = simulation.nodes[edge.a]
|
val n1 = simulation.nodes[edge.a]
|
||||||
val n2 = simulation.nodes[edge.b]
|
val n2 = simulation.nodes[edge.b]
|
||||||
if (n1 != null && n2 != null && shouldMarkWifiEdge(edge)) {
|
if (n1 == null || n2 == null) return@flatMap emptyList()
|
||||||
((n1.x + n2.x) / 2f) to ((n1.y + n2.y) / 2f)
|
|
||||||
} else {
|
val isWifi = shouldMarkDirectEdge(edge, wifiAwarePeerIDs, localID)
|
||||||
null
|
val isBle = shouldMarkDirectEdge(edge, blePeerIDs, localID)
|
||||||
|
if (!isWifi && !isBle) return@flatMap emptyList()
|
||||||
|
|
||||||
|
val markers = mutableListOf<EdgeMarker>()
|
||||||
|
if (isWifi) {
|
||||||
|
val (x, y) = edgeMarkerPosition(n1.x, n1.y, n2.x, n2.y, iconOffsetPx, sideSign = 1f)
|
||||||
|
markers.add(EdgeMarker(x, y, EdgeTransport.WIFI))
|
||||||
}
|
}
|
||||||
|
if (isBle) {
|
||||||
|
val side = if (isWifi) -1f else 1f
|
||||||
|
val (x, y) = edgeMarkerPosition(n1.x, n1.y, n2.x, n2.y, iconOffsetPx, sideSign = side)
|
||||||
|
markers.add(EdgeMarker(x, y, EdgeTransport.BLE))
|
||||||
|
}
|
||||||
|
markers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wifiEdgeMidpoints.forEach { (midX, midY) ->
|
edgeMarkers.forEach { marker ->
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Wifi,
|
imageVector = when (marker.transport) {
|
||||||
|
EdgeTransport.WIFI -> Icons.Filled.Wifi
|
||||||
|
EdgeTransport.BLE -> Icons.Outlined.Bluetooth
|
||||||
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFF9C27B0).copy(alpha = 0.82f),
|
tint = colorScheme.onSurface.copy(alpha = 0.82f),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset {
|
.offset {
|
||||||
IntOffset(
|
IntOffset(
|
||||||
x = (midX - halfIconSizePx).roundToInt(),
|
x = (marker.x - halfIconSizePx).roundToInt(),
|
||||||
y = (midY - halfIconSizePx).roundToInt()
|
y = (marker.y - halfIconSizePx).roundToInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.size(iconSize)
|
.size(iconSize)
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import com.bitchat.android.model.IdentityAnnouncement
|
||||||
|
import com.bitchat.android.model.RoutedPacket
|
||||||
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
|
import com.bitchat.android.protocol.MessageType
|
||||||
|
import com.bitchat.android.protocol.SpecialRecipients
|
||||||
|
import com.bitchat.android.services.meshgraph.MeshGraphService
|
||||||
|
import com.bitchat.android.util.AppConstants
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.isNull
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.never
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.RuntimeEnvironment
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
||||||
|
class MessageHandlerTest {
|
||||||
|
private lateinit var handler: MessageHandler
|
||||||
|
private lateinit var delegate: MessageHandlerDelegate
|
||||||
|
|
||||||
|
private val myPeerID = "1111222233334444"
|
||||||
|
private val peerID = "aaaabbbbccccdddd"
|
||||||
|
private val nickname = "peer"
|
||||||
|
private val noiseKey = ByteArray(32) { 0x0B }
|
||||||
|
private val signingKey = ByteArray(32) { 0x0A }
|
||||||
|
private val signature = ByteArray(64) { 1 }
|
||||||
|
private val announceClockSkewToleranceMs = 10 * 60 * 1000L
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MeshGraphService.resetForTesting()
|
||||||
|
handler = MessageHandler(myPeerID, RuntimeEnvironment.getApplication())
|
||||||
|
delegate = mock()
|
||||||
|
handler.delegate = delegate
|
||||||
|
|
||||||
|
whenever(delegate.getPeerInfo(peerID)).thenReturn(null)
|
||||||
|
whenever(delegate.verifyEd25519Signature(any(), any(), any())).thenReturn(true)
|
||||||
|
whenever(delegate.updatePeerInfo(any(), any(), any(), any(), any())).thenReturn(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
MeshGraphService.resetForTesting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleAnnounce accepts announce within clock skew tolerance for identity binding`() = runBlocking {
|
||||||
|
val packet = announcePacket(ageMs = AppConstants.Mesh.STALE_PEER_TIMEOUT_MS + 1_000)
|
||||||
|
|
||||||
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "direct-link"))
|
||||||
|
|
||||||
|
assertTrue("Announce within clock skew tolerance should still store peer identity", result)
|
||||||
|
verify(delegate).updatePeerInfo(eq(peerID), eq(nickname), any(), any(), eq(true))
|
||||||
|
verify(delegate).updatePeerIDBinding(eq(peerID), eq(nickname), any(), isNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleAnnounce accepts future announce within clock skew tolerance`() = runBlocking {
|
||||||
|
val packet = announcePacket(ageMs = -(AppConstants.Mesh.STALE_PEER_TIMEOUT_MS + 1_000))
|
||||||
|
|
||||||
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "direct-link"))
|
||||||
|
|
||||||
|
assertTrue("Future announce within clock skew tolerance should still store peer identity", result)
|
||||||
|
verify(delegate).updatePeerInfo(eq(peerID), eq(nickname), any(), any(), eq(true))
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleAnnounce rejects announce older than clock skew tolerance`() = runBlocking {
|
||||||
|
val packet = announcePacket(ageMs = announceClockSkewToleranceMs + 1_000)
|
||||||
|
|
||||||
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "relay-link"))
|
||||||
|
|
||||||
|
assertFalse("Announce older than clock skew tolerance should not store peer identity", result)
|
||||||
|
verify(delegate, never()).updatePeerInfo(any(), any(), any(), any(), any())
|
||||||
|
verify(delegate, never()).updatePeerIDBinding(any(), any(), any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun announcePacket(
|
||||||
|
ageMs: Long,
|
||||||
|
ttl: UByte = (AppConstants.MESSAGE_TTL_HOPS.toInt() - 1).toUByte()
|
||||||
|
): BitchatPacket {
|
||||||
|
val announcement = IdentityAnnouncement(
|
||||||
|
nickname = nickname,
|
||||||
|
noisePublicKey = noiseKey,
|
||||||
|
signingPublicKey = signingKey
|
||||||
|
)
|
||||||
|
return BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
|
type = MessageType.ANNOUNCE.value,
|
||||||
|
senderID = peerID.hexToBytes(),
|
||||||
|
recipientID = SpecialRecipients.BROADCAST,
|
||||||
|
timestamp = (System.currentTimeMillis() - ageMs).toULong(),
|
||||||
|
payload = announcement.encode()!!,
|
||||||
|
signature = signature,
|
||||||
|
ttl = ttl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.hexToBytes(): ByteArray {
|
||||||
|
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user