mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
631 lines
28 KiB
Kotlin
631 lines
28 KiB
Kotlin
package com.bitchat.android.mesh
|
|
|
|
import android.util.Log
|
|
import com.bitchat.android.model.BitchatMessage
|
|
import com.bitchat.android.model.BitchatMessageType
|
|
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.util.toHexString
|
|
import kotlinx.coroutines.*
|
|
import java.util.*
|
|
import kotlin.random.Random
|
|
|
|
/**
|
|
* Handles processing of different message types
|
|
* Extracted from BluetoothMeshService for better separation of concerns
|
|
*/
|
|
class MessageHandler(private val myPeerID: String, private val appContext: android.content.Context) {
|
|
|
|
companion object {
|
|
private const val TAG = "MessageHandler"
|
|
}
|
|
|
|
// Delegate for callbacks
|
|
var delegate: MessageHandlerDelegate? = null
|
|
|
|
// Reference to PacketProcessor for recursive packet handling
|
|
var packetProcessor: PacketProcessor? = null
|
|
|
|
// Coroutines
|
|
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
|
|
/**
|
|
* Handle Noise encrypted transport message - SIMPLIFIED iOS-compatible version
|
|
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
|
|
*/
|
|
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
|
val packet = routed.packet
|
|
val peerID = routed.peerID ?: "unknown"
|
|
|
|
Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)")
|
|
|
|
// Skip our own messages, except when coming from sync (TTL=SYNC_TTL_HOPS) and not tracked in gossip
|
|
if (peerID == myPeerID) {
|
|
val isSyncTtl = packet.ttl == com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS
|
|
val seenInGossip = try { delegate?.isInGossipSync(packet) } catch (_: Exception) { null }
|
|
if (!(isSyncTtl && (seenInGossip == false))) return
|
|
}
|
|
|
|
// Check if this message is for us
|
|
val recipientID = packet.recipientID?.toHexString()
|
|
if (recipientID != myPeerID) {
|
|
Log.d(TAG, "🔐 Encrypted message not for me (for $recipientID, I am $myPeerID)")
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Decrypt the message using the Noise service
|
|
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
|
if (decryptedData == null) {
|
|
Log.w(TAG, "Failed to decrypt Noise message from $peerID - may need handshake")
|
|
return
|
|
}
|
|
|
|
if (decryptedData.isEmpty()) {
|
|
Log.w(TAG, "Decrypted data is empty from $peerID")
|
|
return
|
|
}
|
|
|
|
// NEW: Use NoisePayload system exactly like iOS
|
|
val noisePayload = com.bitchat.android.model.NoisePayload.decode(decryptedData)
|
|
if (noisePayload == null) {
|
|
Log.w(TAG, "Failed to parse NoisePayload from $peerID")
|
|
return
|
|
}
|
|
|
|
Log.d(TAG, "🔓 Decrypted NoisePayload type ${noisePayload.type} from $peerID")
|
|
|
|
when (noisePayload.type) {
|
|
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
|
// Decode TLV private message exactly like iOS
|
|
val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data)
|
|
if (privateMessage != null) {
|
|
Log.d(TAG, "🔓 Decrypted TLV PM from $peerID: ${privateMessage.content.take(30)}...")
|
|
|
|
// Handle favorite/unfavorite notifications embedded as PMs
|
|
val pmContent = privateMessage.content
|
|
if (pmContent.startsWith("[FAVORITED]") || pmContent.startsWith("[UNFAVORITED]")) {
|
|
handleFavoriteNotificationFromMesh(pmContent, peerID)
|
|
// Acknowledge delivery for UX parity
|
|
sendDeliveryAck(privateMessage.messageID, peerID)
|
|
return
|
|
}
|
|
|
|
// Create BitchatMessage - preserve source packet timestamp
|
|
val message = BitchatMessage(
|
|
id = privateMessage.messageID,
|
|
sender = delegate?.getPeerNickname(peerID) ?: "Unknown",
|
|
content = privateMessage.content,
|
|
timestamp = java.util.Date(packet.timestamp.toLong()),
|
|
isRelay = false,
|
|
originalSender = null,
|
|
isPrivate = true,
|
|
recipientNickname = delegate?.getMyNickname(),
|
|
senderPeerID = peerID,
|
|
mentions = null // TODO: Parse mentions if needed
|
|
)
|
|
|
|
// Notify delegate
|
|
delegate?.onMessageReceived(message)
|
|
|
|
// Send delivery ACK exactly like iOS
|
|
sendDeliveryAck(privateMessage.messageID, peerID)
|
|
}
|
|
}
|
|
|
|
com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER -> {
|
|
// Handle encrypted file transfer; generate unique message ID
|
|
val file = com.bitchat.android.model.BitchatFilePacket.decode(noisePayload.data)
|
|
if (file != null) {
|
|
Log.d(TAG, "🔓 Decrypted encrypted file from $peerID: name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}'")
|
|
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
|
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
|
val message = BitchatMessage(
|
|
id = uniqueMsgId,
|
|
sender = delegate?.getPeerNickname(peerID) ?: "Unknown",
|
|
content = savedPath,
|
|
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
|
timestamp = java.util.Date(packet.timestamp.toLong()),
|
|
isRelay = false,
|
|
isPrivate = true,
|
|
recipientNickname = delegate?.getMyNickname(),
|
|
senderPeerID = peerID
|
|
)
|
|
|
|
Log.d(TAG, "📄 Saved encrypted incoming file to $savedPath (msgId=$uniqueMsgId)")
|
|
delegate?.onMessageReceived(message)
|
|
|
|
// Send delivery ACK with generated message ID
|
|
sendDeliveryAck(uniqueMsgId, peerID)
|
|
} else {
|
|
Log.w(TAG, "⚠️ Failed to decode encrypted file transfer from $peerID")
|
|
}
|
|
}
|
|
|
|
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
|
// Handle delivery ACK exactly like iOS
|
|
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
|
Log.d(TAG, "📬 Delivery ACK received from $peerID for message $messageID")
|
|
|
|
// Simplified: Call delegate with messageID and peerID directly
|
|
delegate?.onDeliveryAckReceived(messageID, peerID)
|
|
}
|
|
|
|
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
|
// Handle read receipt exactly like iOS
|
|
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
|
Log.d(TAG, "👁️ Read receipt received from $peerID for message $messageID")
|
|
|
|
// Simplified: Call delegate with messageID and peerID directly
|
|
delegate?.onReadReceiptReceived(messageID, peerID)
|
|
}
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Error processing Noise encrypted message from $peerID: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send delivery ACK for a received private message - exactly like iOS
|
|
*/
|
|
private suspend fun sendDeliveryAck(messageID: String, senderPeerID: String) {
|
|
try {
|
|
// Create ACK payload: [type byte] + [message ID] - exactly like iOS
|
|
val ackPayload = com.bitchat.android.model.NoisePayload(
|
|
type = com.bitchat.android.model.NoisePayloadType.DELIVERED,
|
|
data = messageID.toByteArray(Charsets.UTF_8)
|
|
)
|
|
|
|
// Encrypt the payload
|
|
val encryptedPayload = delegate?.encryptForPeer(ackPayload.encode(), senderPeerID)
|
|
if (encryptedPayload == null) {
|
|
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
|
|
return
|
|
}
|
|
|
|
// Create NOISE_ENCRYPTED packet exactly like iOS
|
|
val packet = BitchatPacket(
|
|
version = 1u,
|
|
type = MessageType.NOISE_ENCRYPTED.value,
|
|
senderID = hexStringToByteArray(myPeerID),
|
|
recipientID = hexStringToByteArray(senderPeerID),
|
|
timestamp = System.currentTimeMillis().toULong(),
|
|
payload = encryptedPayload,
|
|
signature = null,
|
|
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS // Same TTL as iOS messageTTL
|
|
)
|
|
|
|
delegate?.sendPacket(packet)
|
|
Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID")
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle announce message with TLV decoding and signature verification - exactly like iOS
|
|
*/
|
|
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
|
val packet = routed.packet
|
|
val peerID = routed.peerID ?: "unknown"
|
|
|
|
// Skip our own announce, except when coming from sync (TTL=SYNC_TTL_HOPS) and not tracked in gossip
|
|
if (peerID == myPeerID) {
|
|
val isSyncTtl = packet.ttl == com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS
|
|
val seenInGossip = try { delegate?.isInGossipSync(packet) } catch (_: Exception) { null }
|
|
if (!(isSyncTtl && (seenInGossip == false))) return false
|
|
}
|
|
|
|
// Ignore stale announcements older than STALE_PEER_TIMEOUT
|
|
val now = System.currentTimeMillis()
|
|
val age = now - packet.timestamp.toLong()
|
|
if (age > com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS) {
|
|
Log.w(TAG, "Ignoring stale ANNOUNCE from ${peerID.take(8)} (age=${age}ms > ${com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS}ms)")
|
|
return false
|
|
}
|
|
|
|
// Try to decode as iOS-compatible IdentityAnnouncement with TLV format
|
|
val announcement = IdentityAnnouncement.decode(packet.payload)
|
|
if (announcement == null) {
|
|
Log.w(TAG, "Failed to decode announce from $peerID as iOS-compatible TLV format")
|
|
return false
|
|
}
|
|
|
|
// Verify packet signature using the announced signing public key
|
|
var verified = false
|
|
if (packet.signature != null) {
|
|
// Verify that the packet was signed by the signing private key corresponding to the announced signing public key
|
|
verified = delegate?.verifyEd25519Signature(packet.signature!!, packet.toBinaryDataForSigning()!!, announcement.signingPublicKey) ?: false
|
|
if (!verified) {
|
|
Log.w(TAG, "⚠️ Signature verification for announce failed ${peerID.take(8)}")
|
|
}
|
|
}
|
|
|
|
// Check for existing peer with different noise public key
|
|
// If existing peer has a different noise public key, do not consider this verified
|
|
val existingPeer = delegate?.getPeerInfo(peerID)
|
|
|
|
if (existingPeer != null && existingPeer.noisePublicKey != null && !existingPeer.noisePublicKey!!.contentEquals(announcement.noisePublicKey)) {
|
|
Log.w(TAG, "⚠️ Announce key mismatch for ${peerID.take(8)}... — keeping unverified")
|
|
verified = false
|
|
}
|
|
|
|
// Require verified announce; ignore otherwise (no backward compatibility)
|
|
if (!verified) {
|
|
Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...")
|
|
return false
|
|
}
|
|
|
|
// Successfully decoded TLV format exactly like iOS
|
|
Log.d(TAG, "✅ Verified announce from $peerID: nickname=${announcement.nickname}, " +
|
|
"noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " +
|
|
"signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
|
|
|
|
// Extract nickname and public keys from TLV data
|
|
val nickname = announcement.nickname
|
|
val noisePublicKey = announcement.noisePublicKey
|
|
val signingPublicKey = announcement.signingPublicKey
|
|
|
|
// Update peer info with verification status through new method
|
|
val isFirstAnnounce = delegate?.updatePeerInfo(
|
|
peerID = peerID,
|
|
nickname = nickname,
|
|
noisePublicKey = noisePublicKey,
|
|
signingPublicKey = signingPublicKey,
|
|
isVerified = true
|
|
) ?: false
|
|
|
|
// Update peer ID binding with noise public key for identity management
|
|
delegate?.updatePeerIDBinding(
|
|
newPeerID = peerID,
|
|
nickname = nickname,
|
|
publicKey = noisePublicKey,
|
|
previousPeerID = null
|
|
)
|
|
|
|
Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID")
|
|
return isFirstAnnounce
|
|
}
|
|
|
|
/**
|
|
* Handle Noise handshake - SIMPLIFIED iOS-compatible version
|
|
* Single handshake type (0x10) with response determined by payload analysis
|
|
*/
|
|
suspend fun handleNoiseHandshake(routed: RoutedPacket) {
|
|
val packet = routed.packet
|
|
val peerID = routed.peerID ?: "unknown"
|
|
|
|
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
|
|
|
// Skip our own handshake messages
|
|
if (peerID == myPeerID) return
|
|
|
|
// Check if handshake is addressed to us
|
|
val recipientID = packet.recipientID?.toHexString()
|
|
if (recipientID != myPeerID) {
|
|
Log.d(TAG, "Handshake not for me (for $recipientID, I am $myPeerID)")
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Process handshake message through delegate (simplified approach)
|
|
val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID)
|
|
|
|
if (response != null) {
|
|
Log.d(TAG, "Generated handshake response for $peerID (${response.size} bytes)")
|
|
|
|
// Send response using same packet type (simplified iOS approach)
|
|
val responsePacket = BitchatPacket(
|
|
version = 1u,
|
|
type = MessageType.NOISE_HANDSHAKE.value,
|
|
senderID = hexStringToByteArray(myPeerID),
|
|
recipientID = hexStringToByteArray(peerID),
|
|
timestamp = System.currentTimeMillis().toULong(),
|
|
payload = response,
|
|
signature = null,
|
|
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS // Same TTL as iOS
|
|
)
|
|
|
|
delegate?.sendPacket(responsePacket)
|
|
Log.d(TAG, "📤 Sent handshake response to $peerID")
|
|
}
|
|
|
|
// Check if session is now established
|
|
val hasSession = delegate?.hasNoiseSession(peerID) ?: false
|
|
if (hasSession) {
|
|
Log.d(TAG, "✅ Noise session established with $peerID")
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle broadcast or private message
|
|
*/
|
|
suspend fun handleMessage(routed: RoutedPacket) {
|
|
val packet = routed.packet
|
|
val peerID = routed.peerID ?: "unknown"
|
|
if (peerID == myPeerID) {
|
|
val isSyncTtl = packet.ttl == com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS
|
|
val seenInGossip = try { delegate?.isInGossipSync(packet) } catch (_: Exception) { null }
|
|
if (!(isSyncTtl && (seenInGossip == false))) return
|
|
}
|
|
val senderNickname = delegate?.getPeerNickname(peerID)
|
|
if (senderNickname != null) {
|
|
Log.d(TAG, "Received message from $senderNickname")
|
|
delegate?.updatePeerNickname(peerID, senderNickname)
|
|
}
|
|
|
|
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
|
|
|
if (recipientID == null) {
|
|
// BROADCAST MESSAGE
|
|
handleBroadcastMessage(routed)
|
|
} else if (recipientID.toHexString() == myPeerID) {
|
|
// PRIVATE MESSAGE FOR US
|
|
handlePrivateMessage(packet, peerID)
|
|
}
|
|
// Message relay is now handled by centralized PacketRelayManager
|
|
}
|
|
|
|
/**
|
|
* Handle broadcast message with verification enforcement
|
|
*/
|
|
private suspend fun handleBroadcastMessage(routed: RoutedPacket) {
|
|
val packet = routed.packet
|
|
val peerID = routed.peerID ?: "unknown"
|
|
|
|
// Enforce: only accept public messages from verified peers we know
|
|
val peerInfo = delegate?.getPeerInfo(peerID)
|
|
if (peerInfo == null || !peerInfo.isVerifiedNickname) {
|
|
Log.w(TAG, "🚫 Dropping public message from unverified or unknown peer ${peerID.take(8)}...")
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER
|
|
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
|
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
|
if (file != null) {
|
|
if (isFileTransfer) {
|
|
Log.d(TAG, "📥 FILE_TRANSFER decode success (broadcast): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
|
}
|
|
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
|
val message = BitchatMessage(
|
|
id = java.util.UUID.randomUUID().toString().uppercase(),
|
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
|
content = savedPath,
|
|
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
|
senderPeerID = peerID,
|
|
timestamp = Date(packet.timestamp.toLong())
|
|
)
|
|
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
|
delegate?.onMessageReceived(message)
|
|
return
|
|
} else if (isFileTransfer) {
|
|
Log.w(TAG, "⚠️ FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)} payloadSize=${packet.payload.size}")
|
|
}
|
|
|
|
// Fallback: plain text
|
|
val message = BitchatMessage(
|
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
|
content = String(packet.payload, Charsets.UTF_8),
|
|
senderPeerID = peerID,
|
|
timestamp = Date(packet.timestamp.toLong())
|
|
)
|
|
delegate?.onMessageReceived(message)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to process broadcast message: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle (decrypted) private message addressed to us
|
|
*/
|
|
private suspend fun handlePrivateMessage(packet: BitchatPacket, peerID: String) {
|
|
try {
|
|
// Verify signature if present
|
|
if (packet.signature != null && !delegate?.verifySignature(packet, peerID)!!) {
|
|
Log.w(TAG, "Invalid signature for private message from $peerID")
|
|
return
|
|
}
|
|
|
|
// Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER
|
|
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
|
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
|
if (file != null) {
|
|
if (isFileTransfer) {
|
|
Log.d(TAG, "📥 FILE_TRANSFER decode success (private): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
|
}
|
|
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
|
val message = BitchatMessage(
|
|
id = java.util.UUID.randomUUID().toString().uppercase(),
|
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
|
content = savedPath,
|
|
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
|
senderPeerID = peerID,
|
|
timestamp = Date(packet.timestamp.toLong()),
|
|
isPrivate = true,
|
|
recipientNickname = delegate?.getMyNickname()
|
|
)
|
|
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
|
delegate?.onMessageReceived(message)
|
|
return
|
|
} else if (isFileTransfer) {
|
|
Log.w(TAG, "⚠️ FILE_TRANSFER decode failed (private) from ${peerID.take(8)} payloadSize=${packet.payload.size}")
|
|
}
|
|
|
|
// Fallback: plain text
|
|
val message = BitchatMessage(
|
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
|
content = String(packet.payload, Charsets.UTF_8),
|
|
senderPeerID = peerID,
|
|
timestamp = Date(packet.timestamp.toLong())
|
|
)
|
|
delegate?.onMessageReceived(message)
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to process private message from $peerID: ${e.message}")
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Handle leave message
|
|
*/
|
|
suspend fun handleLeave(routed: RoutedPacket) {
|
|
val packet = routed.packet
|
|
val peerID = routed.peerID ?: "unknown"
|
|
val content = String(packet.payload, Charsets.UTF_8)
|
|
|
|
if (content.startsWith("#")) {
|
|
// Channel leave
|
|
delegate?.onChannelLeave(content, peerID)
|
|
} else {
|
|
// Peer disconnect
|
|
delegate?.removePeer(peerID)
|
|
}
|
|
|
|
// Leave message relay is now handled by centralized PacketRelayManager
|
|
}
|
|
|
|
/**
|
|
* Get debug information
|
|
*/
|
|
fun getDebugInfo(): String {
|
|
return buildString {
|
|
appendLine("=== Message Handler Debug Info ===")
|
|
appendLine("Handler Scope Active: ${handlerScope.isActive}")
|
|
appendLine("My Peer ID: $myPeerID")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert hex string peer ID to binary data (8 bytes) - same as iOS implementation
|
|
*/
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Shutdown the handler
|
|
*/
|
|
fun shutdown() {
|
|
handlerScope.cancel()
|
|
}
|
|
|
|
/**
|
|
* Handle favorite/unfavorite notification received over mesh as a private message.
|
|
* Content format: "[FAVORITED]:npub..." or "[UNFAVORITED]:npub..."
|
|
*/
|
|
private fun handleFavoriteNotificationFromMesh(content: String, fromPeerID: String) {
|
|
try {
|
|
val isFavorite = content.startsWith("[FAVORITED]")
|
|
val npub = content.substringAfter(":", "").trim().takeIf { it.startsWith("npub1") }
|
|
|
|
// Update mutual favorite status in persistence
|
|
// Resolve full Noise key if available via delegate peer info
|
|
val peerInfo = delegate?.getPeerInfo(fromPeerID)
|
|
val noiseKey = peerInfo?.noisePublicKey
|
|
if (noiseKey != null) {
|
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared.updatePeerFavoritedUs(noiseKey, isFavorite)
|
|
if (npub != null) {
|
|
// Index by noise key and current mesh peerID for fast Nostr routing
|
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateNostrPublicKey(noiseKey, npub)
|
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateNostrPublicKeyForPeerID(fromPeerID, npub)
|
|
}
|
|
|
|
// Determine iOS-style guidance text
|
|
val rel = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey)
|
|
val guidance = if (isFavorite) {
|
|
if (rel?.isFavorite == true) {
|
|
" — mutual! You can continue DMs via Nostr when out of mesh."
|
|
} else {
|
|
" — favorite back to continue DMs later."
|
|
}
|
|
} else {
|
|
". DMs over Nostr will pause unless you both favorite again."
|
|
}
|
|
|
|
// Emit system message via delegate callback
|
|
val action = if (isFavorite) "favorited" else "unfavorited"
|
|
val sys = com.bitchat.android.model.BitchatMessage(
|
|
sender = "system",
|
|
content = "${peerInfo.nickname} $action you$guidance",
|
|
timestamp = java.util.Date(),
|
|
isRelay = false
|
|
)
|
|
delegate?.onMessageReceived(sys)
|
|
}
|
|
} catch (_: Exception) {
|
|
// Best-effort; ignore errors
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delegate interface for message handler callbacks
|
|
*/
|
|
interface MessageHandlerDelegate {
|
|
// Peer management
|
|
fun addOrUpdatePeer(peerID: String, nickname: String): Boolean
|
|
fun removePeer(peerID: String)
|
|
fun updatePeerNickname(peerID: String, nickname: String)
|
|
fun getPeerNickname(peerID: String): String?
|
|
fun getNetworkSize(): Int
|
|
fun getMyNickname(): String?
|
|
fun getPeerInfo(peerID: String): PeerInfo?
|
|
fun updatePeerInfo(peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean): Boolean
|
|
|
|
// Packet operations
|
|
fun sendPacket(packet: BitchatPacket)
|
|
fun relayPacket(routed: RoutedPacket)
|
|
fun getBroadcastRecipient(): ByteArray
|
|
|
|
// Cryptographic operations
|
|
fun verifySignature(packet: BitchatPacket, peerID: String): Boolean
|
|
fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray?
|
|
fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray?
|
|
fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean
|
|
|
|
// Noise protocol operations
|
|
fun hasNoiseSession(peerID: String): Boolean
|
|
fun initiateNoiseHandshake(peerID: String)
|
|
fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray?
|
|
fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
|
publicKey: ByteArray, previousPeerID: String?)
|
|
|
|
// Message operations
|
|
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
|
|
|
// Callbacks
|
|
fun onMessageReceived(message: BitchatMessage)
|
|
fun onChannelLeave(channel: String, fromPeer: String)
|
|
fun onDeliveryAckReceived(messageID: String, peerID: String)
|
|
fun onReadReceiptReceived(messageID: String, peerID: String)
|
|
|
|
// Gossip sync visibility
|
|
fun isInGossipSync(packet: BitchatPacket): Boolean
|
|
}
|