diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 700655fe8..f53fd09cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,7 +15,6 @@ android { targetSdk = libs.versions.targetSdk.get().toInt() versionCode = 33 versionName = "1.7.2" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -129,6 +128,7 @@ dependencies { // WebSocket implementation(libs.okhttp) + implementation("net.java.dev.jna:jna:5.13.0@aar") // Arti (Tor in Rust) Android bridge - custom build from latest source // Built with rustls, 16KB page size support, and onio//un service client diff --git a/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt b/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt index 12cd3e9f4..cd1eea137 100644 --- a/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt +++ b/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt @@ -15,6 +15,7 @@ import java.util.* data class FavoriteRelationship( val peerNoisePublicKey: ByteArray, // Noise static public key (32 bytes) val peerNostrPublicKey: String?, // npub bech32 string + val peerNdrSessionPubkeyHex: String? = null, // Session lookup key used by nostr-double-ratchet val peerNickname: String, val isFavorite: Boolean, // We favorited them val theyFavoritedUs: Boolean, // They favorited us @@ -31,6 +32,7 @@ data class FavoriteRelationship( if (!peerNoisePublicKey.contentEquals(other.peerNoisePublicKey)) return false if (peerNostrPublicKey != other.peerNostrPublicKey) return false + if (peerNdrSessionPubkeyHex != other.peerNdrSessionPubkeyHex) return false if (peerNickname != other.peerNickname) return false if (isFavorite != other.isFavorite) return false if (theyFavoritedUs != other.theyFavoritedUs) return false @@ -41,6 +43,7 @@ data class FavoriteRelationship( override fun hashCode(): Int { var result = peerNoisePublicKey.contentHashCode() result = 31 * result + (peerNostrPublicKey?.hashCode() ?: 0) + result = 31 * result + (peerNdrSessionPubkeyHex?.hashCode() ?: 0) result = 31 * result + peerNickname.hashCode() result = 31 * result + isFavorite.hashCode() result = 31 * result + theyFavoritedUs.hashCode() @@ -124,6 +127,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte val relationship = FavoriteRelationship( peerNoisePublicKey = noisePublicKey, peerNostrPublicKey = nostrPubkey, + peerNdrSessionPubkeyHex = null, peerNickname = "Unknown", isFavorite = false, theyFavoritedUs = false, @@ -166,7 +170,10 @@ class FavoritesPersistenceService private constructor(private val context: Conte val targetHex = normalizeNostrKeyToHex(nostrPubkey) if (targetHex != null) { // Find relationship with matching nostr pubkey (normalized to hex) and then try to map to current peerID via noise key prefix - val rel = favorites.values.firstOrNull { it.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex } + val rel = favorites.values.firstOrNull { + it.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex || + it.peerNdrSessionPubkeyHex == targetHex + } if (rel != null) { val noiseHex = rel.peerNoisePublicKey.joinToString("") { "%02x".format(it) } // Return 16-hex prefix as best-effort if no explicit mapping exists @@ -193,6 +200,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte FavoriteRelationship( peerNoisePublicKey = noisePublicKey, peerNostrPublicKey = null, + peerNdrSessionPubkeyHex = null, peerNickname = nickname, isFavorite = isFavorite, theyFavoritedUs = false, @@ -242,7 +250,8 @@ class FavoritesPersistenceService private constructor(private val context: Conte fun findNoiseKey(forNostrPubkey: String): ByteArray? { val targetHex = normalizeNostrKeyToHex(forNostrPubkey) ?: return null return favorites.values.firstOrNull { rel -> - rel.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex + rel.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex || + rel.peerNdrSessionPubkeyHex == targetHex }?.peerNoisePublicKey } @@ -252,6 +261,30 @@ class FavoritesPersistenceService private constructor(private val context: Conte return favorites[keyHex]?.peerNostrPublicKey } + /** Update the session lookup key used by nostr-double-ratchet for a peer. */ + fun updateNdrSessionPubkeyHex(noisePublicKey: ByteArray, peerPubkeyHex: String) { + val normalized = normalizeNostrKeyToHex(peerPubkeyHex) ?: return + val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) } + val existing = favorites[keyHex] ?: return + if (existing.peerNdrSessionPubkeyHex == normalized) return + + favorites[keyHex] = existing.copy( + peerNdrSessionPubkeyHex = normalized, + lastUpdated = Date() + ) + saveFavorites() + notifyChanged(keyHex) + Log.d(TAG, "Updated NDR session pubkey for ${keyHex.take(16)}... -> ${normalized.take(16)}...") + } + + /** Resolve the best lookup key for NDR session status/sending for a given peer. */ + fun findNdrSessionPubkeyHex(forNoiseKey: ByteArray): String? { + val keyHex = forNoiseKey.joinToString("") { "%02x".format(it) } + val relationship = favorites[keyHex] ?: return null + return relationship.peerNdrSessionPubkeyHex + ?: relationship.peerNostrPublicKey?.let(::normalizeNostrKeyToHex) + } + // MARK: - Persistence private fun loadFavorites() { @@ -339,6 +372,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte private data class FavoriteRelationshipData( val peerNoisePublicKeyHex: String, val peerNostrPublicKey: String?, + val peerNdrSessionPubkeyHex: String? = null, val peerNickname: String, val isFavorite: Boolean, val theyFavoritedUs: Boolean, @@ -350,6 +384,7 @@ private data class FavoriteRelationshipData( return FavoriteRelationshipData( peerNoisePublicKeyHex = relationship.peerNoisePublicKey.joinToString("") { "%02x".format(it) }, peerNostrPublicKey = relationship.peerNostrPublicKey, + peerNdrSessionPubkeyHex = relationship.peerNdrSessionPubkeyHex, peerNickname = relationship.peerNickname, isFavorite = relationship.isFavorite, theyFavoritedUs = relationship.theyFavoritedUs, @@ -364,6 +399,7 @@ private data class FavoriteRelationshipData( return FavoriteRelationship( peerNoisePublicKey = noiseKeyBytes, peerNostrPublicKey = peerNostrPublicKey, + peerNdrSessionPubkeyHex = peerNdrSessionPubkeyHex, peerNickname = peerNickname, isFavorite = isFavorite, theyFavoritedUs = theyFavoritedUs, diff --git a/app/src/main/java/com/bitchat/android/mesh/BlePacketBudget.kt b/app/src/main/java/com/bitchat/android/mesh/BlePacketBudget.kt new file mode 100644 index 000000000..23f72c872 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BlePacketBudget.kt @@ -0,0 +1,13 @@ +package com.bitchat.android.mesh + +object BlePacketBudget { + private const val ATT_PAYLOAD_OVERHEAD_BYTES = 3 + private const val DEFAULT_PACKET_LIMIT_BYTES = 182 + private const val MIN_PACKET_LIMIT_BYTES = 20 + + fun packetLimitBytesForMtu(mtu: Int?): Int { + val payloadBytes = (mtu ?: (DEFAULT_PACKET_LIMIT_BYTES + ATT_PAYLOAD_OVERHEAD_BYTES)) - + ATT_PAYLOAD_OVERHEAD_BYTES + return payloadBytes.coerceAtLeast(MIN_PACKET_LIMIT_BYTES) + } +} diff --git a/app/src/main/java/com/bitchat/android/mesh/BleWriteAccumulator.kt b/app/src/main/java/com/bitchat/android/mesh/BleWriteAccumulator.kt new file mode 100644 index 000000000..419fe0d3b --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BleWriteAccumulator.kt @@ -0,0 +1,94 @@ +package com.bitchat.android.mesh + +import com.bitchat.android.protocol.BitchatPacket +import java.util.concurrent.ConcurrentHashMap + +/** + * Reassembles characteristic writes that arrive in multiple offset chunks. + * + * CoreBluetooth may split a single packet across multiple writes when acting as + * the central. Android's GATT server callback receives those chunks one by one, + * so we keep a per-device sparse buffer and only hand the packet upstream once + * the accumulated bytes decode successfully. + */ +class BleWriteAccumulator { + + private data class PendingWrite( + val buffer: ByteArray, + val receivedRanges: List + ) + + private val pendingWrites = ConcurrentHashMap() + + fun append(deviceAddress: String, offset: Int, chunk: ByteArray): BitchatPacket? { + if (chunk.isEmpty()) { + return null + } + + val current = pendingWrites[deviceAddress] + val existing = if (offset == 0 && current?.receivedRanges?.any { it.first == 0 } == true) { + null + } else { + current + } + val end = offset + chunk.size + val existingBuffer = existing?.buffer ?: ByteArray(0) + val combined = if (existingBuffer.size >= end) { + existingBuffer.copyOf() + } else { + existingBuffer.copyOf(end) + } + chunk.copyInto(combined, destinationOffset = offset) + val mergedRanges = mergeRanges(existing?.receivedRanges.orEmpty(), IntRange(offset, end - 1)) + val pendingWrite = PendingWrite(combined, mergedRanges) + pendingWrites[deviceAddress] = pendingWrite + + if (!isContiguousFromStart(pendingWrite)) { + return null + } + + val packet = BitchatPacket.fromBinaryData(combined) ?: return null + val canonicalEncoding = packet.toBinaryData() ?: return null + if (!canonicalEncoding.contentEquals(combined)) { + return null + } + pendingWrites.remove(deviceAddress) + return packet + } + + fun clear(deviceAddress: String) { + pendingWrites.remove(deviceAddress) + } + + fun clearAll() { + pendingWrites.clear() + } + + private fun mergeRanges(existing: List, next: IntRange): List { + val sorted = buildList { + addAll(existing) + add(next) + }.sortedBy { it.first } + if (sorted.isEmpty()) { + return emptyList() + } + + val merged = mutableListOf() + var current = sorted.first() + for (candidate in sorted.drop(1)) { + current = if (candidate.first <= current.last + 1) { + current.first..maxOf(current.last, candidate.last) + } else { + merged.add(current) + candidate + } + } + merged.add(current) + return merged + } + + private fun isContiguousFromStart(pendingWrite: PendingWrite): Boolean { + val onlyRange = pendingWrite.receivedRanges.singleOrNull() ?: return false + return onlyRange.first == 0 && onlyRange.last + 1 == pendingWrite.buffer.size + } +} diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt index 681185d56..b899aeba0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -4,10 +4,12 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.util.Log +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.CopyOnWriteArrayList /** @@ -30,6 +32,9 @@ class BluetoothConnectionTracker( private val connectedDevices = ConcurrentHashMap() private val subscribedDevices = CopyOnWriteArrayList() val addressPeerMap = ConcurrentHashMap() + private val deviceMtus = ConcurrentHashMap() + private val pendingNotificationAcks = + ConcurrentHashMap>>() // RSSI tracking from scan results (for devices we discover but may connect as servers) private val scanRSSI = ConcurrentHashMap() @@ -232,6 +237,56 @@ class BluetoothConnectionTracker( * Get connected device count */ fun getConnectedDeviceCount(): Int = connectedDevices.size + + fun updateDeviceMtu(deviceAddress: String, mtu: Int) { + if (mtu > 0) { + deviceMtus[deviceAddress] = mtu + } + } + + fun enqueueNotificationAck(deviceAddress: String): CompletableDeferred { + val deferred = CompletableDeferred() + pendingNotificationAcks + .getOrPut(deviceAddress) { ConcurrentLinkedQueue() } + .add(deferred) + return deferred + } + + fun completeNotificationAck(deviceAddress: String, status: Int) { + val queue = pendingNotificationAcks[deviceAddress] ?: return + while (true) { + val deferred = queue.poll() ?: break + if (deferred.complete(status)) { + break + } + } + if (queue.isEmpty()) { + pendingNotificationAcks.remove(deviceAddress, queue) + } + } + + fun cancelNotificationAck( + deviceAddress: String, + deferred: CompletableDeferred, + removeImmediately: Boolean = false + ) { + deferred.cancel() + val queue = pendingNotificationAcks[deviceAddress] ?: return + if (removeImmediately) { + queue.remove(deferred) + } + if (queue.isEmpty()) { + pendingNotificationAcks.remove(deviceAddress, queue) + } + } + + fun clearNotificationAcks(deviceAddress: String) { + pendingNotificationAcks.remove(deviceAddress)?.forEach { it.cancel() } + } + + fun getDevicePacketLimit(deviceAddress: String): Int { + return BlePacketBudget.packetLimitBytesForMtu(deviceMtus[deviceAddress]) + } /** * Check if connection limit is reached @@ -301,6 +356,8 @@ class BluetoothConnectionTracker( subscribedDevices.removeAll { it.address == deviceAddress } addressPeerMap.remove(deviceAddress) } + deviceMtus.remove(deviceAddress) + clearNotificationAcks(deviceAddress) Log.d(TAG, "Cleaned up device connection for $deviceAddress") } @@ -332,6 +389,9 @@ class BluetoothConnectionTracker( connectedDevices.clear() subscribedDevices.clear() addressPeerMap.clear() + deviceMtus.clear() + pendingNotificationAcks.values.forEach { queue -> queue.forEach { it.cancel() } } + pendingNotificationAcks.clear() pendingConnections.clear() scanRSSI.clear() } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index 2bb22fce5..5f867db4d 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -448,6 +448,7 @@ class BluetoothGattClientManager( Log.i(TAG, "Client: MTU changed for $deviceAddress to $mtu with status $status") if (status == BluetoothGatt.GATT_SUCCESS) { + connectionTracker.updateDeviceMtu(deviceAddress, mtu) Log.i(TAG, "MTU successfully negotiated for $deviceAddress. Discovering services.") // Now that MTU is set, connection is fully ready. diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index ad7c9cf12..9d3038333 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -8,7 +8,6 @@ import android.bluetooth.le.BluetoothLeAdvertiser import android.content.Context import android.os.ParcelUuid import android.util.Log -import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.util.AppConstants import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -45,6 +44,7 @@ class BluetoothGattServerManager( // State management private var isActive = false + private val writeAccumulator = BleWriteAccumulator() /** * Disconnect a specific device (used by ConnectionManager to enforce overall limits) @@ -109,6 +109,7 @@ class BluetoothGattServerManager( // Ensure server is closed if present gattServer?.close() gattServer = null + writeAccumulator.clearAll() Log.i(TAG, "GATT server stopped (already inactive)") return } @@ -130,6 +131,7 @@ class BluetoothGattServerManager( // Close GATT server gattServer?.close() gattServer = null + writeAccumulator.clearAll() Log.i(TAG, "GATT server stopped") } @@ -183,6 +185,7 @@ class BluetoothGattServerManager( } BluetoothProfile.STATE_DISCONNECTED -> { Log.i(TAG, "Server: Device disconnected ${device.address}") + writeAccumulator.clear(device.address) connectionTracker.cleanupDeviceConnection(device.address) // Notify delegate about device disconnection so higher layers can update direct flags delegate?.onDeviceDisconnected(device) @@ -220,15 +223,20 @@ class BluetoothGattServerManager( } if (characteristic.uuid == AppConstants.Mesh.Gatt.CHARACTERISTIC_UUID) { - Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") - val packet = BitchatPacket.fromBinaryData(value) + Log.i( + TAG, + "Server: Received write from ${device.address}, size=${value.size} bytes offset=$offset prepared=$preparedWrite" + ) + val packet = writeAccumulator.append(device.address, offset, value) if (packet != null) { val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, device) } else { - Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") - Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") + Log.d( + TAG, + "Server: Buffered partial write from ${device.address}, size=${value.size} bytes offset=$offset" + ) } if (responseNeeded) { @@ -268,6 +276,25 @@ class BluetoothGattServerManager( gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) } } + + override fun onMtuChanged(device: BluetoothDevice, mtu: Int) { + if (!isActive) { + Log.d(TAG, "Server: Ignoring MTU update after shutdown") + return + } + + Log.i(TAG, "Server: MTU changed for ${device.address} to $mtu") + connectionTracker.updateDeviceMtu(device.address, mtu) + } + + override fun onNotificationSent(device: BluetoothDevice, status: Int) { + connectionTracker.completeNotificationAck(device.address, status) + if (!isActive) { + Log.d(TAG, "Server: Notification callback after shutdown for ${device.address} status=$status") + return + } + Log.d(TAG, "Server: Notification delivered to ${device.address} with status $status") + } } // Proper cleanup sequencing to prevent race conditions diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 474c04a5a..32990e62a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -39,6 +39,7 @@ class BluetoothMeshService(private val context: Context) { companion object { private const val TAG = "BluetoothMeshService" + private const val HANDSHAKE_INIT_DELAY_MS = 300L private val MAX_TTL: UByte = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS } @@ -72,6 +73,13 @@ class BluetoothMeshService(private val context: Context) { private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // Tracks whether this instance has been terminated via stopServices() private var terminated = false + private val pendingPrivateMessagesLock = Any() + private val pendingPrivateMessages = mutableMapOf>() + + private data class PendingPrivateMessage( + val content: String, + val messageID: String + ) init { Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID") @@ -194,14 +202,24 @@ class BluetoothMeshService(private val context: Context) { } } + // Replay encrypted packets that arrived before the final handshake packet. + encryptionService.onSessionEstablished = { peerID -> + serviceScope.launch { + messageHandler.flushPendingNoiseEncrypted(peerID) + flushPendingPrivateMessages(peerID) + } + } + // SecurityManager delegate for key exchange notifications securityManager.delegate = object : SecurityManagerDelegate { override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { // Send announcement and cached messages after key exchange serviceScope.launch { Log.d(TAG, "Key exchange completed with $peerID; sending follow-ups") + messageHandler.flushPendingNoiseEncrypted(peerID) delay(100) sendAnnouncementToPeer(peerID) + flushPendingPrivateMessages(peerID) delay(1000) storeForwardManager.sendCachedMessages(peerID) @@ -438,6 +456,15 @@ class BluetoothMeshService(private val context: Context) { override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) { delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs) } + + override fun onNdrEventReceived(peerID: String, payload: ByteArray, timestampMs: Long) { + val currentDelegate = delegate + if (currentDelegate != null) { + currentDelegate.didReceiveNdrEvent(peerID, payload, timestampMs) + } else { + handleNdrEventWithoutUiDelegate(peerID, payload) + } + } } // PacketProcessor delegates @@ -785,10 +812,6 @@ class BluetoothMeshService(private val context: Context) { // Encrypt the payload using Noise val encrypted = encryptionService.encrypt(noisePayload.encode(), recipientPeerID) - if (encrypted == null) { - Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID") - return@launch - } Log.d(TAG, "🔐 Encrypted file payload: ${encrypted.size} bytes") // Create NOISE_ENCRYPTED packet (not FILE_TRANSFER!) @@ -898,15 +921,73 @@ class BluetoothMeshService(private val context: Context) { Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}") } } else { - // Fire and forget - initiate handshake but don't queue exactly like iOS - Log.d(TAG, "🤝 No session with $recipientPeerID, initiating handshake") - messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) + val sessionState = encryptionService.getSessionState(recipientPeerID) + queuePrivateMessage(recipientPeerID, content, finalMessageID) + when (sessionState) { + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Handshaking -> { + Log.d(TAG, "🤝 Handshake already in progress with $recipientPeerID; queued PM behind it") + } + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Uninitialized, + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Failed -> { + Log.d(TAG, "🤝 No established session with $recipientPeerID, scheduling handshake") + scheduleHandshakeIfNeeded(recipientPeerID) + } + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Established -> { + Log.d(TAG, "🤝 Session became established while queueing PM for $recipientPeerID") + flushPendingPrivateMessages(recipientPeerID) + } + } // FIXED: Don't send didReceiveMessage for our own sent messages // The UI will handle showing the message in the chat interface } } } + + private fun scheduleHandshakeIfNeeded(recipientPeerID: String) { + serviceScope.launch { + delay(HANDSHAKE_INIT_DELAY_MS) + when (encryptionService.getSessionState(recipientPeerID)) { + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Handshaking -> { + Log.d(TAG, "🤝 Handshake started by peer with $recipientPeerID; not sending competing init") + } + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Established -> { + Log.d(TAG, "🤝 Session established before delayed init for $recipientPeerID") + flushPendingPrivateMessages(recipientPeerID) + } + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Uninitialized, + is com.bitchat.android.noise.NoiseSession.NoiseSessionState.Failed -> { + Log.d(TAG, "🤝 Delayed handshake init with $recipientPeerID") + messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) + } + } + } + } + + private fun queuePrivateMessage(recipientPeerID: String, content: String, messageID: String) { + synchronized(pendingPrivateMessagesLock) { + val queue = pendingPrivateMessages.getOrPut(recipientPeerID) { mutableListOf() } + queue += PendingPrivateMessage(content = content, messageID = messageID) + Log.d(TAG, "🕒 Queued PM for $recipientPeerID until handshake completes (pending=${queue.size})") + } + } + + private suspend fun flushPendingPrivateMessages(recipientPeerID: String) { + val queued = synchronized(pendingPrivateMessagesLock) { + pendingPrivateMessages.remove(recipientPeerID)?.toList().orEmpty() + } + if (queued.isEmpty()) return + + Log.d(TAG, "📤 Flushing ${queued.size} queued PM(s) for $recipientPeerID after handshake") + queued.forEach { pending -> + sendPrivateMessage( + content = pending.content, + recipientPeerID = recipientPeerID, + recipientNickname = peerManager.getPeerNickname(recipientPeerID) ?: recipientPeerID, + messageID = pending.messageID + ) + } + } /** * Send read receipt for a received private message - NEW NoisePayloadType implementation @@ -990,6 +1071,54 @@ class BluetoothMeshService(private val context: Context) { sendNoisePayloadToPeer(payload, peerID, "verify response") } + fun sendNdrEvent(peerID: String, eventJson: String) { + val data = eventJson.toByteArray(Charsets.UTF_8) + if (data.isEmpty()) return + val payload = NoisePayload( + type = NoisePayloadType.NDR_EVENT, + data = data + ) + sendNoisePayloadToPeer(payload, peerID, "ndr event") + } + + private fun handleNdrEventWithoutUiDelegate(peerID: String, payload: ByteArray) { + val eventJson = payload.toString(Charsets.UTF_8).takeIf { it.isNotBlank() } ?: return + val peerInfo = getPeerInfo(peerID) ?: run { + Log.d(TAG, "Dropping background NDR event from $peerID: no peer info") + return + } + val noiseKey = peerInfo.noisePublicKey ?: run { + Log.d(TAG, "Dropping background NDR event from $peerID: no noise key") + return + } + + val appContext = context.applicationContext + com.bitchat.android.favorites.FavoritesPersistenceService.initialize(appContext) + val favorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared + val relationship = favorites.getFavoriteStatus(noiseKey) + if (relationship?.isMutual != true) { + Log.d(TAG, "Ignoring background NDR event from $peerID without mutual favorite") + return + } + + val identity = com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(appContext) ?: return + val ndrService = com.bitchat.android.nostr.NdrNostrService.getInstance(appContext) + ndrService.configureIfNeeded(identity) + val expectedPeerPubkeyHex = favorites.findNdrSessionPubkeyHex(noiseKey) + val result = ndrService.processOutOfBandEventJson(eventJson, expectedPeerPubkeyHex) + val sessionLookupPubkeyHex = listOfNotNull( + result.sessionLookupPubkeyHex, + expectedPeerPubkeyHex + ).firstOrNull { ndrService.hasActiveSession(it) } + + if (sessionLookupPubkeyHex != null && ndrService.hasActiveSession(sessionLookupPubkeyHex)) { + favorites.updateNdrSessionPubkeyHex(noiseKey, sessionLookupPubkeyHex) + } + result.outboundPayloads.forEach { response -> + sendNdrEvent(peerID, response) + } + } + private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String, label: String) { serviceScope.launch { try { @@ -1425,6 +1554,7 @@ interface BluetoothMeshDelegate { fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) + fun didReceiveNdrEvent(peerID: String, payload: ByteArray, timestampMs: Long) fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? fun getNickname(): String? fun isFavorite(peerID: String): Boolean diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt index 37af030d4..2296fbfe1 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt @@ -5,10 +5,13 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattServer +import android.bluetooth.BluetoothStatusCodes +import android.os.Build import android.util.Log -import com.bitchat.android.protocol.SpecialRecipients 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.util.toHexString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,6 +20,10 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.Job import java.util.concurrent.ConcurrentHashMap @@ -49,6 +56,8 @@ class BluetoothPacketBroadcaster( companion object { private const val TAG = "BluetoothPacketBroadcaster" private const val CLEANUP_DELAY = com.bitchat.android.util.AppConstants.Mesh.BROADCAST_CLEANUP_DELAY_MS + private const val FRAGMENT_SEND_DELAY_MS = com.bitchat.android.util.AppConstants.Mesh.FRAGMENT_SEND_DELAY_MS + private const val NOTIFICATION_ACK_TIMEOUT_MS = com.bitchat.android.util.AppConstants.Mesh.NOTIFICATION_ACK_TIMEOUT_MS } // Optional nickname resolver injected by higher layer (peerID -> nickname?) @@ -118,6 +127,7 @@ class BluetoothPacketBroadcaster( // Actor scope for the broadcaster private val broadcasterScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val transferJobs = ConcurrentHashMap() + private val notificationMutexes = ConcurrentHashMap() // SERIALIZATION: Actor to serialize all broadcast operations @OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class) @@ -148,8 +158,9 @@ class BluetoothPacketBroadcaster( val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null) // Check if we need to fragment if (fragmentManager != null) { + val maxPacketSize = resolveMaxPacketSize(packet, routed) val fragments = try { - fragmentManager.createFragments(packet) + fragmentManager.createFragments(packet, maxPacketSize = maxPacketSize) } catch (e: Exception) { Log.e(TAG, "❌ Fragment creation failed: ${e.message}", e) if (isFile) { @@ -172,8 +183,7 @@ class BluetoothPacketBroadcaster( // If cancelled, stop sending remaining fragments if (transferId != null && transferJobs[transferId]?.isCancelled == true) return@launch broadcastSinglePacket(RoutedPacket(fragment, transferId = transferId), gattServer, characteristic) - // 20ms delay between fragments - delay(20) + delay(FRAGMENT_SEND_DELAY_MS) if (transferId != null) { sent += 1 TransferProgressManager.progress(transferId, sent, fragments.size) @@ -272,6 +282,48 @@ class BluetoothPacketBroadcaster( md.digest().joinToString("") { "%02x".format(it) } } catch (_: Exception) { bytes.size.toString(16) } + private fun resolveMaxPacketSize(packet: BitchatPacket, routed: RoutedPacket): Int { + val senderID = packet.senderID.toHexString() + + if (packet.senderID.toHexString() == myPeerID && packet.route?.isNotEmpty() == true) { + val firstHop = packet.route!!.first().toHexString() + return maxPacketSizeForPeer(firstHop) + } + + if (packet.recipientID != SpecialRecipients.BROADCAST) { + val recipientID = packet.recipientID?.toHexString().orEmpty() + if (recipientID.isNotEmpty()) { + return maxPacketSizeForPeer(recipientID) + } + } + + val candidateLimits = mutableListOf() + connectionTracker.getSubscribedDevices().forEach { device -> + if (device.address == routed.relayAddress) return@forEach + if (connectionTracker.addressPeerMap[device.address] == senderID) return@forEach + candidateLimits += connectionTracker.getDevicePacketLimit(device.address) + } + connectionTracker.getConnectedDevices().values.forEach { deviceConn -> + if (!deviceConn.isClient || deviceConn.gatt == null || deviceConn.characteristic == null) return@forEach + if (deviceConn.device.address == routed.relayAddress) return@forEach + if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) return@forEach + candidateLimits += connectionTracker.getDevicePacketLimit(deviceConn.device.address) + } + + return candidateLimits.minOrNull() ?: BlePacketBudget.packetLimitBytesForMtu(null) + } + + private fun maxPacketSizeForPeer(peerID: String): Int { + val candidateLimits = mutableListOf() + connectionTracker.getSubscribedDevices() + .filter { connectionTracker.addressPeerMap[it.address] == peerID } + .forEach { candidateLimits += connectionTracker.getDevicePacketLimit(it.address) } + connectionTracker.getConnectedDevices().values + .filter { connectionTracker.addressPeerMap[it.device.address] == peerID } + .forEach { candidateLimits += connectionTracker.getDevicePacketLimit(it.device.address) } + return candidateLimits.minOrNull() ?: BlePacketBudget.packetLimitBytesForMtu(null) + } + /** * Public entry point for broadcasting - submits request to actor for serialization @@ -365,7 +417,7 @@ class BluetoothPacketBroadcaster( if (serverTarget != null) { Log.d(TAG, "Source Routing: sending directly to first hop (server conn) $firstHop: ${serverTarget.address}") - if (notifyDevice(serverTarget, data, gattServer, characteristic)) { + if (notifyDeviceSuspending(serverTarget, data, gattServer, characteristic)) { val toPeer = connectionTracker.addressPeerMap[serverTarget.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, serverTarget.address, packet.ttl, packet.version, routeInfo) sent = true @@ -402,7 +454,7 @@ class BluetoothPacketBroadcaster( // If found, send directly if (targetDevice != null) { Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") - if (notifyDevice(targetDevice, data, gattServer, characteristic)) { + if (notifyDeviceSuspending(targetDevice, data, gattServer, characteristic)) { val toPeer = connectionTracker.addressPeerMap[targetDevice.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDevice.address, packet.ttl, packet.version, routeInfo) return // Sent, no need to continue @@ -442,7 +494,7 @@ class BluetoothPacketBroadcaster( Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}") return@forEach } - val sent = notifyDevice(device, data, gattServer, characteristic) + val sent = notifyDeviceSuspending(device, data, gattServer, characteristic) if (sent) { val toPeer = connectionTracker.addressPeerMap[device.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, device.address, packet.ttl, packet.version, routeInfo) @@ -478,20 +530,69 @@ class BluetoothPacketBroadcaster( gattServer: BluetoothGattServer?, characteristic: BluetoothGattCharacteristic? ): Boolean { - return try { - characteristic?.let { char -> - char.value = data - val result = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false - result - } ?: false - } catch (e: Exception) { - Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") - connectionScope.launch { - delay(CLEANUP_DELAY) - connectionTracker.removeSubscribedDevice(device) - connectionTracker.addressPeerMap.remove(device.address) + return runBlocking { + notifyDeviceSuspending(device, data, gattServer, characteristic) + } + } + + private suspend fun notifyDeviceSuspending( + device: BluetoothDevice, + data: ByteArray, + gattServer: BluetoothGattServer?, + characteristic: BluetoothGattCharacteristic? + ): Boolean { + val mutex = notificationMutexes.getOrPut(device.address) { Mutex() } + return mutex.withLock { + val char = characteristic ?: return@withLock false + val server = gattServer ?: return@withLock false + val ack = connectionTracker.enqueueNotificationAck(device.address) + try { + val queued = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + server.notifyCharacteristicChanged(device, char, false, data) + } else { + @Suppress("DEPRECATION") + run { + char.value = data + if (server.notifyCharacteristicChanged(device, char, false)) { + BluetoothStatusCodes.SUCCESS + } else { + BluetoothStatusCodes.ERROR_UNKNOWN + } + } + } + + if (queued != BluetoothStatusCodes.SUCCESS) { + connectionTracker.cancelNotificationAck(device.address, ack, removeImmediately = true) + Log.w(TAG, "Queued notification failed for ${device.address} with status $queued") + return@withLock false + } + + val callbackStatus = withTimeoutOrNull(NOTIFICATION_ACK_TIMEOUT_MS) { + ack.await() + } + + when { + callbackStatus == null -> { + connectionTracker.cancelNotificationAck(device.address, ack) + Log.w(TAG, "Timed out waiting for notification ack from ${device.address}") + false + } + callbackStatus != BluetoothGatt.GATT_SUCCESS -> { + Log.w(TAG, "Notification send failed for ${device.address} with callback status $callbackStatus") + false + } + else -> true + } + } catch (e: Exception) { + connectionTracker.cancelNotificationAck(device.address, ack, removeImmediately = true) + Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") + connectionScope.launch { + delay(CLEANUP_DELAY) + connectionTracker.removeSubscribedDevice(device) + connectionTracker.addressPeerMap.remove(device.address) + } + false } - false } } @@ -504,9 +605,16 @@ class BluetoothPacketBroadcaster( ): Boolean { return try { deviceConn.characteristic?.let { char -> - char.value = data - val result = deviceConn.gatt?.writeCharacteristic(char) ?: false - result + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + (deviceConn.gatt?.writeCharacteristic(char, data, char.writeType) + ?: BluetoothStatusCodes.ERROR_UNKNOWN) == BluetoothStatusCodes.SUCCESS + } else { + @Suppress("DEPRECATION") + run { + char.value = data + deviceConn.gatt?.writeCharacteristic(char) ?: false + } + } } ?: false } catch (e: Exception) { Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") @@ -525,7 +633,7 @@ class BluetoothPacketBroadcaster( return buildString { appendLine("=== Packet Broadcaster Debug Info ===") appendLine("Broadcaster Scope Active: ${broadcasterScope.isActive}") - appendLine("Actor Channel Closed: ${broadcasterActor.isClosedForSend}") + appendLine("Transfer Jobs Active: ${transferJobs.size}") appendLine("Connection Scope Active: ${connectionScope.isActive}") } } diff --git a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt index 2be19841f..a8b2e583b 100644 --- a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt @@ -51,7 +51,10 @@ class FragmentManager { * Create fragments from a large packet - 100% iOS Compatible * Matches iOS sendFragmentedPacket() implementation exactly */ - fun createFragments(packet: BitchatPacket): List { + fun createFragments( + packet: BitchatPacket, + maxPacketSize: Int = FRAGMENT_SIZE_THRESHOLD + ): List { try { Log.d(TAG, "🔀 Creating fragments for packet type ${packet.type}, payload: ${packet.payload.size} bytes") val encoded = packet.toBinaryData() @@ -71,7 +74,7 @@ class FragmentManager { Log.d(TAG, "📏 Unpadded to ${fullData.size} bytes") // iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue - if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) { + if (fullData.size <= maxPacketSize) { return listOf(packet) // No fragmentation needed } @@ -93,9 +96,10 @@ class FragmentManager { val fragmentHeaderSize = 13 // FragmentPayload header val paddingBuffer = 16 // MessagePadding.optimalBlockSize adds 16 bytes overhead - // 512 - Overhead + // Match the iOS BLE send path: fragment based on the current link budget. val packetOverhead = headerSize + senderSize + recipientSize + routeSize + fragmentHeaderSize + paddingBuffer - val maxDataSize = (512 - packetOverhead).coerceAtMost(MAX_FRAGMENT_SIZE) + val maxDataSize = (maxPacketSize - packetOverhead) + .coerceAtMost(MAX_FRAGMENT_SIZE) if (maxDataSize <= 0) { Log.e(TAG, "❌ Calculated maxDataSize is non-positive ($maxDataSize). Route too large?") diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 63081d749..4bef41847 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -20,6 +20,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro companion object { private const val TAG = "MessageHandler" + private const val MAX_PENDING_NOISE_ENCRYPTED_PER_PEER = 16 } // Delegate for callbacks @@ -30,6 +31,8 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro // Coroutines private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val pendingNoiseEncryptedLock = Any() + private val pendingNoiseEncrypted = mutableMapOf>() /** * Handle Noise encrypted transport message - SIMPLIFIED iOS-compatible version @@ -56,6 +59,9 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) if (decryptedData == null) { Log.w(TAG, "Failed to decrypt Noise message from $peerID - may need handshake") + if (delegate?.hasNoiseSession(peerID) != true) { + queuePendingNoiseEncrypted(peerID, routed) + } return } @@ -165,12 +171,37 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)") delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong()) } + com.bitchat.android.model.NoisePayloadType.NDR_EVENT -> { + Log.d(TAG, "🔐 NDR OOB event received from $peerID (${noisePayload.data.size} bytes)") + delegate?.onNdrEventReceived(peerID, noisePayload.data, packet.timestamp.toLong()) + } } } catch (e: Exception) { Log.e(TAG, "Error processing Noise encrypted message from $peerID: ${e.message}") } } + + private fun queuePendingNoiseEncrypted(peerID: String, routed: RoutedPacket) { + synchronized(pendingNoiseEncryptedLock) { + val queue = pendingNoiseEncrypted.getOrPut(peerID) { java.util.ArrayDeque() } + if (queue.size >= MAX_PENDING_NOISE_ENCRYPTED_PER_PEER) { + queue.removeFirst() + } + queue.addLast(routed) + Log.d(TAG, "🕒 Queued encrypted Noise packet from $peerID until handshake completes (pending=${queue.size})") + } + } + + suspend fun flushPendingNoiseEncrypted(peerID: String) { + val queued = synchronized(pendingNoiseEncryptedLock) { + pendingNoiseEncrypted.remove(peerID)?.toList().orEmpty() + } + if (queued.isEmpty()) return + + Log.d(TAG, "🔓 Replaying ${queued.size} queued encrypted Noise packet(s) from $peerID after handshake") + queued.forEach { handleNoiseEncrypted(it) } + } /** * Send delivery ACK for a received private message - exactly like iOS @@ -628,4 +659,5 @@ interface MessageHandlerDelegate { fun onReadReceiptReceived(messageID: String, peerID: String) fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) + fun onNdrEventReceived(peerID: String, payload: ByteArray, timestampMs: Long) } diff --git a/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt b/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt index c46a114fa..bdd73d338 100644 --- a/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt +++ b/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt @@ -23,6 +23,7 @@ enum class NoisePayloadType(val value: UByte) { DELIVERED(0x03u), // Message was delivered VERIFY_CHALLENGE(0x10u), // Verification challenge VERIFY_RESPONSE(0x11u), // Verification response + NDR_EVENT(0x12u), // UTF-8 Nostr event JSON for double-ratchet OOB bootstrap FILE_TRANSFER(0x20u); diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt index 58dc810ea..3e8fe0261 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -51,6 +51,7 @@ class NoiseEncryptionService(private val context: Context) { // Callbacks var onPeerAuthenticated: ((String, String) -> Unit)? = null // (peerID, fingerprint) var onHandshakeRequired: ((String) -> Unit)? = null // peerID needs handshake + var onSessionEstablished: ((String) -> Unit)? = null // peerID established transport session init { // Initialize identity state manager for persistent storage @@ -181,6 +182,9 @@ class NoiseEncryptionService(private val context: Context) { fun initiateHandshake(peerID: String): ByteArray? { return try { sessionManager.initiateHandshake(peerID) + } catch (e: NoiseSessionError.HandshakeAlreadyInProgress) { + Log.d(TAG, "Handshake already in progress with $peerID; not sending a competing init") + null } catch (e: Exception) { Log.e(TAG, "Failed to initiate handshake with $peerID: ${e.message}") null @@ -389,6 +393,7 @@ class NoiseEncryptionService(private val context: Context) { // Notify about authentication onPeerAuthenticated?.invoke(peerID, fingerprint) + onSessionEstablished?.invoke(peerID) } /** diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt index dc4a3d50b..5a5f8ee36 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt @@ -194,6 +194,7 @@ class NoiseSession( fun getState(): NoiseSessionState = state fun isEstablished(): Boolean = state is NoiseSessionState.Established fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking + fun isInitiatorSession(): Boolean = isInitiator fun getCreationTime(): Long = creationTime init { diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt index 2d9e06d81..0578942e3 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt @@ -13,6 +13,7 @@ class NoiseSessionManager( companion object { private const val TAG = "NoiseSessionManager" + private const val INITIAL_XX_MESSAGE_SIZE = 32 } private val sessions = ConcurrentHashMap() @@ -51,9 +52,16 @@ class NoiseSessionManager( /** * SIMPLIFIED: Initiate handshake - no tie breaker, just start */ + @Synchronized fun initiateHandshake(peerID: String): ByteArray { Log.d(TAG, "initiateHandshake($peerID)") + val existing = getSession(peerID) + if (existing?.isHandshaking() == true) { + Log.d(TAG, "Handshake already in progress with $peerID; not restarting") + throw NoiseSessionError.HandshakeAlreadyInProgress + } + // Remove any existing session first removeSession(peerID) @@ -80,12 +88,26 @@ class NoiseSessionManager( /** * Handle incoming handshake message */ + @Synchronized fun processHandshakeMessage(peerID: String, message: ByteArray): ByteArray? { Log.d(TAG, "processHandshakeMessage($peerID, ${message.size} bytes)") try { var session = getSession(peerID) + // If both peers initiate at the same time, the inbound 32-byte XX + // message is the peer's first message. Yield to it so one side can + // become responder instead of trying to read it as message 2. + if ( + session?.isInitiatorSession() == true && + session.isHandshaking() && + message.size == INITIAL_XX_MESSAGE_SIZE + ) { + Log.d(TAG, "Simultaneous initiator collision with $peerID; switching to RESPONDER") + removeSession(peerID) + session = null + } + // If no session exists, create one as responder if (session == null) { Log.d(TAG, "Creating new RESPONDER session for $peerID") @@ -222,5 +244,6 @@ sealed class NoiseSessionError(message: String, cause: Throwable? = null) : Exce object SessionNotEstablished : NoiseSessionError("Session not established") object InvalidState : NoiseSessionError("Session in invalid state") object HandshakeFailed : NoiseSessionError("Handshake failed") + object HandshakeAlreadyInProgress : NoiseSessionError("Handshake already in progress") object AlreadyEstablished : NoiseSessionError("Session already established") } diff --git a/app/src/main/java/com/bitchat/android/nostr/NdrBootstrapDecider.kt b/app/src/main/java/com/bitchat/android/nostr/NdrBootstrapDecider.kt new file mode 100644 index 000000000..95ccde586 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/nostr/NdrBootstrapDecider.kt @@ -0,0 +1,38 @@ +package com.bitchat.android.nostr + +enum class NdrBootstrapAction { + NONE, + START_NOISE_HANDSHAKE, + SEND_OOB_INVITE +} + +object NdrBootstrapDecider { + private const val INVITE_RETRY_MS = 15_000L + private const val HANDSHAKE_RETRY_MS = 5_000L + + fun decide( + hasActiveDoubleRatchet: Boolean, + hasEstablishedNoiseSession: Boolean, + nowMs: Long, + lastInviteAttemptMs: Long, + lastHandshakeAttemptMs: Long + ): NdrBootstrapAction { + if (hasActiveDoubleRatchet) { + return NdrBootstrapAction.NONE + } + + if (!hasEstablishedNoiseSession) { + return if (nowMs - lastHandshakeAttemptMs >= HANDSHAKE_RETRY_MS) { + NdrBootstrapAction.START_NOISE_HANDSHAKE + } else { + NdrBootstrapAction.NONE + } + } + + return if (nowMs - lastInviteAttemptMs >= INVITE_RETRY_MS) { + NdrBootstrapAction.SEND_OOB_INVITE + } else { + NdrBootstrapAction.NONE + } + } +} diff --git a/app/src/main/java/com/bitchat/android/nostr/NdrNostrService.kt b/app/src/main/java/com/bitchat/android/nostr/NdrNostrService.kt new file mode 100644 index 000000000..20a1d22e3 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/nostr/NdrNostrService.kt @@ -0,0 +1,513 @@ +package com.bitchat.android.nostr + +import android.content.Context +import android.util.Log +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser + +class NdrNostrService( + private val relayManager: NdrRelayManager, + private val runtimeFactory: NdrSessionManagerFactory, + private val storageDirectoryProvider: () -> String, + private val deviceIdProvider: () -> String +) { + + companion object { + private const val TAG = "NdrNostrService" + private const val COMPACT_INVITE_URL_ROOT = "https://b" + + @Volatile + private var INSTANCE: NdrNostrService? = null + + fun getInstance(context: Context): NdrNostrService { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: create(context.applicationContext).also { INSTANCE = it } + } + } + + private fun create(context: Context): NdrNostrService { + val relayManager = object : NdrRelayManager { + override fun subscribe(filter: NostrFilter, id: String, handler: (NostrEvent) -> Unit) { + NostrRelayManager.getInstance(context).subscribe(filter, id, handler) + } + + override fun unsubscribe(id: String) { + NostrRelayManager.getInstance(context).unsubscribe(id) + } + + override fun sendEvent(event: NostrEvent) { + NostrRelayManager.getInstance(context).sendEvent(event) + } + } + + val runtimeFactory = object : NdrSessionManagerFactory { + override fun newWithStoragePath( + ourPubkeyHex: String, + ourIdentityPrivkeyHex: String, + deviceId: String, + storagePath: String, + ownerPubkeyHex: String? + ): NdrSessionManager { + return UniffiNdrSessionManager( + uniffi.ndr_ffi.SessionManagerHandle.newWithStoragePath( + ourPubkeyHex, + ourIdentityPrivkeyHex, + deviceId, + storagePath, + ownerPubkeyHex + ) + ) + } + } + + return NdrNostrService( + relayManager = relayManager, + runtimeFactory = runtimeFactory, + storageDirectoryProvider = { + context.filesDir.resolve("ndr").apply { mkdirs() }.absolutePath + }, + deviceIdProvider = { + val prefs = context.getSharedPreferences("bitchat_ndr", Context.MODE_PRIVATE) + prefs.getString("device_id", null) ?: java.util.UUID.randomUUID().toString().also { + prefs.edit().putString("device_id", it).apply() + } + } + ) + } + } + + @Volatile + var onDecryptedMessage: ((NdrDecryptedMessage) -> Unit)? = null + + @Volatile + private var sessionManager: NdrSessionManager? = null + + @Volatile + private var configuredForPubkeyHex: String? = null + + @Volatile + private var cachedInviteEventJson: String? = null + + private val activeSubIds = linkedSetOf() + + val isConfigured: Boolean + get() = sessionManager != null + + fun currentInviteEventJson(): String? = cachedInviteEventJson + + @Synchronized + fun configureIfNeeded(identity: NostrIdentity) { + val pubkeyHex = identity.publicKeyHex.lowercase() + if (configuredForPubkeyHex == pubkeyHex && sessionManager != null) { + return + } + + teardownLocked() + configuredForPubkeyHex = pubkeyHex + + try { + val runtime = runtimeFactory.newWithStoragePath( + ourPubkeyHex = pubkeyHex, + ourIdentityPrivkeyHex = identity.privateKeyHex, + deviceId = deviceIdProvider(), + storagePath = storageDirectoryProvider(), + ownerPubkeyHex = null + ) + runtime.init() + sessionManager = runtime + drainAndApplyPubSubEventsLocked() + Log.d(TAG, "Configured NDR for ${pubkeyHex.take(8)}...") + } catch (t: Throwable) { + Log.e(TAG, "Failed to configure NDR: ${t.message}") + teardownLocked() + } + } + + fun hasActiveSession(peerPubkeyHex: String): Boolean { + val runtime = sessionManager ?: return false + return try { + runtime.getActiveSessionState(peerPubkeyHex.lowercase()) != null + } catch (_: Throwable) { + false + } + } + + fun activeSessionStateJson(peerPubkeyHex: String): String? { + val runtime = sessionManager ?: return null + return try { + runtime.getActiveSessionState(peerPubkeyHex.lowercase()) + } catch (_: Throwable) { + null + } + } + + fun sendIfPossible(text: String, peerPubkeyHex: String): Boolean { + val runtime = sessionManager ?: return false + if (!hasActiveSession(peerPubkeyHex)) return false + return try { + val outboundEventIds = runtime.sendText(peerPubkeyHex.lowercase(), text, null) + synchronized(this) { + drainAndApplyPubSubEventsLocked() + } + if (outboundEventIds.isEmpty()) { + Log.d(TAG, "NDR send queued no relay publish for ${peerPubkeyHex.take(8)}...") + } + true + } catch (t: Throwable) { + Log.d(TAG, "NDR send failed: ${t.message}") + synchronized(this) { + drainAndApplyPubSubEventsLocked() + } + false + } + } + + fun processOutOfBandEventJson( + eventJson: String, + expectedPeerPubkeyHex: String? = null + ): NdrOutOfBandProcessResult { + val runtime = sessionManager ?: return NdrOutOfBandProcessResult(emptyList()) + val trimmedPayload = eventJson.trim() + val expectedPeer = expectedPeerPubkeyHex + ?.lowercase() + ?.takeIf { it.matches(Regex("^[0-9a-f]{64}$")) } + val inboundInvite = parseOutOfBandInvite(trimmedPayload) + val parsedEventPubkeyHex = NostrEvent.fromJsonString(trimmedPayload)?.pubkey?.lowercase() + var acceptResult: NdrAcceptInviteResult? = null + + try { + when { + inboundInvite?.transport == OutOfBandInviteTransport.EVENT_JSON -> { + acceptResult = runtime.acceptInviteFromEventJson(trimmedPayload, expectedPeer) + } + inboundInvite?.transport == OutOfBandInviteTransport.URL || !trimmedPayload.startsWith("{") -> { + acceptResult = runtime.acceptInviteFromUrl(trimmedPayload, expectedPeer) + } + else -> { + runtime.processEvent(trimmedPayload) + } + } + } catch (t: Throwable) { + Log.d(TAG, "Ignoring OOB event: ${t.message}") + } + + val outOfBandPublishes = synchronized(this) { + drainAndApplyPubSubEventsLocked(collectOutOfBandPublishes = true) + } + val sessionLookupPubkeyHex = acceptResult?.ownerPubkeyHex?.lowercase() + ?: expectedPeer?.takeIf { hasActiveSession(it) } + ?: parsedEventPubkeyHex + ?: inboundInvite?.senderPubkeyHex + + if (inboundInvite != null && + inboundInvite.transport == OutOfBandInviteTransport.EVENT_JSON && + outOfBandPublishes.isEmpty() && + sessionLookupPubkeyHex != null && + hasActiveSession(sessionLookupPubkeyHex) + ) { + preferredInviteOobPayload()?.let { + return NdrOutOfBandProcessResult( + outboundPayloads = outOfBandPublishes + it, + sessionLookupPubkeyHex = sessionLookupPubkeyHex + ) + } + } + + return NdrOutOfBandProcessResult( + outboundPayloads = outOfBandPublishes, + sessionLookupPubkeyHex = sessionLookupPubkeyHex + ) + } + + fun processInboundRelayEvent(event: NostrEvent) { + val runtime = sessionManager ?: return + + try { + runtime.processEvent(event.toJsonString()) + } catch (t: Throwable) { + Log.d(TAG, "Ignoring relay event ${event.id.take(8)}...: ${t.message}") + } + + synchronized(this) { + drainAndApplyPubSubEventsLocked() + } + } + + @Synchronized + private fun drainAndApplyPubSubEventsLocked( + collectOutOfBandPublishes: Boolean = false + ): List { + val runtime = sessionManager ?: return emptyList() + val outOfBandPublishes = mutableListOf() + + val events = try { + runtime.drainEvents() + } catch (t: Throwable) { + Log.e(TAG, "Failed to drain NDR events: ${t.message}") + return emptyList() + } + + events.forEach { event -> + applyPubSubEventLocked( + event = event, + collectOutOfBandPublish = if (collectOutOfBandPublishes) { + { value -> outOfBandPublishes.add(value) } + } else { + null + } + ) + } + + return outOfBandPublishes + } + + @Synchronized + private fun applyPubSubEventLocked( + event: NdrPubSubEvent, + collectOutOfBandPublish: ((String) -> Unit)? + ) { + when (event.kind) { + "subscribe" -> { + val subid = event.subid ?: return + val filterJson = event.filterJson ?: return + if (shouldIgnoreNdrSubscription(filterJson)) { + return + } + if (!activeSubIds.add(subid)) { + return + } + val filter = parseFilterJson(filterJson) + relayManager.subscribe(filter, subid) { inbound -> + processInboundRelayEvent(inbound) + } + } + + "unsubscribe" -> { + val subid = event.subid ?: return + relayManager.unsubscribe(subid) + activeSubIds.remove(subid) + } + + "publish_signed" -> { + val eventJson = event.eventJson ?: return + val nostrEvent = NostrEvent.fromJsonString(eventJson) ?: return + when { + isDoubleRatchetInviteEvent(nostrEvent) -> { + cachedInviteEventJson = eventJson + collectOutOfBandPublish?.invoke(eventJson) + } + + nostrEvent.kind == NostrKind.GIFT_WRAP -> { + collectOutOfBandPublish?.invoke(eventJson) + } + + else -> relayManager.sendEvent(nostrEvent) + } + } + + "decrypted_message" -> { + val content = event.content ?: return + val senderPubkeyHex = event.senderPubkeyHex ?: return + onDecryptedMessage?.invoke( + NdrDecryptedMessage( + content = content, + senderPubkeyHex = senderPubkeyHex.lowercase(), + eventId = event.eventId, + innerEventJson = content.takeIf { it.trimStart().startsWith("{") } + ) + ) + } + } + } + + @Synchronized + private fun teardownLocked() { + activeSubIds.forEach { relayManager.unsubscribe(it) } + activeSubIds.clear() + cachedInviteEventJson = null + configuredForPubkeyHex = null + sessionManager?.destroy() + sessionManager = null + } + + private fun isDoubleRatchetInviteEvent(event: NostrEvent): Boolean { + if (event.kind != 30078) { + return false + } + return event.tags.any { tag -> + (tag.size >= 2 && tag[0] == "l" && tag[1] == "double-ratchet/invites") || + (tag.size >= 2 && tag[0] == "d" && tag[1].startsWith("double-ratchet/invites/")) + } + } + + private enum class OutOfBandInviteTransport { + EVENT_JSON, + URL + } + + private data class ParsedOutOfBandInvite( + val senderPubkeyHex: String, + val transport: OutOfBandInviteTransport + ) + + fun outOfBandSenderPubkeyHex(payload: String): String? { + return parseOutOfBandInvite(payload.trim())?.senderPubkeyHex + } + + private fun parseOutOfBandInvite(payload: String): ParsedOutOfBandInvite? { + if (payload.isBlank()) return null + + if (payload.startsWith("{")) { + val event = NostrEvent.fromJsonString(payload) ?: return null + if (!isDoubleRatchetInviteEvent(event)) return null + return ParsedOutOfBandInvite( + senderPubkeyHex = event.pubkey.lowercase(), + transport = OutOfBandInviteTransport.EVENT_JSON + ) + } + + return try { + val invite = uniffi.ndr_ffi.InviteHandle.fromUrl(payload) + invite.use { + ParsedOutOfBandInvite( + senderPubkeyHex = it.`getInviterPubkeyHex`().lowercase(), + transport = OutOfBandInviteTransport.URL + ) + } + } catch (_: Throwable) { + null + } + } + + private fun preferredInviteOobPayload(): String? { + val inviteEventJson = cachedInviteEventJson ?: return null + return compactInviteUrl(inviteEventJson) ?: inviteEventJson + } + + private fun compactInviteUrl(eventJson: String): String? { + return try { + val invite = uniffi.ndr_ffi.InviteHandle.fromEventJson(eventJson) + invite.use { it.`toUrl`(COMPACT_INVITE_URL_ROOT) } + } catch (_: Throwable) { + null + } + } + + private fun shouldIgnoreNdrSubscription(filterJson: String): Boolean { + return try { + val root = JsonParser.parseString(filterJson).asJsonObject + val kinds = root.getAsJsonArray("kinds")?.mapNotNull { it.asInt } ?: emptyList() + if (NostrKind.GIFT_WRAP in kinds) { + return true + } + if (30078 !in kinds) { + return false + } + val labelValues = root.getAsJsonArray("#l")?.mapNotNull { it.asString } ?: emptyList() + labelValues.contains("double-ratchet/invites") + } catch (_: Throwable) { + false + } + } + + private fun parseFilterJson(filterJson: String): NostrFilter { + val root = JsonParser.parseString(filterJson).asJsonObject + val builder = NostrFilter.Builder() + + root.strings("ids")?.let { if (it.isNotEmpty()) builder.ids(*it.toTypedArray()) } + root.strings("authors")?.let { if (it.isNotEmpty()) builder.authors(*it.toTypedArray()) } + root.ints("kinds")?.let { if (it.isNotEmpty()) builder.kinds(*it.toIntArray()) } + root.get("since")?.takeIf { !it.isJsonNull }?.asLong?.let { builder.since(it * 1000L) } + root.get("until")?.takeIf { !it.isJsonNull }?.asLong?.let { builder.until(it * 1000L) } + root.get("limit")?.takeIf { !it.isJsonNull }?.asInt?.let { builder.limit(it) } + + root.entrySet().forEach { (key, value) -> + if (!key.startsWith("#") || !value.isJsonArray) { + return@forEach + } + val tagValues = value.asJsonArray.mapNotNull { if (it.isJsonNull) null else it.asString } + if (tagValues.isNotEmpty()) { + builder.tag(key.removePrefix("#"), *tagValues.toTypedArray()) + } + } + + return builder.build() + } + + private fun JsonObject.strings(name: String): List? { + return getAsJsonArray(name)?.mapNotNull { if (it.isJsonNull) null else it.asString } + } + + private fun JsonObject.ints(name: String): List? { + return getAsJsonArray(name)?.mapNotNull { if (it.isJsonNull) null else it.asInt } + } +} + +private class UniffiNdrSessionManager( + private val handle: uniffi.ndr_ffi.SessionManagerHandle +) : NdrSessionManager { + override fun init() { + handle.`init`() + } + + override fun acceptInviteFromEventJson( + eventJson: String, + ownerPubkeyHintHex: String? + ): NdrAcceptInviteResult { + val result = handle.`acceptInviteFromEventJson`(eventJson, ownerPubkeyHintHex) + return NdrAcceptInviteResult( + ownerPubkeyHex = result.ownerPubkeyHex, + inviterDevicePubkeyHex = result.inviterDevicePubkeyHex, + deviceId = result.deviceId, + createdNewSession = result.createdNewSession + ) + } + + override fun acceptInviteFromUrl( + inviteUrl: String, + ownerPubkeyHintHex: String? + ): NdrAcceptInviteResult { + val result = handle.`acceptInviteFromUrl`(inviteUrl, ownerPubkeyHintHex) + return NdrAcceptInviteResult( + ownerPubkeyHex = result.ownerPubkeyHex, + inviterDevicePubkeyHex = result.inviterDevicePubkeyHex, + deviceId = result.deviceId, + createdNewSession = result.createdNewSession + ) + } + + override fun processEvent(eventJson: String) { + handle.`processEvent`(eventJson) + } + + override fun drainEvents(): List { + return handle.`drainEvents`().map { + NdrPubSubEvent( + kind = it.kind, + subid = it.subid, + filterJson = it.filterJson, + eventJson = it.eventJson, + senderPubkeyHex = it.senderPubkeyHex, + content = it.content, + eventId = it.eventId + ) + } + } + + override fun getActiveSessionState(peerPubkeyHex: String): String? { + return handle.`getActiveSessionState`(peerPubkeyHex) + } + + override fun sendText(recipientPubkeyHex: String, text: String, expiresAtSeconds: ULong?): List { + return handle.`sendText`(recipientPubkeyHex, text, expiresAtSeconds) + } + + override fun getOurPubkeyHex(): String = handle.`getOurPubkeyHex`() + + override fun getTotalSessions(): ULong = handle.`getTotalSessions`() + + override fun destroy() { + handle.destroy() + } +} diff --git a/app/src/main/java/com/bitchat/android/nostr/NdrTypes.kt b/app/src/main/java/com/bitchat/android/nostr/NdrTypes.kt new file mode 100644 index 000000000..4d54ca3f7 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/nostr/NdrTypes.kt @@ -0,0 +1,59 @@ +package com.bitchat.android.nostr + +data class NdrPubSubEvent( + val kind: String, + val subid: String? = null, + val filterJson: String? = null, + val eventJson: String? = null, + val senderPubkeyHex: String? = null, + val content: String? = null, + val eventId: String? = null +) + +data class NdrDecryptedMessage( + val content: String, + val senderPubkeyHex: String, + val eventId: String? = null, + val innerEventJson: String? = null +) + +data class NdrAcceptInviteResult( + val ownerPubkeyHex: String, + val inviterDevicePubkeyHex: String, + val deviceId: String, + val createdNewSession: Boolean +) + +data class NdrOutOfBandProcessResult( + val outboundPayloads: List, + val sessionLookupPubkeyHex: String? = null +) + +interface NdrRelayManager { + fun subscribe(filter: NostrFilter, id: String, handler: (NostrEvent) -> Unit) + fun unsubscribe(id: String) + fun sendEvent(event: NostrEvent) +} + +interface NdrSessionManager { + fun init() + fun acceptInviteFromEventJson(eventJson: String, ownerPubkeyHintHex: String?): NdrAcceptInviteResult + fun acceptInviteFromUrl(inviteUrl: String, ownerPubkeyHintHex: String?): NdrAcceptInviteResult + fun processEvent(eventJson: String) + fun drainEvents(): List + fun getActiveSessionState(peerPubkeyHex: String): String? + fun sendText(recipientPubkeyHex: String, text: String, expiresAtSeconds: ULong? = null): List + fun getOurPubkeyHex(): String + fun getTotalSessions(): ULong + fun destroy() +} + +interface NdrSessionManagerFactory { + fun newWithStoragePath( + ourPubkeyHex: String, + ourIdentityPrivkeyHex: String, + deviceId: String, + storagePath: String, + ownerPubkeyHex: String? + ): NdrSessionManager +} diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrDirectMessageHandler.kt b/app/src/main/java/com/bitchat/android/nostr/NostrDirectMessageHandler.kt index 18c12e4de..05de9d00a 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrDirectMessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrDirectMessageHandler.kt @@ -31,6 +31,7 @@ class NostrDirectMessageHandler( companion object { private const val TAG = "NostrDirectMessageHandler" } private val seenStore by lazy { SeenMessageStore.getInstance(application) } + private val ndrService by lazy { NdrNostrService.getInstance(application) } // Simple event deduplication private val processedIds = ArrayDeque() @@ -48,6 +49,13 @@ class NostrDirectMessageHandler( return false } + fun configureDoubleRatchet(identity: NostrIdentity) { + ndrService.configureIfNeeded(identity) + ndrService.onDecryptedMessage = { message -> + onDoubleRatchetMessage(message, identity) + } + } + fun onGiftWrap(giftWrap: NostrEvent, geohash: String, identity: NostrIdentity) { scope.launch(Dispatchers.Default) { try { @@ -67,44 +75,96 @@ class NostrDirectMessageHandler( // If sender is blocked for geohash contexts, drop any events from this pubkey // Applies to both geohash DMs (geohash != "") and account DMs (geohash == "") if (dataManager.isGeohashUserBlocked(senderPubkey)) return@launch - if (!content.startsWith("bitchat1:")) return@launch - - val base64Content = content.removePrefix("bitchat1:") - val packetData = base64URLDecode(base64Content) ?: return@launch - val packet = BitchatPacket.fromBinaryData(packetData) ?: return@launch - - if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch - - val noisePayload = NoisePayload.decode(packet.payload) ?: return@launch - val messageTimestamp = Date(giftWrap.createdAt * 1000L) - val convKey = "nostr_${senderPubkey.take(16)}" - repo.putNostrKeyMapping(convKey, senderPubkey) - com.bitchat.android.nostr.GeohashAliasRegistry.put(convKey, senderPubkey) - if (geohash.isNotEmpty()) { - // Remember which geohash this conversation belongs to so we can subscribe on-demand - repo.setConversationGeohash(convKey, geohash) - GeohashConversationRegistry.set(convKey, geohash) - } + processEmbeddedBitChatContent( + content = content, + senderPubkey = senderPubkey, + timestamp = Date(giftWrap.createdAt * 1000L), + geohash = geohash, + recipientIdentity = identity + ) - // Ensure sender appears in geohash people list even if they haven't posted publicly yet - if (geohash.isNotEmpty()) { - // Cache a best-effort nickname and mark as participant - val cached = repo.getCachedNickname(senderPubkey) - if (cached == null) { - val base = repo.displayNameForNostrPubkeyUI(senderPubkey).substringBefore("#") - repo.cacheNickname(senderPubkey, base) - } - repo.updateParticipant(geohash, senderPubkey, messageTimestamp) - } + } catch (e: Exception) { + Log.e(TAG, "onGiftWrap error: ${e.message}") + } + } + } - val senderNickname = repo.displayNameForNostrPubkeyUI(senderPubkey) + private fun onDoubleRatchetMessage(message: NdrDecryptedMessage, identity: NostrIdentity) { + scope.launch(Dispatchers.Default) { + try { + val innerEvent = message.innerEventJson?.let(NostrEvent::fromJsonString) + val dedupeId = innerEvent?.id + ?: message.eventId + ?: "${message.senderPubkeyHex}:${message.content.hashCode()}" + if (dedupe(dedupeId)) return@launch + val senderPubkeyHex = innerEvent?.pubkey ?: message.senderPubkeyHex + if (dataManager.isGeohashUserBlocked(senderPubkeyHex)) return@launch - processNoisePayload(noisePayload, convKey, senderNickname, messageTimestamp, senderPubkey, identity) + Log.d( + TAG, + "Received NDR message event=${message.eventId ?: "unknown"} sender=${senderPubkeyHex.take(8)}..." + ) + processEmbeddedBitChatContent( + content = innerEvent?.content ?: message.content, + senderPubkey = senderPubkeyHex, + timestamp = innerEvent?.let { Date(it.createdAt * 1000L) } ?: Date(), + geohash = "", + recipientIdentity = identity + ) } catch (e: Exception) { - Log.e(TAG, "onGiftWrap error: ${e.message}") + Log.e(TAG, "onDoubleRatchetMessage error: ${e.message}") + } + } + } + + private suspend fun processEmbeddedBitChatContent( + content: String, + senderPubkey: String, + timestamp: Date, + geohash: String, + recipientIdentity: NostrIdentity + ) { + if (!content.startsWith("bitchat1:")) { + Log.d(TAG, "Ignoring non-embedded Nostr DM content") + return + } + + val base64Content = content.removePrefix("bitchat1:") + val packetData = base64URLDecode(base64Content) ?: run { + Log.w(TAG, "Failed to base64url-decode embedded BitChat packet") + return + } + val packet = BitchatPacket.fromBinaryData(packetData) ?: run { + Log.w(TAG, "Failed to decode embedded BitChat packet bytes=${packetData.size}") + return + } + if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) { + Log.d(TAG, "Ignoring embedded BitChat packet type=${packet.type}") + return + } + + val noisePayload = NoisePayload.decode(packet.payload) ?: run { + Log.w(TAG, "Failed to decode embedded Noise payload bytes=${packet.payload.size}") + return + } + val convKey = "nostr_${senderPubkey.take(16)}" + repo.putNostrKeyMapping(convKey, senderPubkey) + GeohashAliasRegistry.put(convKey, senderPubkey) + + if (geohash.isNotEmpty()) { + repo.setConversationGeohash(convKey, geohash) + GeohashConversationRegistry.set(convKey, geohash) + val cached = repo.getCachedNickname(senderPubkey) + if (cached == null) { + val base = repo.displayNameForNostrPubkeyUI(senderPubkey).substringBefore("#") + repo.cacheNickname(senderPubkey, base) } + repo.updateParticipant(geohash, senderPubkey, timestamp) } + + val senderNickname = repo.displayNameForNostrPubkeyUI(senderPubkey) + processNoisePayload(noisePayload, convKey, senderNickname, timestamp, senderPubkey, recipientIdentity) } private suspend fun processNoisePayload( @@ -117,9 +177,13 @@ class NostrDirectMessageHandler( ) { when (payload.type) { NoisePayloadType.PRIVATE_MESSAGE -> { - val pm = PrivateMessagePacket.decode(payload.data) ?: return + val pm = PrivateMessagePacket.decode(payload.data) ?: run { + Log.w(TAG, "Failed to decode Nostr private message TLV bytes=${payload.data.size}") + return + } val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList() if (existingMessages.any { it.id == pm.messageID }) return + Log.d(TAG, "Processing embedded Nostr private message") val message = BitchatMessage( id = pm.messageID, @@ -142,13 +206,26 @@ class NostrDirectMessageHandler( if (!seenStore.hasDelivered(pm.messageID)) { val nostrTransport = NostrTransport.getInstance(application) - nostrTransport.sendDeliveryAckGeohash(pm.messageID, senderPubkey, recipientIdentity) + val targetPeerID = resolvePeerIDForNostr(senderPubkey) + if (targetPeerID != null) { + nostrTransport.sendDeliveryAck(pm.messageID, targetPeerID) + } else { + nostrTransport.sendDeliveryAckGeohash(pm.messageID, senderPubkey, recipientIdentity) + } seenStore.markDelivered(pm.messageID) } if (isViewing && !suppressUnread) { val nostrTransport = NostrTransport.getInstance(application) - nostrTransport.sendReadReceiptGeohash(pm.messageID, senderPubkey, recipientIdentity) + val targetPeerID = resolvePeerIDForNostr(senderPubkey) + if (targetPeerID != null) { + nostrTransport.sendReadReceipt( + com.bitchat.android.model.ReadReceipt(pm.messageID), + targetPeerID + ) + } else { + nostrTransport.sendReadReceiptGeohash(pm.messageID, senderPubkey, recipientIdentity) + } seenStore.markRead(pm.messageID) } } @@ -190,7 +267,18 @@ class NostrDirectMessageHandler( } } NoisePayloadType.VERIFY_CHALLENGE, - NoisePayloadType.VERIFY_RESPONSE -> Unit // Ignore verification payloads in Nostr direct messages + NoisePayloadType.VERIFY_RESPONSE, + NoisePayloadType.NDR_EVENT -> Unit // Ignore transport-control payloads in Nostr direct messages + } + } + + private fun resolvePeerIDForNostr(senderPubkey: String): String? { + return try { + val favorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared + favorites.findPeerIDForNostrPubkey(senderPubkey) + ?: favorites.findNoiseKey(senderPubkey)?.joinToString("") { "%02x".format(it) } + } catch (_: Exception) { + null } } diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt b/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt index 26ba83695..2279f9350 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt @@ -40,6 +40,7 @@ class NostrTransport( private val readQueue = ConcurrentLinkedQueue() private var isSendingReadAcks = false private val transportScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val ndrService by lazy { NdrNostrService.getInstance(context) } // MARK: - Transport Interface Methods @@ -72,18 +73,12 @@ class NostrTransport( Log.d(TAG, "NostrTransport: preparing PM to ${recipientNostrPubkey.take(16)}... for peerID ${to.take(8)}... id=${messageID.take(8)}...") - // Convert recipient npub -> hex (x-only) - val recipientHex = try { - val (hrp, data) = Bech32.decode(recipientNostrPubkey) - if (hrp != "npub") { - Log.e(TAG, "NostrTransport: recipient key not npub (hrp=$hrp)") - return@launch - } - data.joinToString("") { "%02x".format(it) } - } catch (e: Exception) { - Log.e(TAG, "NostrTransport: failed to decode npub -> hex: $e") + val recipientHex = normalizeNostrPubkeyToHex(recipientNostrPubkey) + if (recipientHex == null) { + Log.e(TAG, "NostrTransport: failed to normalize recipient key") return@launch } + val ndrRecipientHex = resolveNdrRecipientHex(to, recipientHex) // Strict: lookup the recipient's current BitChat peer ID using favorites mapping val recipientPeerIDForEmbed = try { @@ -106,18 +101,14 @@ class NostrTransport( Log.e(TAG, "NostrTransport: failed to embed PM packet") return@launch } - - val giftWraps = NostrProtocol.createPrivateMessage( + + sendWrappedMessage( content = embedded, - recipientPubkey = recipientHex, - senderIdentity = senderIdentity + fallbackRecipientHex = recipientHex, + senderIdentity = senderIdentity, + ndrRecipientHex = ndrRecipientHex ) - giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending PM giftWrap id=${event.id.take(16)}...") - NostrRelayManager.getInstance(context).sendEvent(event) - } - } catch (e: Exception) { Log.e(TAG, "Failed to send private message via Nostr: ${e.message}") } @@ -167,18 +158,12 @@ class NostrTransport( Log.d(TAG, "NostrTransport: preparing READ ack for id=${item.receipt.originalMessageID.take(8)}... to ${recipientNostrPubkey.take(16)}...") - // Convert recipient npub -> hex - val recipientHex = try { - val (hrp, data) = Bech32.decode(recipientNostrPubkey) - if (hrp != "npub") { - scheduleNextReadAck() - return@launch - } - data.joinToString("") { "%02x".format(it) } - } catch (e: Exception) { + val recipientHex = normalizeNostrPubkeyToHex(recipientNostrPubkey) + if (recipientHex == null) { scheduleNextReadAck() return@launch } + val ndrRecipientHex = resolveNdrRecipientHex(item.peerID, recipientHex) val ack = NostrEmbeddedBitChat.encodeAckForNostr( type = NoisePayloadType.READ_RECEIPT, @@ -192,18 +177,14 @@ class NostrTransport( scheduleNextReadAck() return@launch } - - val giftWraps = NostrProtocol.createPrivateMessage( + + sendWrappedMessage( content = ack, - recipientPubkey = recipientHex, - senderIdentity = senderIdentity + fallbackRecipientHex = recipientHex, + senderIdentity = senderIdentity, + ndrRecipientHex = ndrRecipientHex ) - giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending READ ack giftWrap id=${event.id.take(16)}...") - NostrRelayManager.getInstance(context).sendEvent(event) - } - scheduleNextReadAck() } catch (e: Exception) { @@ -244,14 +225,11 @@ class NostrTransport( Log.d(TAG, "NostrTransport: preparing FAVORITE($isFavorite) to ${recipientNostrPubkey.take(16)}...") - // Convert recipient npub -> hex - val recipientHex = try { - val (hrp, data) = Bech32.decode(recipientNostrPubkey) - if (hrp != "npub") return@launch - data.joinToString("") { "%02x".format(it) } - } catch (e: Exception) { + val recipientHex = normalizeNostrPubkeyToHex(recipientNostrPubkey) + if (recipientHex == null) { return@launch } + val ndrRecipientHex = resolveNdrRecipientHex(to, recipientHex) val embedded = NostrEmbeddedBitChat.encodePMForNostr( content = content, @@ -264,18 +242,14 @@ class NostrTransport( Log.e(TAG, "NostrTransport: failed to embed favorite notification") return@launch } - - val giftWraps = NostrProtocol.createPrivateMessage( + + sendWrappedMessage( content = embedded, - recipientPubkey = recipientHex, - senderIdentity = senderIdentity + fallbackRecipientHex = recipientHex, + senderIdentity = senderIdentity, + ndrRecipientHex = ndrRecipientHex ) - giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending favorite giftWrap id=${event.id.take(16)}...") - NostrRelayManager.getInstance(context).sendEvent(event) - } - } catch (e: Exception) { Log.e(TAG, "Failed to send favorite notification via Nostr: ${e.message}") } @@ -303,13 +277,11 @@ class NostrTransport( Log.d(TAG, "NostrTransport: preparing DELIVERED ack for id=${messageID.take(8)}... to ${recipientNostrPubkey.take(16)}...") - val recipientHex = try { - val (hrp, data) = Bech32.decode(recipientNostrPubkey) - if (hrp != "npub") return@launch - data.joinToString("") { "%02x".format(it) } - } catch (e: Exception) { + val recipientHex = normalizeNostrPubkeyToHex(recipientNostrPubkey) + if (recipientHex == null) { return@launch } + val ndrRecipientHex = resolveNdrRecipientHex(to, recipientHex) val ack = NostrEmbeddedBitChat.encodeAckForNostr( type = NoisePayloadType.DELIVERED, @@ -322,18 +294,14 @@ class NostrTransport( Log.e(TAG, "NostrTransport: failed to embed DELIVERED ack") return@launch } - - val giftWraps = NostrProtocol.createPrivateMessage( + + sendWrappedMessage( content = ack, - recipientPubkey = recipientHex, - senderIdentity = senderIdentity + fallbackRecipientHex = recipientHex, + senderIdentity = senderIdentity, + ndrRecipientHex = ndrRecipientHex ) - giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending DELIVERED ack giftWrap id=${event.id.take(16)}...") - NostrRelayManager.getInstance(context).sendEvent(event) - } - } catch (e: Exception) { Log.e(TAG, "Failed to send delivery ack via Nostr: ${e.message}") } @@ -502,6 +470,69 @@ class NostrTransport( return null } } + + private fun sendWrappedMessage( + content: String, + fallbackRecipientHex: String, + senderIdentity: NostrIdentity, + ndrRecipientHex: String = fallbackRecipientHex + ): Boolean { + ndrService.configureIfNeeded(senderIdentity) + if (ndrService.sendIfPossible(content, ndrRecipientHex)) { + Log.d(TAG, "NostrTransport: sent via NDR to ${ndrRecipientHex.take(8)}...") + return true + } + + val giftWraps = NostrProtocol.createPrivateMessage( + content = content, + recipientPubkey = fallbackRecipientHex, + senderIdentity = senderIdentity + ) + + giftWraps.forEach { event -> + Log.d(TAG, "NostrTransport: sending fallback giftWrap id=${event.id.take(16)}...") + NostrRelayManager.getInstance(context).sendEvent(event) + } + return false + } + + private fun resolveNdrRecipientHex(target: String, fallbackRecipientHex: String): String { + val favoriteRelationship = resolveFavoriteRelationship(target) ?: return fallbackRecipientHex + return com.bitchat.android.favorites.FavoritesPersistenceService.shared + .findNdrSessionPubkeyHex(favoriteRelationship.peerNoisePublicKey) + ?: fallbackRecipientHex + } + + private fun resolveFavoriteRelationship(target: String): com.bitchat.android.favorites.FavoriteRelationship? { + val favorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared + return try { + when { + target.length == 16 && target.matches(Regex("^[0-9a-fA-F]+$")) -> { + favorites.getFavoriteStatus(target) + } + target.length == 64 && target.matches(Regex("^[0-9a-fA-F]+$")) -> { + favorites.getFavoriteStatus(hexStringToByteArray(target)) + } + else -> null + } + } catch (_: Exception) { + null + } + } + + private fun normalizeNostrPubkeyToHex(npubOrHex: String): String? { + return try { + if (npubOrHex.startsWith("npub1")) { + val (hrp, data) = Bech32.decode(npubOrHex) + if (hrp != "npub") return null + data.joinToString("") { "%02x".format(it) } + } else { + npubOrHex.lowercase() + } + } catch (_: Exception) { + null + } + } /** * Convert full hex string to byte array diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 786e7e915..ee554b45c 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -14,6 +14,9 @@ import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.service.MeshServiceHolder import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessageType +import com.bitchat.android.nostr.NdrBootstrapAction +import com.bitchat.android.nostr.NdrBootstrapDecider +import com.bitchat.android.nostr.NdrNostrService import com.bitchat.android.nostr.NostrIdentityBridge import com.bitchat.android.protocol.BitchatPacket @@ -143,6 +146,9 @@ class ChatViewModel( dataManager = dataManager, notificationManager = notificationManager ) + private val ndrService by lazy { NdrNostrService.getInstance(getApplication()) } + private val ndrBootstrapAttemptMs = mutableMapOf() + private val ndrNoiseHandshakeAttemptMs = mutableMapOf() @@ -627,8 +633,9 @@ class ChatViewModel( try { val myNostr = com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(getApplication()) val announcementContent = if (isNowFavorite) "[FAVORITED]:${myNostr?.npub ?: ""}" else "[UNFAVORITED]:${myNostr?.npub ?: ""}" - // Prefer mesh if session established, else try Nostr - if (meshService.hasEstablishedSession(peerID)) { + // Prefer mesh whenever the peer is connected; BluetoothMeshService will + // queue the notification until the Noise session finishes handshaking. + if (meshService.getPeerInfo(peerID)?.isConnected == true) { // Reuse existing private message path for notifications meshService.sendPrivateMessage( announcementContent, @@ -727,6 +734,7 @@ class ChatViewModel( if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) { verificationHandler.sendPendingVerificationIfNeeded(peerID) } + maybeBootstrapDoubleRatchetIfNeeded(peerID) } } @@ -887,6 +895,36 @@ class ChatViewModel( override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) { verificationHandler.didReceiveVerifyResponse(peerID, payload) } + + override fun didReceiveNdrEvent(peerID: String, payload: ByteArray, timestampMs: Long) { + val eventJson = payload.toString(Charsets.UTF_8) + if (eventJson.isBlank()) return + + val peerInfo = meshService.getPeerInfo(peerID) ?: return + val noiseKey = peerInfo.noisePublicKey ?: return + val relationship = FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey) + if (relationship?.isMutual != true) { + Log.d(TAG, "Ignoring NDR OOB event from $peerID without mutual favorite") + return + } + + val identity = NostrIdentityBridge.getCurrentNostrIdentity(getApplication()) ?: return + ndrService.configureIfNeeded(identity) + val expectedPeerPubkeyHex = FavoritesPersistenceService.shared.findNdrSessionPubkeyHex(noiseKey) + val result = ndrService.processOutOfBandEventJson(eventJson, expectedPeerPubkeyHex) + val sessionLookupPubkeyHex = listOfNotNull( + result.sessionLookupPubkeyHex, + expectedPeerPubkeyHex + ).firstOrNull { ndrService.hasActiveSession(it) } + if (sessionLookupPubkeyHex != null && ndrService.hasActiveSession(sessionLookupPubkeyHex)) { + FavoritesPersistenceService.shared.updateNdrSessionPubkeyHex(noiseKey, sessionLookupPubkeyHex) + ndrBootstrapAttemptMs.remove(peerID) + ndrNoiseHandshakeAttemptMs.remove(peerID) + } + result.outboundPayloads.forEach { response -> + meshService.sendNdrEvent(peerID, response) + } + } override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { return meshDelegateHandler.decryptChannelMessage(encryptedContent, channel) @@ -899,7 +937,52 @@ class ChatViewModel( override fun isFavorite(peerID: String): Boolean { return meshDelegateHandler.isFavorite(peerID) } - + + private fun maybeBootstrapDoubleRatchetIfNeeded(peerID: String) { + val peerInfo = meshService.getPeerInfo(peerID) ?: return + val noiseKey = peerInfo.noisePublicKey ?: return + val relationship = FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey) ?: return + if (!relationship.isMutual) return + + val peerPubkeyHex = FavoritesPersistenceService.shared.findNdrSessionPubkeyHex(noiseKey) ?: return + val identity = NostrIdentityBridge.getCurrentNostrIdentity(getApplication()) ?: return + + ndrService.configureIfNeeded(identity) + val hasActiveSession = ndrService.hasActiveSession(peerPubkeyHex) + if (hasActiveSession) { + ndrBootstrapAttemptMs.remove(peerID) + ndrNoiseHandshakeAttemptMs.remove(peerID) + return + } + + val now = System.currentTimeMillis() + val hasEstablishedNoiseSession = + meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established + + when (NdrBootstrapDecider.decide( + hasActiveDoubleRatchet = hasActiveSession, + hasEstablishedNoiseSession = hasEstablishedNoiseSession, + nowMs = now, + lastInviteAttemptMs = ndrBootstrapAttemptMs[peerID] ?: 0L, + lastHandshakeAttemptMs = ndrNoiseHandshakeAttemptMs[peerID] ?: 0L + )) { + NdrBootstrapAction.NONE -> return + NdrBootstrapAction.START_NOISE_HANDSHAKE -> { + ndrNoiseHandshakeAttemptMs[peerID] = now + meshService.initiateNoiseHandshake(peerID) + Log.d(TAG, "Initiating Noise handshake before NDR bootstrap for $peerID") + return + } + NdrBootstrapAction.SEND_OOB_INVITE -> Unit + } + + val inviteJson = ndrService.currentInviteEventJson() ?: return + ndrNoiseHandshakeAttemptMs.remove(peerID) + ndrBootstrapAttemptMs[peerID] = now + meshService.sendNdrEvent(peerID, inviteJson) + Log.d(TAG, "Sent NDR bootstrap invite to $peerID for ${peerPubkeyHex.take(8)}...") + } + // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager // MARK: - Emergency Clear diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt b/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt index 09b32d4ff..08dc75767 100644 --- a/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt @@ -83,6 +83,7 @@ class GeohashViewModel( } val identity = NostrIdentityBridge.getCurrentNostrIdentity(getApplication()) if (identity != null) { + dmHandler.configureDoubleRatchet(identity) // Use global chat-messages only for full account DMs (mesh context). For geohash DMs, subscribe per-geohash below. subscriptionManager.subscribeGiftWraps( pubkey = identity.publicKeyHex, diff --git a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt index d27719c76..4d05cce4f 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt @@ -224,6 +224,10 @@ class MeshDelegateHandler( override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) { // Handled by ChatViewModel for verification flow } + + override fun didReceiveNdrEvent(peerID: String, payload: ByteArray, timestampMs: Long) { + // Handled by ChatViewModel for double-ratchet bootstrap flow + } override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { return channelManager.decryptChannelMessage(encryptedContent, channel) diff --git a/app/src/main/java/com/bitchat/android/util/AppConstants.kt b/app/src/main/java/com/bitchat/android/util/AppConstants.kt index 11df5b080..600b098ff 100644 --- a/app/src/main/java/com/bitchat/android/util/AppConstants.kt +++ b/app/src/main/java/com/bitchat/android/util/AppConstants.kt @@ -21,6 +21,8 @@ object AppConstants { const val CONNECTION_CLEANUP_DELAY_MS: Long = 500L const val CONNECTION_CLEANUP_INTERVAL_MS: Long = 30_000L const val BROADCAST_CLEANUP_DELAY_MS: Long = 500L + const val FRAGMENT_SEND_DELAY_MS: Long = 30L + const val NOTIFICATION_ACK_TIMEOUT_MS: Long = 1_000L // GATT client RSSI updates const val RSSI_UPDATE_INTERVAL_MS: Long = 5_000L diff --git a/app/src/main/java/uniffi/ndr_ffi/ndr_ffi.kt b/app/src/main/java/uniffi/ndr_ffi/ndr_ffi.kt new file mode 100644 index 000000000..2b94dffe0 --- /dev/null +++ b/app/src/main/java/uniffi/ndr_ffi/ndr_ffi.kt @@ -0,0 +1,4786 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package uniffi.ndr_ffi + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.IntegerType +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.Callback +import com.sun.jna.ptr.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +/** + * @suppress + */ +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + // Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values. + // When dealing with these fields, make sure to call `toULong()`. + @JvmField var capacity: Long = 0 + @JvmField var len: Long = 0 + @JvmField var data: Pointer? = null + + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference + + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + + companion object { + internal fun alloc(size: ULong = 0UL) = uniffiRustCall() { status -> + // Note: need to convert the size to a `Long` value to make this work with JVM. + UniffiLib.INSTANCE.ffi_ndr_ffi_rustbuffer_alloc(size.toLong(), status) + }.also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + + internal fun create(capacity: ULong, len: ULong, data: Pointer?): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity.toLong() + buf.len = len.toLong() + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = uniffiRustCall() { status -> + UniffiLib.INSTANCE.ffi_ndr_ffi_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + * + * @suppress + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setLong(0, value.capacity) + pointer.setLong(8, value.len) + pointer.setPointer(16, value.data) + } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getLong(0)) + value.writeField("len", pointer.getLong(8)) + value.writeField("data", pointer.getLong(16)) + + return value + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +internal open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} +/** + * The FfiConverter interface handles converter types to and from the FFI + * + * All implementing objects should be public to support external types. When a + * type is external we need to import it's FfiConverter. + * + * @suppress + */ +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): ULong + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position().toLong()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +/** + * FfiConverter that uses `RustBuffer` as the FfiType + * + * @suppress + */ +public interface FfiConverterRustBuffer: FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + +@Structure.FieldOrder("code", "error_buf") +internal open class UniffiRustCallStatus : Structure() { + @JvmField var code: Byte = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue: UniffiRustCallStatus(), Structure.ByValue + + fun isSuccess(): Boolean { + return code == UNIFFI_CALL_SUCCESS + } + + fun isError(): Boolean { + return code == UNIFFI_CALL_ERROR + } + + fun isPanic(): Boolean { + return code == UNIFFI_CALL_UNEXPECTED_ERROR + } + + companion object { + fun create(code: Byte, errorBuf: RustBuffer.ByValue): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } +} + +class InternalException(message: String) : kotlin.Exception(message) + +/** + * Each top-level error class has a companion object that can lift the error from the call status's rust buffer + * + * @suppress + */ +interface UniffiRustCallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun uniffiRustCallWithError(errorHandler: UniffiRustCallStatusErrorHandler, callback: (UniffiRustCallStatus) -> U): U { + var status = UniffiRustCallStatus() + val return_value = callback(status) + uniffiCheckCallStatus(errorHandler, status) + return return_value +} + +// Check UniffiRustCallStatus and throw an error if the call wasn't successful +private fun uniffiCheckCallStatus(errorHandler: UniffiRustCallStatusErrorHandler, status: UniffiRustCallStatus) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +/** + * UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR + * + * @suppress + */ +object UniffiNullRustCallStatusErrorHandler: UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun uniffiRustCall(callback: (UniffiRustCallStatus) -> U): U { + return uniffiRustCallWithError(UniffiNullRustCallStatusErrorHandler, callback) +} + +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } + } +} +// Map handles to objects +// +// This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. +internal class UniffiHandleMap { + private val map = ConcurrentHashMap() + private val counter = java.util.concurrent.atomic.AtomicLong(0) + + val size: Int + get() = map.size + + // Insert a new object into the handle map and get a handle for it + fun insert(obj: T): Long { + val handle = counter.getAndAdd(1) + map.put(handle, obj) + return handle + } + + // Get an object from the handle map + fun get(handle: Long): T { + return map.get(handle) ?: throw InternalException("UniffiHandleMap.get: Invalid handle") + } + + // Remove an entry from the handlemap and get the Kotlin object back + fun remove(handle: Long): T { + return map.remove(handle) ?: throw InternalException("UniffiHandleMap: Invalid handle") + } +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "ndr_ffi" +} + +private inline fun loadIndirect( + componentName: String +): Lib { + return Native.load(findLibraryName(componentName), Lib::class.java) +} + +// Define FFI callback types +internal interface UniffiRustFutureContinuationCallback : com.sun.jna.Callback { + fun callback(`data`: Long,`pollResult`: Byte,) +} +internal interface UniffiForeignFutureFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +internal interface UniffiCallbackInterfaceFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +@Structure.FieldOrder("handle", "free") +internal open class UniffiForeignFuture( + @JvmField internal var `handle`: Long = 0.toLong(), + @JvmField internal var `free`: UniffiForeignFutureFree? = null, +) : Structure() { + class UniffiByValue( + `handle`: Long = 0.toLong(), + `free`: UniffiForeignFutureFree? = null, + ): UniffiForeignFuture(`handle`,`free`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFuture) { + `handle` = other.`handle` + `free` = other.`free` + } + +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF32( + @JvmField internal var `returnValue`: Float = 0.0f, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Float = 0.0f, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF64( + @JvmField internal var `returnValue`: Double = 0.0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Double = 0.0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructPointer( + @JvmField internal var `returnValue`: Pointer = Pointer.NULL, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Pointer = Pointer.NULL, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructPointer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructPointer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompletePointer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructPointer.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructRustBuffer( + @JvmField internal var `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructRustBuffer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructRustBuffer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteRustBuffer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructRustBuffer.UniffiByValue,) +} +@Structure.FieldOrder("callStatus") +internal open class UniffiForeignFutureStructVoid( + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructVoid(`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructVoid) { + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructVoid.UniffiByValue,) +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +internal interface UniffiLib : Library { + companion object { + internal val INSTANCE: UniffiLib by lazy { + loadIndirect(componentName = "ndr_ffi") + .also { lib: UniffiLib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } + } + + // The Cleaner for the whole library + internal val CLEANER: UniffiCleaner by lazy { + UniffiCleaner.create() + } + } + + fun uniffi_ndr_ffi_fn_clone_invitehandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_free_invitehandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_constructor_invitehandle_create_new(`inviterPubkeyHex`: RustBuffer.ByValue,`deviceId`: RustBuffer.ByValue,`maxUses`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_constructor_invitehandle_deserialize(`json`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_constructor_invitehandle_from_event_json(`eventJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_constructor_invitehandle_from_url(`url`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_method_invitehandle_accept(`ptr`: Pointer,`inviteePubkeyHex`: RustBuffer.ByValue,`inviteePrivkeyHex`: RustBuffer.ByValue,`deviceId`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_accept_with_owner(`ptr`: Pointer,`inviteePubkeyHex`: RustBuffer.ByValue,`inviteePrivkeyHex`: RustBuffer.ByValue,`deviceId`: RustBuffer.ByValue,`ownerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_get_inviter_pubkey_hex(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_get_shared_secret_hex(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_process_response(`ptr`: Pointer,`eventJson`: RustBuffer.ByValue,`inviterPrivkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_serialize(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_set_owner_pubkey_hex(`ptr`: Pointer,`ownerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_invitehandle_set_purpose(`ptr`: Pointer,`purpose`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_invitehandle_to_event_json(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_invitehandle_to_url(`ptr`: Pointer,`root`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_clone_sessionhandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_free_sessionhandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_constructor_sessionhandle_from_state_json(`stateJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_constructor_sessionhandle_init(`theirEphemeralPubkeyHex`: RustBuffer.ByValue,`ourEphemeralPrivkeyHex`: RustBuffer.ByValue,`isInitiator`: Byte,`sharedSecretHex`: RustBuffer.ByValue,`name`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_method_sessionhandle_can_send(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_ndr_ffi_fn_method_sessionhandle_decrypt_event(`ptr`: Pointer,`outerEventJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionhandle_is_dr_message(`ptr`: Pointer,`eventJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_ndr_ffi_fn_method_sessionhandle_send_text(`ptr`: Pointer,`text`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionhandle_state_json(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_clone_sessionmanagerhandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_free_sessionmanagerhandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_constructor_sessionmanagerhandle_new(`ourPubkeyHex`: RustBuffer.ByValue,`ourIdentityPrivkeyHex`: RustBuffer.ByValue,`deviceId`: RustBuffer.ByValue,`ownerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_constructor_sessionmanagerhandle_new_with_storage_path(`ourPubkeyHex`: RustBuffer.ByValue,`ourIdentityPrivkeyHex`: RustBuffer.ByValue,`deviceId`: RustBuffer.ByValue,`storagePath`: RustBuffer.ByValue,`ownerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_accept_invite_from_event_json(`ptr`: Pointer,`eventJson`: RustBuffer.ByValue,`ownerPubkeyHintHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_accept_invite_from_url(`ptr`: Pointer,`inviteUrl`: RustBuffer.ByValue,`ownerPubkeyHintHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_drain_events(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_active_session_state(`ptr`: Pointer,`peerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_device_id(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_message_push_author_pubkeys(`ptr`: Pointer,`peerOwnerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_message_push_session_states(`ptr`: Pointer,`peerOwnerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_our_pubkey_hex(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_owner_pubkey_hex(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_stored_user_record_json(`ptr`: Pointer,`peerOwnerPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_total_sessions(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_create(`ptr`: Pointer,`name`: RustBuffer.ByValue,`memberOwnerPubkeys`: RustBuffer.ByValue,`fanoutMetadata`: RustBuffer.ByValue,`nowMs`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_handle_incoming_session_event(`ptr`: Pointer,`eventJson`: RustBuffer.ByValue,`fromOwnerPubkeyHex`: RustBuffer.ByValue,`fromSenderDevicePubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_handle_outer_event(`ptr`: Pointer,`eventJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_known_sender_event_pubkeys(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_outer_subscription_plan(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_remove(`ptr`: Pointer,`groupId`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_send_event(`ptr`: Pointer,`groupId`: RustBuffer.ByValue,`kind`: Int,`content`: RustBuffer.ByValue,`tagsJson`: RustBuffer.ByValue,`nowMs`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_upsert(`ptr`: Pointer,`group`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_import_session_state(`ptr`: Pointer,`peerPubkeyHex`: RustBuffer.ByValue,`stateJson`: RustBuffer.ByValue,`deviceId`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_init(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_known_peer_owner_pubkeys(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_process_event(`ptr`: Pointer,`eventJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_event_with_inner_id(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`kind`: Int,`content`: RustBuffer.ByValue,`tagsJson`: RustBuffer.ByValue,`createdAtSeconds`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_reaction(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`messageId`: RustBuffer.ByValue,`emoji`: RustBuffer.ByValue,`expiresAtSeconds`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_receipt(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`receiptType`: RustBuffer.ByValue,`messageIds`: RustBuffer.ByValue,`expiresAtSeconds`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_rumor_json(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`rumorJson`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_text(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`text`: RustBuffer.ByValue,`expiresAtSeconds`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_text_with_inner_id(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`text`: RustBuffer.ByValue,`expiresAtSeconds`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_typing(`ptr`: Pointer,`recipientPubkeyHex`: RustBuffer.ByValue,`expiresAtSeconds`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_method_sessionmanagerhandle_setup_user(`ptr`: Pointer,`userPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_fn_func_create_signed_app_keys_event(`ownerPubkeyHex`: RustBuffer.ByValue,`ownerPrivkeyHex`: RustBuffer.ByValue,`devices`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_func_derive_public_key(`privkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_func_generate_keypair(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_func_parse_app_keys_event(`eventJson`: RustBuffer.ByValue,`ownerPrivkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_func_resolve_conversation_candidate_pubkeys(`ownerPubkeyHex`: RustBuffer.ByValue,`rumorPubkeyHex`: RustBuffer.ByValue,`rumorTags`: RustBuffer.ByValue,`senderPubkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_func_resolve_latest_app_keys_devices(`eventJsons`: RustBuffer.ByValue,`ownerPrivkeyHex`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_ndr_ffi_fn_func_version(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_ndr_ffi_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_ndr_ffi_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_ndr_ffi_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun ffi_ndr_ffi_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_ndr_ffi_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_u8(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_u8(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_ndr_ffi_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_i8(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_i8(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_ndr_ffi_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_u16(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_u16(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_ndr_ffi_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_i16(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_i16(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_ndr_ffi_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_u32(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_u32(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_ndr_ffi_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_i32(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_i32(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_ndr_ffi_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_u64(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_u64(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_ndr_ffi_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_i64(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_i64(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_ndr_ffi_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_f32(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_f32(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Float + fun ffi_ndr_ffi_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_f64(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_f64(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Double + fun ffi_ndr_ffi_rust_future_poll_pointer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_pointer(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_pointer(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun ffi_ndr_ffi_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_rust_buffer(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_rust_buffer(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_ndr_ffi_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_cancel_void(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_free_void(`handle`: Long, + ): Unit + fun ffi_ndr_ffi_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_ndr_ffi_checksum_func_create_signed_app_keys_event( + ): Short + fun uniffi_ndr_ffi_checksum_func_derive_public_key( + ): Short + fun uniffi_ndr_ffi_checksum_func_generate_keypair( + ): Short + fun uniffi_ndr_ffi_checksum_func_parse_app_keys_event( + ): Short + fun uniffi_ndr_ffi_checksum_func_resolve_conversation_candidate_pubkeys( + ): Short + fun uniffi_ndr_ffi_checksum_func_resolve_latest_app_keys_devices( + ): Short + fun uniffi_ndr_ffi_checksum_func_version( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_accept( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_accept_with_owner( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_get_inviter_pubkey_hex( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_get_shared_secret_hex( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_process_response( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_serialize( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_set_owner_pubkey_hex( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_set_purpose( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_to_event_json( + ): Short + fun uniffi_ndr_ffi_checksum_method_invitehandle_to_url( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionhandle_can_send( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionhandle_decrypt_event( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionhandle_is_dr_message( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionhandle_send_text( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionhandle_state_json( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_accept_invite_from_event_json( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_accept_invite_from_url( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_drain_events( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_active_session_state( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_device_id( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_message_push_author_pubkeys( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_message_push_session_states( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_our_pubkey_hex( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_owner_pubkey_hex( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_stored_user_record_json( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_total_sessions( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_create( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_handle_incoming_session_event( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_handle_outer_event( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_known_sender_event_pubkeys( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_outer_subscription_plan( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_remove( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_send_event( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_upsert( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_import_session_state( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_init( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_known_peer_owner_pubkeys( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_process_event( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_event_with_inner_id( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_reaction( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_receipt( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_rumor_json( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_text( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_text_with_inner_id( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_typing( + ): Short + fun uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_setup_user( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_invitehandle_create_new( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_invitehandle_deserialize( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_invitehandle_from_event_json( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_invitehandle_from_url( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_sessionhandle_from_state_json( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_sessionhandle_init( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_sessionmanagerhandle_new( + ): Short + fun uniffi_ndr_ffi_checksum_constructor_sessionmanagerhandle_new_with_storage_path( + ): Short + fun ffi_ndr_ffi_uniffi_contract_version( + ): Int + +} + +private fun uniffiCheckContractApiVersion(lib: UniffiLib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 26 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_ndr_ffi_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} + +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: UniffiLib) { + if (lib.uniffi_ndr_ffi_checksum_func_create_signed_app_keys_event() != 62391.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_func_derive_public_key() != 23373.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_func_generate_keypair() != 56100.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_func_parse_app_keys_event() != 24603.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_func_resolve_conversation_candidate_pubkeys() != 3184.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_func_resolve_latest_app_keys_devices() != 24185.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_func_version() != 58200.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_accept() != 50404.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_accept_with_owner() != 19609.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_get_inviter_pubkey_hex() != 17047.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_get_shared_secret_hex() != 42269.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_process_response() != 8323.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_serialize() != 6090.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_set_owner_pubkey_hex() != 988.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_set_purpose() != 14438.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_to_event_json() != 25504.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_invitehandle_to_url() != 21533.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionhandle_can_send() != 64471.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionhandle_decrypt_event() != 61795.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionhandle_is_dr_message() != 39495.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionhandle_send_text() != 53814.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionhandle_state_json() != 62261.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_accept_invite_from_event_json() != 10447.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_accept_invite_from_url() != 1488.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_drain_events() != 33023.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_active_session_state() != 34884.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_device_id() != 27863.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_message_push_author_pubkeys() != 25521.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_message_push_session_states() != 52859.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_our_pubkey_hex() != 15248.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_owner_pubkey_hex() != 38134.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_stored_user_record_json() != 54503.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_get_total_sessions() != 54736.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_create() != 41536.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_handle_incoming_session_event() != 45714.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_handle_outer_event() != 9485.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_known_sender_event_pubkeys() != 34048.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_outer_subscription_plan() != 65323.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_remove() != 33157.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_send_event() != 4165.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_group_upsert() != 12505.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_import_session_state() != 57446.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_init() != 12215.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_known_peer_owner_pubkeys() != 12569.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_process_event() != 55445.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_event_with_inner_id() != 35167.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_reaction() != 32190.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_receipt() != 34112.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_rumor_json() != 27697.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_text() != 39171.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_text_with_inner_id() != 49408.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_send_typing() != 11765.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_method_sessionmanagerhandle_setup_user() != 27115.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_invitehandle_create_new() != 4301.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_invitehandle_deserialize() != 552.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_invitehandle_from_event_json() != 46752.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_invitehandle_from_url() != 7682.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_sessionhandle_from_state_json() != 2882.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_sessionhandle_init() != 28461.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_sessionmanagerhandle_new() != 8939.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ndr_ffi_checksum_constructor_sessionmanagerhandle_new_with_storage_path() != 33050.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// Async support + +// Public interface members begin here. + + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + args.filterIsInstance() + .forEach(Disposable::destroy) + } + } +} + +/** + * @suppress + */ +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +/** + * Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. + * + * @suppress + * */ +object NoPointer + +/** + * @suppress + */ +public object FfiConverterUInt: FfiConverter { + override fun lift(value: Int): UInt { + return value.toUInt() + } + + override fun read(buf: ByteBuffer): UInt { + return lift(buf.getInt()) + } + + override fun lower(value: UInt): Int { + return value.toInt() + } + + override fun allocationSize(value: UInt) = 4UL + + override fun write(value: UInt, buf: ByteBuffer) { + buf.putInt(value.toInt()) + } +} + +/** + * @suppress + */ +public object FfiConverterULong: FfiConverter { + override fun lift(value: Long): ULong { + return value.toULong() + } + + override fun read(buf: ByteBuffer): ULong { + return lift(buf.getLong()) + } + + override fun lower(value: ULong): Long { + return value.toLong() + } + + override fun allocationSize(value: ULong) = 8UL + + override fun write(value: ULong, buf: ByteBuffer) { + buf.putLong(value.toLong()) + } +} + +/** + * @suppress + */ +public object FfiConverterBoolean: FfiConverter { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1UL + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} + +/** + * @suppress + */ +public object FfiConverterString: FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len.toInt()) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit().toULong()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): ULong { + val sizeForLength = 4UL + val sizeForString = value.length.toULong() * 3UL + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +/** + * The cleaner interface for Object finalization code to run. + * This is the entry point to any implementation that we're using. + * + * The cleaner registers objects and returns cleanables, so now we are + * defining a `UniffiCleaner` with a `UniffiClenaer.Cleanable` to abstract the + * different implmentations available at compile time. + * + * @suppress + */ +interface UniffiCleaner { + interface Cleanable { + fun clean() + } + + fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable + + companion object +} + +// The fallback Jna cleaner, which is available for both Android, and the JVM. +private class UniffiJnaCleaner : UniffiCleaner { + private val cleaner = com.sun.jna.internal.Cleaner.getCleaner() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + UniffiJnaCleanable(cleaner.register(value, cleanUpTask)) +} + +private class UniffiJnaCleanable( + private val cleanable: com.sun.jna.internal.Cleaner.Cleanable, +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + +// We decide at uniffi binding generation time whether we were +// using Android or not. +// There are further runtime checks to chose the correct implementation +// of the cleaner. +private fun UniffiCleaner.Companion.create(): UniffiCleaner = + try { + // For safety's sake: if the library hasn't been run in android_cleaner = true + // mode, but is being run on Android, then we still need to think about + // Android API versions. + // So we check if java.lang.ref.Cleaner is there, and use that… + java.lang.Class.forName("java.lang.ref.Cleaner") + JavaLangRefCleaner() + } catch (e: ClassNotFoundException) { + // … otherwise, fallback to the JNA cleaner. + UniffiJnaCleaner() + } + +private class JavaLangRefCleaner : UniffiCleaner { + val cleaner = java.lang.ref.Cleaner.create() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + JavaLangRefCleanable(cleaner.register(value, cleanUpTask)) +} + +private class JavaLangRefCleanable( + val cleanable: java.lang.ref.Cleaner.Cleanable +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} +/** + * FFI wrapper for Invite. + */ +public interface InviteHandleInterface { + + /** + * Accept the invite and create a session. + */ + fun `accept`(`inviteePubkeyHex`: kotlin.String, `inviteePrivkeyHex`: kotlin.String, `deviceId`: kotlin.String?): InviteAcceptResult + + /** + * Accept the invite as an owner and include the owner pubkey in the response payload. + */ + fun `acceptWithOwner`(`inviteePubkeyHex`: kotlin.String, `inviteePrivkeyHex`: kotlin.String, `deviceId`: kotlin.String?, `ownerPubkeyHex`: kotlin.String?): InviteAcceptResult + + /** + * Get the inviter's public key as hex. + */ + fun `getInviterPubkeyHex`(): kotlin.String + + /** + * Get the shared secret as hex. + */ + fun `getSharedSecretHex`(): kotlin.String + + /** + * Process an invite response event and create a session (inviter side). + * + * Returns `None` if the event is not a valid response for this invite. + */ + fun `processResponse`(`eventJson`: kotlin.String, `inviterPrivkeyHex`: kotlin.String): InviteProcessResult? + + /** + * Serialize the invite to JSON for persistence. + */ + fun `serialize`(): kotlin.String + + /** + * Update the owner pubkey embedded in invite URLs. + */ + fun `setOwnerPubkeyHex`(`ownerPubkeyHex`: kotlin.String?) + + /** + * Update the invite purpose (e.g. \"link\"). + */ + fun `setPurpose`(`purpose`: kotlin.String?) + + /** + * Convert the invite to a Nostr event JSON. + */ + fun `toEventJson`(): kotlin.String + + /** + * Convert the invite to a shareable URL. + */ + fun `toUrl`(`root`: kotlin.String): kotlin.String + + companion object +} + +/** + * FFI wrapper for Invite. + */ +open class InviteHandle: Disposable, AutoCloseable, InviteHandleInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_free_invitehandle(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_clone_invitehandle(pointer!!, status) + } + } + + + /** + * Accept the invite and create a session. + */ + @Throws(NdrException::class)override fun `accept`(`inviteePubkeyHex`: kotlin.String, `inviteePrivkeyHex`: kotlin.String, `deviceId`: kotlin.String?): InviteAcceptResult { + return FfiConverterTypeInviteAcceptResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_accept( + it, FfiConverterString.lower(`inviteePubkeyHex`),FfiConverterString.lower(`inviteePrivkeyHex`),FfiConverterOptionalString.lower(`deviceId`),_status) +} + } + ) + } + + + + /** + * Accept the invite as an owner and include the owner pubkey in the response payload. + */ + @Throws(NdrException::class)override fun `acceptWithOwner`(`inviteePubkeyHex`: kotlin.String, `inviteePrivkeyHex`: kotlin.String, `deviceId`: kotlin.String?, `ownerPubkeyHex`: kotlin.String?): InviteAcceptResult { + return FfiConverterTypeInviteAcceptResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_accept_with_owner( + it, FfiConverterString.lower(`inviteePubkeyHex`),FfiConverterString.lower(`inviteePrivkeyHex`),FfiConverterOptionalString.lower(`deviceId`),FfiConverterOptionalString.lower(`ownerPubkeyHex`),_status) +} + } + ) + } + + + + /** + * Get the inviter's public key as hex. + */override fun `getInviterPubkeyHex`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_get_inviter_pubkey_hex( + it, _status) +} + } + ) + } + + + + /** + * Get the shared secret as hex. + */override fun `getSharedSecretHex`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_get_shared_secret_hex( + it, _status) +} + } + ) + } + + + + /** + * Process an invite response event and create a session (inviter side). + * + * Returns `None` if the event is not a valid response for this invite. + */ + @Throws(NdrException::class)override fun `processResponse`(`eventJson`: kotlin.String, `inviterPrivkeyHex`: kotlin.String): InviteProcessResult? { + return FfiConverterOptionalTypeInviteProcessResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_process_response( + it, FfiConverterString.lower(`eventJson`),FfiConverterString.lower(`inviterPrivkeyHex`),_status) +} + } + ) + } + + + + /** + * Serialize the invite to JSON for persistence. + */ + @Throws(NdrException::class)override fun `serialize`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_serialize( + it, _status) +} + } + ) + } + + + + /** + * Update the owner pubkey embedded in invite URLs. + */ + @Throws(NdrException::class)override fun `setOwnerPubkeyHex`(`ownerPubkeyHex`: kotlin.String?) + = + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_set_owner_pubkey_hex( + it, FfiConverterOptionalString.lower(`ownerPubkeyHex`),_status) +} + } + + + + + /** + * Update the invite purpose (e.g. \"link\"). + */override fun `setPurpose`(`purpose`: kotlin.String?) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_set_purpose( + it, FfiConverterOptionalString.lower(`purpose`),_status) +} + } + + + + + /** + * Convert the invite to a Nostr event JSON. + */ + @Throws(NdrException::class)override fun `toEventJson`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_to_event_json( + it, _status) +} + } + ) + } + + + + /** + * Convert the invite to a shareable URL. + */ + @Throws(NdrException::class)override fun `toUrl`(`root`: kotlin.String): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_invitehandle_to_url( + it, FfiConverterString.lower(`root`),_status) +} + } + ) + } + + + + + + companion object { + + /** + * Create a new invite. + */ + @Throws(NdrException::class) fun `createNew`(`inviterPubkeyHex`: kotlin.String, `deviceId`: kotlin.String?, `maxUses`: kotlin.UInt?): InviteHandle { + return FfiConverterTypeInviteHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_invitehandle_create_new( + FfiConverterString.lower(`inviterPubkeyHex`),FfiConverterOptionalString.lower(`deviceId`),FfiConverterOptionalUInt.lower(`maxUses`),_status) +} + ) + } + + + + /** + * Deserialize an invite from JSON. + */ + @Throws(NdrException::class) fun `deserialize`(`json`: kotlin.String): InviteHandle { + return FfiConverterTypeInviteHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_invitehandle_deserialize( + FfiConverterString.lower(`json`),_status) +} + ) + } + + + + /** + * Parse an invite from a Nostr event JSON. + */ + @Throws(NdrException::class) fun `fromEventJson`(`eventJson`: kotlin.String): InviteHandle { + return FfiConverterTypeInviteHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_invitehandle_from_event_json( + FfiConverterString.lower(`eventJson`),_status) +} + ) + } + + + + /** + * Parse an invite from a URL. + */ + @Throws(NdrException::class) fun `fromUrl`(`url`: kotlin.String): InviteHandle { + return FfiConverterTypeInviteHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_invitehandle_from_url( + FfiConverterString.lower(`url`),_status) +} + ) + } + + + + } + +} + +/** + * @suppress + */ +public object FfiConverterTypeInviteHandle: FfiConverter { + + override fun lower(value: InviteHandle): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): InviteHandle { + return InviteHandle(value) + } + + override fun read(buf: ByteBuffer): InviteHandle { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: InviteHandle) = 8UL + + override fun write(value: InviteHandle, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +/** + * FFI wrapper for Session. + */ +public interface SessionHandleInterface { + + /** + * Check if the session is ready to send messages. + */ + fun `canSend`(): kotlin.Boolean + + /** + * Decrypt a received event. + */ + fun `decryptEvent`(`outerEventJson`: kotlin.String): DecryptResult + + /** + * Check if an event is a double-ratchet message. + */ + fun `isDrMessage`(`eventJson`: kotlin.String): kotlin.Boolean + + /** + * Send a text message. + */ + fun `sendText`(`text`: kotlin.String): SendResult + + /** + * Serialize the session state to JSON. + */ + fun `stateJson`(): kotlin.String + + companion object +} + +/** + * FFI wrapper for Session. + */ +open class SessionHandle: Disposable, AutoCloseable, SessionHandleInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_free_sessionhandle(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_clone_sessionhandle(pointer!!, status) + } + } + + + /** + * Check if the session is ready to send messages. + */override fun `canSend`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionhandle_can_send( + it, _status) +} + } + ) + } + + + + /** + * Decrypt a received event. + */ + @Throws(NdrException::class)override fun `decryptEvent`(`outerEventJson`: kotlin.String): DecryptResult { + return FfiConverterTypeDecryptResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionhandle_decrypt_event( + it, FfiConverterString.lower(`outerEventJson`),_status) +} + } + ) + } + + + + /** + * Check if an event is a double-ratchet message. + */override fun `isDrMessage`(`eventJson`: kotlin.String): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionhandle_is_dr_message( + it, FfiConverterString.lower(`eventJson`),_status) +} + } + ) + } + + + + /** + * Send a text message. + */ + @Throws(NdrException::class)override fun `sendText`(`text`: kotlin.String): SendResult { + return FfiConverterTypeSendResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionhandle_send_text( + it, FfiConverterString.lower(`text`),_status) +} + } + ) + } + + + + /** + * Serialize the session state to JSON. + */ + @Throws(NdrException::class)override fun `stateJson`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionhandle_state_json( + it, _status) +} + } + ) + } + + + + + + companion object { + + /** + * Restore a session from serialized state JSON. + */ + @Throws(NdrException::class) fun `fromStateJson`(`stateJson`: kotlin.String): SessionHandle { + return FfiConverterTypeSessionHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_sessionhandle_from_state_json( + FfiConverterString.lower(`stateJson`),_status) +} + ) + } + + + + /** + * Initialize a new session. + */ + @Throws(NdrException::class) fun `init`(`theirEphemeralPubkeyHex`: kotlin.String, `ourEphemeralPrivkeyHex`: kotlin.String, `isInitiator`: kotlin.Boolean, `sharedSecretHex`: kotlin.String, `name`: kotlin.String?): SessionHandle { + return FfiConverterTypeSessionHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_sessionhandle_init( + FfiConverterString.lower(`theirEphemeralPubkeyHex`),FfiConverterString.lower(`ourEphemeralPrivkeyHex`),FfiConverterBoolean.lower(`isInitiator`),FfiConverterString.lower(`sharedSecretHex`),FfiConverterOptionalString.lower(`name`),_status) +} + ) + } + + + + } + +} + +/** + * @suppress + */ +public object FfiConverterTypeSessionHandle: FfiConverter { + + override fun lower(value: SessionHandle): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): SessionHandle { + return SessionHandle(value) + } + + override fun read(buf: ByteBuffer): SessionHandle { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: SessionHandle) = 8UL + + override fun write(value: SessionHandle, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +/** + * FFI wrapper for SessionManager. + */ +public interface SessionManagerHandleInterface { + + /** + * Accept an invite event JSON using SessionManager's owner-aware routing/auth checks. + */ + fun `acceptInviteFromEventJson`(`eventJson`: kotlin.String, `ownerPubkeyHintHex`: kotlin.String?): SessionManagerAcceptInviteResult + + /** + * Accept an invite URL using SessionManager's owner-aware routing/auth checks. + * + * This flow also emits the signed invite response via SessionManager pubsub events, + * so hosts should continue draining and publishing `publish_signed` events. + */ + fun `acceptInviteFromUrl`(`inviteUrl`: kotlin.String, `ownerPubkeyHintHex`: kotlin.String?): SessionManagerAcceptInviteResult + + /** + * Drain pending pubsub events from the internal queue. + */ + fun `drainEvents`(): List + + /** + * Export the active session state for a peer. + */ + fun `getActiveSessionState`(`peerPubkeyHex`: kotlin.String): kotlin.String? + + /** + * Get our device id. + */ + fun `getDeviceId`(): kotlin.String + + /** + * Return the tracked message-push author pubkeys for a peer owner. + */ + fun `getMessagePushAuthorPubkeys`(`peerOwnerPubkeyHex`: kotlin.String): List + + /** + * Return pairwise session snapshots used for message-push routing for a peer owner. + */ + fun `getMessagePushSessionStates`(`peerOwnerPubkeyHex`: kotlin.String): List + + /** + * Get our public key as hex. + */ + fun `getOurPubkeyHex`(): kotlin.String + + /** + * Get owner public key as hex. + */ + fun `getOwnerPubkeyHex`(): kotlin.String + + /** + * Return the persisted user-record snapshot JSON for a peer owner, if present. + */ + fun `getStoredUserRecordJson`(`peerOwnerPubkeyHex`: kotlin.String): kotlin.String? + + /** + * Get total active sessions. + */ + fun `getTotalSessions`(): kotlin.ULong + + /** + * Create a group through the embedded GroupManager, with optional metadata fanout. + */ + fun `groupCreate`(`name`: kotlin.String, `memberOwnerPubkeys`: List, `fanoutMetadata`: kotlin.Boolean?, `nowMs`: kotlin.ULong?): GroupCreateResult + + /** + * Handle a decrypted pairwise session rumor that may carry sender-key distribution. + */ + fun `groupHandleIncomingSessionEvent`(`eventJson`: kotlin.String, `fromOwnerPubkeyHex`: kotlin.String, `fromSenderDevicePubkeyHex`: kotlin.String?): List + + /** + * Handle an incoming relay event that may be an encrypted one-to-many group outer event. + */ + fun `groupHandleOuterEvent`(`eventJson`: kotlin.String): GroupDecryptedResult? + + /** + * Return known sender-event pubkeys used for one-to-many group transport. + */ + fun `groupKnownSenderEventPubkeys`(): List + + /** + * Return the current group outer authors and which ones were newly added + * since the last sync plan request for this handle. + */ + fun `groupOuterSubscriptionPlan`(): GroupOuterSubscriptionPlanResult + + /** + * Remove a group from the embedded GroupManager. + */ + fun `groupRemove`(`groupId`: kotlin.String) + + /** + * Send a group event through GroupManager. + * + * Pairwise sender-key distribution rumors are sent through SessionManager sessions. + * The encrypted one-to-many outer event is emitted via the SessionManager pubsub queue. + */ + fun `groupSendEvent`(`groupId`: kotlin.String, `kind`: kotlin.UInt, `content`: kotlin.String, `tagsJson`: kotlin.String, `nowMs`: kotlin.ULong?): GroupSendResult + + /** + * Upsert group metadata into the embedded GroupManager. + */ + fun `groupUpsert`(`group`: FfiGroupData) + + /** + * Import a session state for a peer. + */ + fun `importSessionState`(`peerPubkeyHex`: kotlin.String, `stateJson`: kotlin.String, `deviceId`: kotlin.String?) + + /** + * Initialize the session manager (loads state, creates device invite, subscribes). + */ + fun `init`() + + /** + * List peer owner pubkeys known from loaded state or persisted storage. + */ + fun `knownPeerOwnerPubkeys`(): List + + /** + * Process a received Nostr event JSON. + */ + fun `processEvent`(`eventJson`: kotlin.String) + + /** + * Send an arbitrary inner rumor event to a recipient, returning stable inner id + outer ids. + * + * This is used for group chats where we need custom kinds/tags (e.g. group metadata kind 40, + * group-tagged chat messages kind 14, reactions kind 7, typing kind 25). + * + * The caller controls the inner rumor tags via `tags_json` (JSON array of string arrays). + * For group fan-out, do NOT include recipient-specific tags like `["p", ]` so + * the inner rumor id stays stable across all recipients. + */ + fun `sendEventWithInnerId`(`recipientPubkeyHex`: kotlin.String, `kind`: kotlin.UInt, `content`: kotlin.String, `tagsJson`: kotlin.String, `createdAtSeconds`: kotlin.ULong?): SendTextResult + + /** + * Send an emoji reaction (kind 7) to a specific message id. + */ + fun `sendReaction`(`recipientPubkeyHex`: kotlin.String, `messageId`: kotlin.String, `emoji`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): List + + /** + * Send a delivery/read receipt for messages. + */ + fun `sendReceipt`(`recipientPubkeyHex`: kotlin.String, `receiptType`: kotlin.String, `messageIds`: List, `expiresAtSeconds`: kotlin.ULong?): List + + /** + * Send an already-built rumor JSON to a recipient without rebuilding it. + * + * This preserves the original rumor pubkey, timestamp, tags, and id. + */ + fun `sendRumorJson`(`recipientPubkeyHex`: kotlin.String, `rumorJson`: kotlin.String): SendTextResult + + /** + * Send a text message to a recipient. + */ + fun `sendText`(`recipientPubkeyHex`: kotlin.String, `text`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): List + + /** + * Send a text message and return both the stable inner (rumor) id and the + * list of outer message event ids that were published. + */ + fun `sendTextWithInnerId`(`recipientPubkeyHex`: kotlin.String, `text`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): SendTextResult + + /** + * Send a typing indicator. + */ + fun `sendTyping`(`recipientPubkeyHex`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): List + + /** + * Subscribe to a user's AppKeys/device-invite streams and converge sessions. + */ + fun `setupUser`(`userPubkeyHex`: kotlin.String) + + companion object +} + +/** + * FFI wrapper for SessionManager. + */ +open class SessionManagerHandle: Disposable, AutoCloseable, SessionManagerHandleInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + /** + * Create a new session manager with an internal event queue. + */ + constructor(`ourPubkeyHex`: kotlin.String, `ourIdentityPrivkeyHex`: kotlin.String, `deviceId`: kotlin.String, `ownerPubkeyHex`: kotlin.String?) : + this( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_sessionmanagerhandle_new( + FfiConverterString.lower(`ourPubkeyHex`),FfiConverterString.lower(`ourIdentityPrivkeyHex`),FfiConverterString.lower(`deviceId`),FfiConverterOptionalString.lower(`ownerPubkeyHex`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_free_sessionmanagerhandle(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_clone_sessionmanagerhandle(pointer!!, status) + } + } + + + /** + * Accept an invite event JSON using SessionManager's owner-aware routing/auth checks. + */ + @Throws(NdrException::class)override fun `acceptInviteFromEventJson`(`eventJson`: kotlin.String, `ownerPubkeyHintHex`: kotlin.String?): SessionManagerAcceptInviteResult { + return FfiConverterTypeSessionManagerAcceptInviteResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_accept_invite_from_event_json( + it, FfiConverterString.lower(`eventJson`),FfiConverterOptionalString.lower(`ownerPubkeyHintHex`),_status) +} + } + ) + } + + + + /** + * Accept an invite URL using SessionManager's owner-aware routing/auth checks. + * + * This flow also emits the signed invite response via SessionManager pubsub events, + * so hosts should continue draining and publishing `publish_signed` events. + */ + @Throws(NdrException::class)override fun `acceptInviteFromUrl`(`inviteUrl`: kotlin.String, `ownerPubkeyHintHex`: kotlin.String?): SessionManagerAcceptInviteResult { + return FfiConverterTypeSessionManagerAcceptInviteResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_accept_invite_from_url( + it, FfiConverterString.lower(`inviteUrl`),FfiConverterOptionalString.lower(`ownerPubkeyHintHex`),_status) +} + } + ) + } + + + + /** + * Drain pending pubsub events from the internal queue. + */ + @Throws(NdrException::class)override fun `drainEvents`(): List { + return FfiConverterSequenceTypePubSubEvent.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_drain_events( + it, _status) +} + } + ) + } + + + + /** + * Export the active session state for a peer. + */ + @Throws(NdrException::class)override fun `getActiveSessionState`(`peerPubkeyHex`: kotlin.String): kotlin.String? { + return FfiConverterOptionalString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_active_session_state( + it, FfiConverterString.lower(`peerPubkeyHex`),_status) +} + } + ) + } + + + + /** + * Get our device id. + */override fun `getDeviceId`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_device_id( + it, _status) +} + } + ) + } + + + + /** + * Return the tracked message-push author pubkeys for a peer owner. + */ + @Throws(NdrException::class)override fun `getMessagePushAuthorPubkeys`(`peerOwnerPubkeyHex`: kotlin.String): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_message_push_author_pubkeys( + it, FfiConverterString.lower(`peerOwnerPubkeyHex`),_status) +} + } + ) + } + + + + /** + * Return pairwise session snapshots used for message-push routing for a peer owner. + */ + @Throws(NdrException::class)override fun `getMessagePushSessionStates`(`peerOwnerPubkeyHex`: kotlin.String): List { + return FfiConverterSequenceTypeMessagePushSessionStateResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_message_push_session_states( + it, FfiConverterString.lower(`peerOwnerPubkeyHex`),_status) +} + } + ) + } + + + + /** + * Get our public key as hex. + */override fun `getOurPubkeyHex`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_our_pubkey_hex( + it, _status) +} + } + ) + } + + + + /** + * Get owner public key as hex. + */override fun `getOwnerPubkeyHex`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_owner_pubkey_hex( + it, _status) +} + } + ) + } + + + + /** + * Return the persisted user-record snapshot JSON for a peer owner, if present. + */ + @Throws(NdrException::class)override fun `getStoredUserRecordJson`(`peerOwnerPubkeyHex`: kotlin.String): kotlin.String? { + return FfiConverterOptionalString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_stored_user_record_json( + it, FfiConverterString.lower(`peerOwnerPubkeyHex`),_status) +} + } + ) + } + + + + /** + * Get total active sessions. + */override fun `getTotalSessions`(): kotlin.ULong { + return FfiConverterULong.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_get_total_sessions( + it, _status) +} + } + ) + } + + + + /** + * Create a group through the embedded GroupManager, with optional metadata fanout. + */ + @Throws(NdrException::class)override fun `groupCreate`(`name`: kotlin.String, `memberOwnerPubkeys`: List, `fanoutMetadata`: kotlin.Boolean?, `nowMs`: kotlin.ULong?): GroupCreateResult { + return FfiConverterTypeGroupCreateResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_create( + it, FfiConverterString.lower(`name`),FfiConverterSequenceString.lower(`memberOwnerPubkeys`),FfiConverterOptionalBoolean.lower(`fanoutMetadata`),FfiConverterOptionalULong.lower(`nowMs`),_status) +} + } + ) + } + + + + /** + * Handle a decrypted pairwise session rumor that may carry sender-key distribution. + */ + @Throws(NdrException::class)override fun `groupHandleIncomingSessionEvent`(`eventJson`: kotlin.String, `fromOwnerPubkeyHex`: kotlin.String, `fromSenderDevicePubkeyHex`: kotlin.String?): List { + return FfiConverterSequenceTypeGroupDecryptedResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_handle_incoming_session_event( + it, FfiConverterString.lower(`eventJson`),FfiConverterString.lower(`fromOwnerPubkeyHex`),FfiConverterOptionalString.lower(`fromSenderDevicePubkeyHex`),_status) +} + } + ) + } + + + + /** + * Handle an incoming relay event that may be an encrypted one-to-many group outer event. + */ + @Throws(NdrException::class)override fun `groupHandleOuterEvent`(`eventJson`: kotlin.String): GroupDecryptedResult? { + return FfiConverterOptionalTypeGroupDecryptedResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_handle_outer_event( + it, FfiConverterString.lower(`eventJson`),_status) +} + } + ) + } + + + + /** + * Return known sender-event pubkeys used for one-to-many group transport. + */override fun `groupKnownSenderEventPubkeys`(): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_known_sender_event_pubkeys( + it, _status) +} + } + ) + } + + + + /** + * Return the current group outer authors and which ones were newly added + * since the last sync plan request for this handle. + */override fun `groupOuterSubscriptionPlan`(): GroupOuterSubscriptionPlanResult { + return FfiConverterTypeGroupOuterSubscriptionPlanResult.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_outer_subscription_plan( + it, _status) +} + } + ) + } + + + + /** + * Remove a group from the embedded GroupManager. + */override fun `groupRemove`(`groupId`: kotlin.String) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_remove( + it, FfiConverterString.lower(`groupId`),_status) +} + } + + + + + /** + * Send a group event through GroupManager. + * + * Pairwise sender-key distribution rumors are sent through SessionManager sessions. + * The encrypted one-to-many outer event is emitted via the SessionManager pubsub queue. + */ + @Throws(NdrException::class)override fun `groupSendEvent`(`groupId`: kotlin.String, `kind`: kotlin.UInt, `content`: kotlin.String, `tagsJson`: kotlin.String, `nowMs`: kotlin.ULong?): GroupSendResult { + return FfiConverterTypeGroupSendResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_send_event( + it, FfiConverterString.lower(`groupId`),FfiConverterUInt.lower(`kind`),FfiConverterString.lower(`content`),FfiConverterString.lower(`tagsJson`),FfiConverterOptionalULong.lower(`nowMs`),_status) +} + } + ) + } + + + + /** + * Upsert group metadata into the embedded GroupManager. + */ + @Throws(NdrException::class)override fun `groupUpsert`(`group`: FfiGroupData) + = + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_group_upsert( + it, FfiConverterTypeFfiGroupData.lower(`group`),_status) +} + } + + + + + /** + * Import a session state for a peer. + */ + @Throws(NdrException::class)override fun `importSessionState`(`peerPubkeyHex`: kotlin.String, `stateJson`: kotlin.String, `deviceId`: kotlin.String?) + = + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_import_session_state( + it, FfiConverterString.lower(`peerPubkeyHex`),FfiConverterString.lower(`stateJson`),FfiConverterOptionalString.lower(`deviceId`),_status) +} + } + + + + + /** + * Initialize the session manager (loads state, creates device invite, subscribes). + */ + @Throws(NdrException::class)override fun `init`() + = + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_init( + it, _status) +} + } + + + + + /** + * List peer owner pubkeys known from loaded state or persisted storage. + */override fun `knownPeerOwnerPubkeys`(): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_known_peer_owner_pubkeys( + it, _status) +} + } + ) + } + + + + /** + * Process a received Nostr event JSON. + */ + @Throws(NdrException::class)override fun `processEvent`(`eventJson`: kotlin.String) + = + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_process_event( + it, FfiConverterString.lower(`eventJson`),_status) +} + } + + + + + /** + * Send an arbitrary inner rumor event to a recipient, returning stable inner id + outer ids. + * + * This is used for group chats where we need custom kinds/tags (e.g. group metadata kind 40, + * group-tagged chat messages kind 14, reactions kind 7, typing kind 25). + * + * The caller controls the inner rumor tags via `tags_json` (JSON array of string arrays). + * For group fan-out, do NOT include recipient-specific tags like `["p", ]` so + * the inner rumor id stays stable across all recipients. + */ + @Throws(NdrException::class)override fun `sendEventWithInnerId`(`recipientPubkeyHex`: kotlin.String, `kind`: kotlin.UInt, `content`: kotlin.String, `tagsJson`: kotlin.String, `createdAtSeconds`: kotlin.ULong?): SendTextResult { + return FfiConverterTypeSendTextResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_event_with_inner_id( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterUInt.lower(`kind`),FfiConverterString.lower(`content`),FfiConverterString.lower(`tagsJson`),FfiConverterOptionalULong.lower(`createdAtSeconds`),_status) +} + } + ) + } + + + + /** + * Send an emoji reaction (kind 7) to a specific message id. + */ + @Throws(NdrException::class)override fun `sendReaction`(`recipientPubkeyHex`: kotlin.String, `messageId`: kotlin.String, `emoji`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_reaction( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterString.lower(`messageId`),FfiConverterString.lower(`emoji`),FfiConverterOptionalULong.lower(`expiresAtSeconds`),_status) +} + } + ) + } + + + + /** + * Send a delivery/read receipt for messages. + */ + @Throws(NdrException::class)override fun `sendReceipt`(`recipientPubkeyHex`: kotlin.String, `receiptType`: kotlin.String, `messageIds`: List, `expiresAtSeconds`: kotlin.ULong?): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_receipt( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterString.lower(`receiptType`),FfiConverterSequenceString.lower(`messageIds`),FfiConverterOptionalULong.lower(`expiresAtSeconds`),_status) +} + } + ) + } + + + + /** + * Send an already-built rumor JSON to a recipient without rebuilding it. + * + * This preserves the original rumor pubkey, timestamp, tags, and id. + */ + @Throws(NdrException::class)override fun `sendRumorJson`(`recipientPubkeyHex`: kotlin.String, `rumorJson`: kotlin.String): SendTextResult { + return FfiConverterTypeSendTextResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_rumor_json( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterString.lower(`rumorJson`),_status) +} + } + ) + } + + + + /** + * Send a text message to a recipient. + */ + @Throws(NdrException::class)override fun `sendText`(`recipientPubkeyHex`: kotlin.String, `text`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_text( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterString.lower(`text`),FfiConverterOptionalULong.lower(`expiresAtSeconds`),_status) +} + } + ) + } + + + + /** + * Send a text message and return both the stable inner (rumor) id and the + * list of outer message event ids that were published. + */ + @Throws(NdrException::class)override fun `sendTextWithInnerId`(`recipientPubkeyHex`: kotlin.String, `text`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): SendTextResult { + return FfiConverterTypeSendTextResult.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_text_with_inner_id( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterString.lower(`text`),FfiConverterOptionalULong.lower(`expiresAtSeconds`),_status) +} + } + ) + } + + + + /** + * Send a typing indicator. + */ + @Throws(NdrException::class)override fun `sendTyping`(`recipientPubkeyHex`: kotlin.String, `expiresAtSeconds`: kotlin.ULong?): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_send_typing( + it, FfiConverterString.lower(`recipientPubkeyHex`),FfiConverterOptionalULong.lower(`expiresAtSeconds`),_status) +} + } + ) + } + + + + /** + * Subscribe to a user's AppKeys/device-invite streams and converge sessions. + */ + @Throws(NdrException::class)override fun `setupUser`(`userPubkeyHex`: kotlin.String) + = + callWithPointer { + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_method_sessionmanagerhandle_setup_user( + it, FfiConverterString.lower(`userPubkeyHex`),_status) +} + } + + + + + + + companion object { + + /** + * Create a new session manager with file-backed storage. + */ + @Throws(NdrException::class) fun `newWithStoragePath`(`ourPubkeyHex`: kotlin.String, `ourIdentityPrivkeyHex`: kotlin.String, `deviceId`: kotlin.String, `storagePath`: kotlin.String, `ownerPubkeyHex`: kotlin.String?): SessionManagerHandle { + return FfiConverterTypeSessionManagerHandle.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_constructor_sessionmanagerhandle_new_with_storage_path( + FfiConverterString.lower(`ourPubkeyHex`),FfiConverterString.lower(`ourIdentityPrivkeyHex`),FfiConverterString.lower(`deviceId`),FfiConverterString.lower(`storagePath`),FfiConverterOptionalString.lower(`ownerPubkeyHex`),_status) +} + ) + } + + + + } + +} + +/** + * @suppress + */ +public object FfiConverterTypeSessionManagerHandle: FfiConverter { + + override fun lower(value: SessionManagerHandle): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): SessionManagerHandle { + return SessionManagerHandle(value) + } + + override fun read(buf: ByteBuffer): SessionManagerHandle { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: SessionManagerHandle) = 8UL + + override fun write(value: SessionManagerHandle, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + + +/** + * Result of decrypting a message. + */ +data class DecryptResult ( + var `plaintext`: kotlin.String, + var `innerEventJson`: kotlin.String +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeDecryptResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DecryptResult { + return DecryptResult( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: DecryptResult) = ( + FfiConverterString.allocationSize(value.`plaintext`) + + FfiConverterString.allocationSize(value.`innerEventJson`) + ) + + override fun write(value: DecryptResult, buf: ByteBuffer) { + FfiConverterString.write(value.`plaintext`, buf) + FfiConverterString.write(value.`innerEventJson`, buf) + } +} + + + +/** + * FFI-friendly device entry for AppKeys. + */ +data class FfiDeviceEntry ( + var `identityPubkeyHex`: kotlin.String, + var `createdAt`: kotlin.ULong, + var `deviceLabel`: kotlin.String?, + var `clientLabel`: kotlin.String? +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeFfiDeviceEntry: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): FfiDeviceEntry { + return FfiDeviceEntry( + FfiConverterString.read(buf), + FfiConverterULong.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + ) + } + + override fun allocationSize(value: FfiDeviceEntry) = ( + FfiConverterString.allocationSize(value.`identityPubkeyHex`) + + FfiConverterULong.allocationSize(value.`createdAt`) + + FfiConverterOptionalString.allocationSize(value.`deviceLabel`) + + FfiConverterOptionalString.allocationSize(value.`clientLabel`) + ) + + override fun write(value: FfiDeviceEntry, buf: ByteBuffer) { + FfiConverterString.write(value.`identityPubkeyHex`, buf) + FfiConverterULong.write(value.`createdAt`, buf) + FfiConverterOptionalString.write(value.`deviceLabel`, buf) + FfiConverterOptionalString.write(value.`clientLabel`, buf) + } +} + + + +/** + * FFI-friendly group metadata payload. + */ +data class FfiGroupData ( + var `id`: kotlin.String, + var `name`: kotlin.String, + var `description`: kotlin.String?, + var `picture`: kotlin.String?, + var `members`: List, + var `admins`: List, + var `createdAtMs`: kotlin.ULong, + var `secret`: kotlin.String?, + var `accepted`: kotlin.Boolean? +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeFfiGroupData: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): FfiGroupData { + return FfiGroupData( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterSequenceString.read(buf), + FfiConverterSequenceString.read(buf), + FfiConverterULong.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalBoolean.read(buf), + ) + } + + override fun allocationSize(value: FfiGroupData) = ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterString.allocationSize(value.`name`) + + FfiConverterOptionalString.allocationSize(value.`description`) + + FfiConverterOptionalString.allocationSize(value.`picture`) + + FfiConverterSequenceString.allocationSize(value.`members`) + + FfiConverterSequenceString.allocationSize(value.`admins`) + + FfiConverterULong.allocationSize(value.`createdAtMs`) + + FfiConverterOptionalString.allocationSize(value.`secret`) + + FfiConverterOptionalBoolean.allocationSize(value.`accepted`) + ) + + override fun write(value: FfiGroupData, buf: ByteBuffer) { + FfiConverterString.write(value.`id`, buf) + FfiConverterString.write(value.`name`, buf) + FfiConverterOptionalString.write(value.`description`, buf) + FfiConverterOptionalString.write(value.`picture`, buf) + FfiConverterSequenceString.write(value.`members`, buf) + FfiConverterSequenceString.write(value.`admins`, buf) + FfiConverterULong.write(value.`createdAtMs`, buf) + FfiConverterOptionalString.write(value.`secret`, buf) + FfiConverterOptionalBoolean.write(value.`accepted`, buf) + } +} + + + +/** + * FFI-friendly keypair with hex-encoded keys. + */ +data class FfiKeyPair ( + var `publicKeyHex`: kotlin.String, + var `privateKeyHex`: kotlin.String +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeFfiKeyPair: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): FfiKeyPair { + return FfiKeyPair( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: FfiKeyPair) = ( + FfiConverterString.allocationSize(value.`publicKeyHex`) + + FfiConverterString.allocationSize(value.`privateKeyHex`) + ) + + override fun write(value: FfiKeyPair, buf: ByteBuffer) { + FfiConverterString.write(value.`publicKeyHex`, buf) + FfiConverterString.write(value.`privateKeyHex`, buf) + } +} + + + +/** + * Metadata fanout summary for group creation. + */ +data class GroupCreateFanout ( + var `enabled`: kotlin.Boolean, + var `attempted`: kotlin.ULong, + var `succeeded`: List, + var `failed`: List +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeGroupCreateFanout: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): GroupCreateFanout { + return GroupCreateFanout( + FfiConverterBoolean.read(buf), + FfiConverterULong.read(buf), + FfiConverterSequenceString.read(buf), + FfiConverterSequenceString.read(buf), + ) + } + + override fun allocationSize(value: GroupCreateFanout) = ( + FfiConverterBoolean.allocationSize(value.`enabled`) + + FfiConverterULong.allocationSize(value.`attempted`) + + FfiConverterSequenceString.allocationSize(value.`succeeded`) + + FfiConverterSequenceString.allocationSize(value.`failed`) + ) + + override fun write(value: GroupCreateFanout, buf: ByteBuffer) { + FfiConverterBoolean.write(value.`enabled`, buf) + FfiConverterULong.write(value.`attempted`, buf) + FfiConverterSequenceString.write(value.`succeeded`, buf) + FfiConverterSequenceString.write(value.`failed`, buf) + } +} + + + +/** + * Result of creating a group through GroupManager. + */ +data class GroupCreateResult ( + var `group`: FfiGroupData, + var `metadataRumorJson`: kotlin.String?, + var `fanout`: GroupCreateFanout +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeGroupCreateResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): GroupCreateResult { + return GroupCreateResult( + FfiConverterTypeFfiGroupData.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterTypeGroupCreateFanout.read(buf), + ) + } + + override fun allocationSize(value: GroupCreateResult) = ( + FfiConverterTypeFfiGroupData.allocationSize(value.`group`) + + FfiConverterOptionalString.allocationSize(value.`metadataRumorJson`) + + FfiConverterTypeGroupCreateFanout.allocationSize(value.`fanout`) + ) + + override fun write(value: GroupCreateResult, buf: ByteBuffer) { + FfiConverterTypeFfiGroupData.write(value.`group`, buf) + FfiConverterOptionalString.write(value.`metadataRumorJson`, buf) + FfiConverterTypeGroupCreateFanout.write(value.`fanout`, buf) + } +} + + + +/** + * Decrypted group event returned by GroupManager. + */ +data class GroupDecryptedResult ( + var `groupId`: kotlin.String, + var `senderEventPubkeyHex`: kotlin.String, + var `senderDevicePubkeyHex`: kotlin.String, + var `senderOwnerPubkeyHex`: kotlin.String?, + var `outerEventId`: kotlin.String, + var `outerCreatedAt`: kotlin.ULong, + var `keyId`: kotlin.UInt, + var `messageNumber`: kotlin.UInt, + var `innerEventJson`: kotlin.String, + var `innerEventId`: kotlin.String +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeGroupDecryptedResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): GroupDecryptedResult { + return GroupDecryptedResult( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterString.read(buf), + FfiConverterULong.read(buf), + FfiConverterUInt.read(buf), + FfiConverterUInt.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: GroupDecryptedResult) = ( + FfiConverterString.allocationSize(value.`groupId`) + + FfiConverterString.allocationSize(value.`senderEventPubkeyHex`) + + FfiConverterString.allocationSize(value.`senderDevicePubkeyHex`) + + FfiConverterOptionalString.allocationSize(value.`senderOwnerPubkeyHex`) + + FfiConverterString.allocationSize(value.`outerEventId`) + + FfiConverterULong.allocationSize(value.`outerCreatedAt`) + + FfiConverterUInt.allocationSize(value.`keyId`) + + FfiConverterUInt.allocationSize(value.`messageNumber`) + + FfiConverterString.allocationSize(value.`innerEventJson`) + + FfiConverterString.allocationSize(value.`innerEventId`) + ) + + override fun write(value: GroupDecryptedResult, buf: ByteBuffer) { + FfiConverterString.write(value.`groupId`, buf) + FfiConverterString.write(value.`senderEventPubkeyHex`, buf) + FfiConverterString.write(value.`senderDevicePubkeyHex`, buf) + FfiConverterOptionalString.write(value.`senderOwnerPubkeyHex`, buf) + FfiConverterString.write(value.`outerEventId`, buf) + FfiConverterULong.write(value.`outerCreatedAt`, buf) + FfiConverterUInt.write(value.`keyId`, buf) + FfiConverterUInt.write(value.`messageNumber`, buf) + FfiConverterString.write(value.`innerEventJson`, buf) + FfiConverterString.write(value.`innerEventId`, buf) + } +} + + + +/** + * Shared outer-subscription sync plan for group sender-event authors. + */ +data class GroupOuterSubscriptionPlanResult ( + var `authors`: List, + var `addedAuthors`: List +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeGroupOuterSubscriptionPlanResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): GroupOuterSubscriptionPlanResult { + return GroupOuterSubscriptionPlanResult( + FfiConverterSequenceString.read(buf), + FfiConverterSequenceString.read(buf), + ) + } + + override fun allocationSize(value: GroupOuterSubscriptionPlanResult) = ( + FfiConverterSequenceString.allocationSize(value.`authors`) + + FfiConverterSequenceString.allocationSize(value.`addedAuthors`) + ) + + override fun write(value: GroupOuterSubscriptionPlanResult, buf: ByteBuffer) { + FfiConverterSequenceString.write(value.`authors`, buf) + FfiConverterSequenceString.write(value.`addedAuthors`, buf) + } +} + + + +/** + * Result of sending a group event through GroupManager. + */ +data class GroupSendResult ( + var `outerEventJson`: kotlin.String, + var `innerEventJson`: kotlin.String, + var `outerEventId`: kotlin.String, + var `innerEventId`: kotlin.String +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeGroupSendResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): GroupSendResult { + return GroupSendResult( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: GroupSendResult) = ( + FfiConverterString.allocationSize(value.`outerEventJson`) + + FfiConverterString.allocationSize(value.`innerEventJson`) + + FfiConverterString.allocationSize(value.`outerEventId`) + + FfiConverterString.allocationSize(value.`innerEventId`) + ) + + override fun write(value: GroupSendResult, buf: ByteBuffer) { + FfiConverterString.write(value.`outerEventJson`, buf) + FfiConverterString.write(value.`innerEventJson`, buf) + FfiConverterString.write(value.`outerEventId`, buf) + FfiConverterString.write(value.`innerEventId`, buf) + } +} + + + +/** + * Result of accepting an invite. + */ +data class InviteAcceptResult ( + var `session`: SessionHandle, + var `responseEventJson`: kotlin.String +) : Disposable { + + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + + Disposable.destroy(this.`session`) + + Disposable.destroy(this.`responseEventJson`) + + } + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeInviteAcceptResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): InviteAcceptResult { + return InviteAcceptResult( + FfiConverterTypeSessionHandle.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: InviteAcceptResult) = ( + FfiConverterTypeSessionHandle.allocationSize(value.`session`) + + FfiConverterString.allocationSize(value.`responseEventJson`) + ) + + override fun write(value: InviteAcceptResult, buf: ByteBuffer) { + FfiConverterTypeSessionHandle.write(value.`session`, buf) + FfiConverterString.write(value.`responseEventJson`, buf) + } +} + + + +/** + * Result of processing an invite response. + */ +data class InviteProcessResult ( + var `session`: SessionHandle, + var `inviteePubkeyHex`: kotlin.String, + var `deviceId`: kotlin.String?, + var `ownerPubkeyHex`: kotlin.String? +) : Disposable { + + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + + Disposable.destroy(this.`session`) + + Disposable.destroy(this.`inviteePubkeyHex`) + + Disposable.destroy(this.`deviceId`) + + Disposable.destroy(this.`ownerPubkeyHex`) + + } + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeInviteProcessResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): InviteProcessResult { + return InviteProcessResult( + FfiConverterTypeSessionHandle.read(buf), + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + ) + } + + override fun allocationSize(value: InviteProcessResult) = ( + FfiConverterTypeSessionHandle.allocationSize(value.`session`) + + FfiConverterString.allocationSize(value.`inviteePubkeyHex`) + + FfiConverterOptionalString.allocationSize(value.`deviceId`) + + FfiConverterOptionalString.allocationSize(value.`ownerPubkeyHex`) + ) + + override fun write(value: InviteProcessResult, buf: ByteBuffer) { + FfiConverterTypeSessionHandle.write(value.`session`, buf) + FfiConverterString.write(value.`inviteePubkeyHex`, buf) + FfiConverterOptionalString.write(value.`deviceId`, buf) + FfiConverterOptionalString.write(value.`ownerPubkeyHex`, buf) + } +} + + + +/** + * Session-state snapshot used to inspect message-push routing without reading storage files. + */ +data class MessagePushSessionStateResult ( + var `stateJson`: kotlin.String, + var `trackedSenderPubkeys`: List, + var `hasReceivingCapability`: kotlin.Boolean +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMessagePushSessionStateResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MessagePushSessionStateResult { + return MessagePushSessionStateResult( + FfiConverterString.read(buf), + FfiConverterSequenceString.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: MessagePushSessionStateResult) = ( + FfiConverterString.allocationSize(value.`stateJson`) + + FfiConverterSequenceString.allocationSize(value.`trackedSenderPubkeys`) + + FfiConverterBoolean.allocationSize(value.`hasReceivingCapability`) + ) + + override fun write(value: MessagePushSessionStateResult, buf: ByteBuffer) { + FfiConverterString.write(value.`stateJson`, buf) + FfiConverterSequenceString.write(value.`trackedSenderPubkeys`, buf) + FfiConverterBoolean.write(value.`hasReceivingCapability`, buf) + } +} + + + +/** + * Event emitted by SessionManager for external publish/subscribe handling. + */ +data class PubSubEvent ( + var `kind`: kotlin.String, + var `subid`: kotlin.String?, + var `filterJson`: kotlin.String?, + var `eventJson`: kotlin.String?, + var `senderPubkeyHex`: kotlin.String?, + var `content`: kotlin.String?, + var `eventId`: kotlin.String? +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypePubSubEvent: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): PubSubEvent { + return PubSubEvent( + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + ) + } + + override fun allocationSize(value: PubSubEvent) = ( + FfiConverterString.allocationSize(value.`kind`) + + FfiConverterOptionalString.allocationSize(value.`subid`) + + FfiConverterOptionalString.allocationSize(value.`filterJson`) + + FfiConverterOptionalString.allocationSize(value.`eventJson`) + + FfiConverterOptionalString.allocationSize(value.`senderPubkeyHex`) + + FfiConverterOptionalString.allocationSize(value.`content`) + + FfiConverterOptionalString.allocationSize(value.`eventId`) + ) + + override fun write(value: PubSubEvent, buf: ByteBuffer) { + FfiConverterString.write(value.`kind`, buf) + FfiConverterOptionalString.write(value.`subid`, buf) + FfiConverterOptionalString.write(value.`filterJson`, buf) + FfiConverterOptionalString.write(value.`eventJson`, buf) + FfiConverterOptionalString.write(value.`senderPubkeyHex`, buf) + FfiConverterOptionalString.write(value.`content`, buf) + FfiConverterOptionalString.write(value.`eventId`, buf) + } +} + + + +/** + * Result of sending a message. + */ +data class SendResult ( + var `outerEventJson`: kotlin.String, + var `innerEventJson`: kotlin.String +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeSendResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): SendResult { + return SendResult( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: SendResult) = ( + FfiConverterString.allocationSize(value.`outerEventJson`) + + FfiConverterString.allocationSize(value.`innerEventJson`) + ) + + override fun write(value: SendResult, buf: ByteBuffer) { + FfiConverterString.write(value.`outerEventJson`, buf) + FfiConverterString.write(value.`innerEventJson`, buf) + } +} + + + +/** + * Result of sending a text message including stable inner id. + */ +data class SendTextResult ( + var `innerId`: kotlin.String, + var `outerEventIds`: List +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeSendTextResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): SendTextResult { + return SendTextResult( + FfiConverterString.read(buf), + FfiConverterSequenceString.read(buf), + ) + } + + override fun allocationSize(value: SendTextResult) = ( + FfiConverterString.allocationSize(value.`innerId`) + + FfiConverterSequenceString.allocationSize(value.`outerEventIds`) + ) + + override fun write(value: SendTextResult, buf: ByteBuffer) { + FfiConverterString.write(value.`innerId`, buf) + FfiConverterSequenceString.write(value.`outerEventIds`, buf) + } +} + + + +/** + * Result of accepting an invite through SessionManager. + */ +data class SessionManagerAcceptInviteResult ( + var `ownerPubkeyHex`: kotlin.String, + var `inviterDevicePubkeyHex`: kotlin.String, + var `deviceId`: kotlin.String, + var `createdNewSession`: kotlin.Boolean +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeSessionManagerAcceptInviteResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): SessionManagerAcceptInviteResult { + return SessionManagerAcceptInviteResult( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: SessionManagerAcceptInviteResult) = ( + FfiConverterString.allocationSize(value.`ownerPubkeyHex`) + + FfiConverterString.allocationSize(value.`inviterDevicePubkeyHex`) + + FfiConverterString.allocationSize(value.`deviceId`) + + FfiConverterBoolean.allocationSize(value.`createdNewSession`) + ) + + override fun write(value: SessionManagerAcceptInviteResult, buf: ByteBuffer) { + FfiConverterString.write(value.`ownerPubkeyHex`, buf) + FfiConverterString.write(value.`inviterDevicePubkeyHex`, buf) + FfiConverterString.write(value.`deviceId`, buf) + FfiConverterBoolean.write(value.`createdNewSession`, buf) + } +} + + + + + +/** + * FFI-friendly error type. + */ +sealed class NdrException: kotlin.Exception() { + + class InvalidKey( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + class InvalidEvent( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + class CryptoFailure( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + class StateMismatch( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + class Serialization( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + class InviteException( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + class SessionNotReady( + + val v1: kotlin.String + ) : NdrException() { + override val message + get() = "v1=${ v1 }" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): NdrException = FfiConverterTypeNdrError.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeNdrError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): NdrException { + + + return when(buf.getInt()) { + 1 -> NdrException.InvalidKey( + FfiConverterString.read(buf), + ) + 2 -> NdrException.InvalidEvent( + FfiConverterString.read(buf), + ) + 3 -> NdrException.CryptoFailure( + FfiConverterString.read(buf), + ) + 4 -> NdrException.StateMismatch( + FfiConverterString.read(buf), + ) + 5 -> NdrException.Serialization( + FfiConverterString.read(buf), + ) + 6 -> NdrException.InviteException( + FfiConverterString.read(buf), + ) + 7 -> NdrException.SessionNotReady( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: NdrException): ULong { + return when(value) { + is NdrException.InvalidKey -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is NdrException.InvalidEvent -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is NdrException.CryptoFailure -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is NdrException.StateMismatch -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is NdrException.Serialization -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is NdrException.InviteException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is NdrException.SessionNotReady -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + } + } + + override fun write(value: NdrException, buf: ByteBuffer) { + when(value) { + is NdrException.InvalidKey -> { + buf.putInt(1) + FfiConverterString.write(value.v1, buf) + Unit + } + is NdrException.InvalidEvent -> { + buf.putInt(2) + FfiConverterString.write(value.v1, buf) + Unit + } + is NdrException.CryptoFailure -> { + buf.putInt(3) + FfiConverterString.write(value.v1, buf) + Unit + } + is NdrException.StateMismatch -> { + buf.putInt(4) + FfiConverterString.write(value.v1, buf) + Unit + } + is NdrException.Serialization -> { + buf.putInt(5) + FfiConverterString.write(value.v1, buf) + Unit + } + is NdrException.InviteException -> { + buf.putInt(6) + FfiConverterString.write(value.v1, buf) + Unit + } + is NdrException.SessionNotReady -> { + buf.putInt(7) + FfiConverterString.write(value.v1, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalUInt: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.UInt? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterUInt.read(buf) + } + + override fun allocationSize(value: kotlin.UInt?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterUInt.allocationSize(value) + } + } + + override fun write(value: kotlin.UInt?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterUInt.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalULong: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.ULong? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterULong.read(buf) + } + + override fun allocationSize(value: kotlin.ULong?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterULong.allocationSize(value) + } + } + + override fun write(value: kotlin.ULong?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterULong.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalBoolean: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.Boolean? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterBoolean.read(buf) + } + + override fun allocationSize(value: kotlin.Boolean?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterBoolean.allocationSize(value) + } + } + + override fun write(value: kotlin.Boolean?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterBoolean.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalString: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.String? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterString.read(buf) + } + + override fun allocationSize(value: kotlin.String?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterString.allocationSize(value) + } + } + + override fun write(value: kotlin.String?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterString.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalTypeGroupDecryptedResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): GroupDecryptedResult? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeGroupDecryptedResult.read(buf) + } + + override fun allocationSize(value: GroupDecryptedResult?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeGroupDecryptedResult.allocationSize(value) + } + } + + override fun write(value: GroupDecryptedResult?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeGroupDecryptedResult.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalTypeInviteProcessResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): InviteProcessResult? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeInviteProcessResult.read(buf) + } + + override fun allocationSize(value: InviteProcessResult?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeInviteProcessResult.allocationSize(value) + } + } + + override fun write(value: InviteProcessResult?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeInviteProcessResult.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceString: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterString.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterString.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeFfiDeviceEntry: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeFfiDeviceEntry.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeFfiDeviceEntry.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeFfiDeviceEntry.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeGroupDecryptedResult: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeGroupDecryptedResult.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeGroupDecryptedResult.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeGroupDecryptedResult.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeMessagePushSessionStateResult: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeMessagePushSessionStateResult.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeMessagePushSessionStateResult.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeMessagePushSessionStateResult.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypePubSubEvent: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypePubSubEvent.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypePubSubEvent.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypePubSubEvent.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceSequenceString: FfiConverterRustBuffer>> { + override fun read(buf: ByteBuffer): List> { + val len = buf.getInt() + return List>(len) { + FfiConverterSequenceString.read(buf) + } + } + + override fun allocationSize(value: List>): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterSequenceString.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List>, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterSequenceString.write(it, buf) + } + } +} + /** + * Create a signed AppKeys event JSON for publishing to relays. + */ + @Throws(NdrException::class) fun `createSignedAppKeysEvent`(`ownerPubkeyHex`: kotlin.String, `ownerPrivkeyHex`: kotlin.String, `devices`: List): kotlin.String { + return FfiConverterString.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_create_signed_app_keys_event( + FfiConverterString.lower(`ownerPubkeyHex`),FfiConverterString.lower(`ownerPrivkeyHex`),FfiConverterSequenceTypeFfiDeviceEntry.lower(`devices`),_status) +} + ) + } + + + /** + * Derive a public key from a hex-encoded private key. + */ + @Throws(NdrException::class) fun `derivePublicKey`(`privkeyHex`: kotlin.String): kotlin.String { + return FfiConverterString.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_derive_public_key( + FfiConverterString.lower(`privkeyHex`),_status) +} + ) + } + + + /** + * Generate a new keypair. + */ fun `generateKeypair`(): FfiKeyPair { + return FfiConverterTypeFfiKeyPair.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_generate_keypair( + _status) +} + ) + } + + + /** + * Parse an AppKeys event JSON and return the contained device entries. + */ + @Throws(NdrException::class) fun `parseAppKeysEvent`(`eventJson`: kotlin.String, `ownerPrivkeyHex`: kotlin.String?): List { + return FfiConverterSequenceTypeFfiDeviceEntry.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_parse_app_keys_event( + FfiConverterString.lower(`eventJson`),FfiConverterOptionalString.lower(`ownerPrivkeyHex`),_status) +} + ) + } + + + /** + * Resolve conversation routing candidates for a decrypted rumor. + */ fun `resolveConversationCandidatePubkeys`(`ownerPubkeyHex`: kotlin.String, `rumorPubkeyHex`: kotlin.String, `rumorTags`: List>, `senderPubkeyHex`: kotlin.String): List { + return FfiConverterSequenceString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_resolve_conversation_candidate_pubkeys( + FfiConverterString.lower(`ownerPubkeyHex`),FfiConverterString.lower(`rumorPubkeyHex`),FfiConverterSequenceSequenceString.lower(`rumorTags`),FfiConverterString.lower(`senderPubkeyHex`),_status) +} + ) + } + + + /** + * Resolve the latest authorized device list from a set of AppKeys event JSON strings. + */ + @Throws(NdrException::class) fun `resolveLatestAppKeysDevices`(`eventJsons`: List, `ownerPrivkeyHex`: kotlin.String?): List { + return FfiConverterSequenceTypeFfiDeviceEntry.lift( + uniffiRustCallWithError(NdrException) { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_resolve_latest_app_keys_devices( + FfiConverterSequenceString.lower(`eventJsons`),FfiConverterOptionalString.lower(`ownerPrivkeyHex`),_status) +} + ) + } + + + /** + * Returns the version of the ndr-ffi crate. + */ fun `version`(): kotlin.String { + return FfiConverterString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_ndr_ffi_fn_func_version( + _status) +} + ) + } + + + diff --git a/app/src/main/jniLibs/arm64-v8a/libndr_ffi.so b/app/src/main/jniLibs/arm64-v8a/libndr_ffi.so new file mode 100755 index 000000000..0308b53a3 Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libndr_ffi.so differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libndr_ffi.so b/app/src/main/jniLibs/armeabi-v7a/libndr_ffi.so new file mode 100755 index 000000000..ac4820b71 Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libndr_ffi.so differ diff --git a/app/src/main/jniLibs/x86/libndr_ffi.so b/app/src/main/jniLibs/x86/libndr_ffi.so new file mode 100755 index 000000000..9b2a2a333 Binary files /dev/null and b/app/src/main/jniLibs/x86/libndr_ffi.so differ diff --git a/app/src/main/jniLibs/x86_64/libndr_ffi.so b/app/src/main/jniLibs/x86_64/libndr_ffi.so new file mode 100755 index 000000000..b8b2277c0 Binary files /dev/null and b/app/src/main/jniLibs/x86_64/libndr_ffi.so differ diff --git a/app/src/main/ndr-ffi/VENDORED_FROM.md b/app/src/main/ndr-ffi/VENDORED_FROM.md new file mode 100644 index 000000000..77ab4dc84 --- /dev/null +++ b/app/src/main/ndr-ffi/VENDORED_FROM.md @@ -0,0 +1,29 @@ +# Android ndr-ffi provenance + +Vendored artifacts: + +- `app/src/main/java/uniffi/ndr_ffi/ndr_ffi.kt` +- `app/src/main/jniLibs/arm64-v8a/libndr_ffi.so` +- `app/src/main/jniLibs/armeabi-v7a/libndr_ffi.so` +- `app/src/main/jniLibs/x86/libndr_ffi.so` +- `app/src/main/jniLibs/x86_64/libndr_ffi.so` + +Source: + +- Repository: `https://github.com/mmalmi/nostr-double-ratchet.git` +- Crate: `rust/crates/ndr-ffi` +- Version: `v0.0.135` +- Source revision: `v0.0.100-58-g8d324ed` +- Commit: `8d324edac835fd3b69471340af8bd05525310dfe` +- Android build script: `scripts/mobile/build-android.sh` +- Android NDK used for the vendored refresh: `28.2.13676358` +- Release builds strip non-runtime symbol tables with the NDK `llvm-strip --strip-unneeded` tool. + +Refresh procedure: + +1. From the source repository, check out the recorded commit. +2. Run `ANDROID_NDK_HOME=/path/to/android-ndk NDK_HOME=/path/to/android-ndk scripts/mobile/build-android.sh --release`. +3. Copy `rust/target/android/jniLibs/*/libndr_ffi.so` into this module's `app/src/main/jniLibs/`. +4. Copy the generated Kotlin binding from `rust/target/android/bindings/` into `app/src/main/java/uniffi/ndr_ffi/ndr_ffi.kt`. + +Recorded on `2026-05-02T17:49:50Z`. diff --git a/app/src/test/kotlin/com/bitchat/android/mesh/MessageHandlerNdrTest.kt b/app/src/test/kotlin/com/bitchat/android/mesh/MessageHandlerNdrTest.kt new file mode 100644 index 000000000..61073429a --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/mesh/MessageHandlerNdrTest.kt @@ -0,0 +1,142 @@ +package com.bitchat.android.mesh + +import com.bitchat.android.model.NoisePayload +import com.bitchat.android.model.NoisePayloadType +import com.bitchat.android.model.RoutedPacket +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MessageHandlerNdrTest { + + @Test + fun handleNoiseEncryptedForwardsNdrPayloadToDelegate() { + val delegate = FakeDelegate() + val handler = MessageHandler( + myPeerID = "0011223344556677", + appContext = ApplicationProvider.getApplicationContext() + ) + handler.delegate = delegate + + val payload = NoisePayload( + type = NoisePayloadType.NDR_EVENT, + data = """{"id":"invite1","kind":30078}""".toByteArray() + ).encode() + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = byteArrayOf(0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17), + recipientID = byteArrayOf(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77), + timestamp = 123uL, + payload = payload, + signature = null, + ttl = 7u + ) + + kotlinx.coroutines.runBlocking { + handler.handleNoiseEncrypted(RoutedPacket(packet = packet, peerID = "1011121314151617")) + } + + assertEquals("1011121314151617", delegate.ndrPeerID) + assertEquals("""{"id":"invite1","kind":30078}""", delegate.ndrPayload) + assertEquals(123L, delegate.ndrTimestampMs) + } + + @Test + fun handleNoiseEncryptedReplaysQueuedPayloadAfterHandshake() { + val delegate = FakeDelegate().apply { + hasSession = false + decryptReturnsNull = true + } + val handler = MessageHandler( + myPeerID = "0011223344556677", + appContext = ApplicationProvider.getApplicationContext() + ) + handler.delegate = delegate + + val payload = NoisePayload( + type = NoisePayloadType.NDR_EVENT, + data = """{"id":"invite2","kind":30078}""".toByteArray() + ).encode() + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = byteArrayOf(0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17), + recipientID = byteArrayOf(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77), + timestamp = 456uL, + payload = payload, + signature = null, + ttl = 7u + ) + + kotlinx.coroutines.runBlocking { + handler.handleNoiseEncrypted(RoutedPacket(packet = packet, peerID = "1011121314151617")) + } + + assertNull(delegate.ndrPeerID) + + delegate.hasSession = true + delegate.decryptReturnsNull = false + + kotlinx.coroutines.runBlocking { + handler.flushPendingNoiseEncrypted("1011121314151617") + } + + assertEquals("1011121314151617", delegate.ndrPeerID) + assertEquals("""{"id":"invite2","kind":30078}""", delegate.ndrPayload) + assertEquals(456L, delegate.ndrTimestampMs) + } + + private class FakeDelegate : MessageHandlerDelegate { + var ndrPeerID: String? = null + var ndrPayload: String? = null + var ndrTimestampMs: Long? = null + var hasSession: Boolean = true + var decryptReturnsNull: Boolean = false + + override fun addOrUpdatePeer(peerID: String, nickname: String): Boolean = false + override fun removePeer(peerID: String) = Unit + override fun updatePeerNickname(peerID: String, nickname: String) = Unit + override fun getPeerNickname(peerID: String): String? = null + override fun getNetworkSize(): Int = 0 + override fun getMyNickname(): String? = null + override fun getPeerInfo(peerID: String): PeerInfo? = null + override fun updatePeerInfo( + peerID: String, + nickname: String, + noisePublicKey: ByteArray, + signingPublicKey: ByteArray, + isVerified: Boolean + ): Boolean = false + override fun sendPacket(packet: BitchatPacket) = Unit + override fun relayPacket(routed: RoutedPacket) = Unit + override fun getBroadcastRecipient(): ByteArray = ByteArray(0) + override fun verifySignature(packet: BitchatPacket, peerID: String): Boolean = true + override fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? = data + override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? = + if (decryptReturnsNull) null else encryptedData + override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean = true + override fun hasNoiseSession(peerID: String): Boolean = hasSession + override fun initiateNoiseHandshake(peerID: String) = Unit + override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? = null + override fun updatePeerIDBinding(newPeerID: String, nickname: String, publicKey: ByteArray, previousPeerID: String?) = Unit + override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? = null + override fun onMessageReceived(message: com.bitchat.android.model.BitchatMessage) = Unit + override fun onChannelLeave(channel: String, fromPeer: String) = Unit + override fun onDeliveryAckReceived(messageID: String, peerID: String) = Unit + override fun onReadReceiptReceived(messageID: String, peerID: String) = Unit + override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) = Unit + override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) = Unit + override fun onNdrEventReceived(peerID: String, payload: ByteArray, timestampMs: Long) { + ndrPeerID = peerID + ndrPayload = String(payload) + ndrTimestampMs = timestampMs + } + } +} diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NdrNostrServiceTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NdrNostrServiceTest.kt new file mode 100644 index 000000000..8a6726118 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NdrNostrServiceTest.kt @@ -0,0 +1,339 @@ +package com.bitchat.android.nostr + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class NdrNostrServiceTest { + + @Test + fun configureCachesInviteAndSkipsOobSubscriptions() { + val relayManager = FakeRelayManager() + val runtime = FakeNdrSessionManager().apply { + drainedEvents += NdrPubSubEvent( + kind = "publish_signed", + eventJson = """ + {"id":"invite1","pubkey":"sender","created_at":1,"kind":30078,"tags":[["l","double-ratchet/invites"]],"content":"invite","sig":"sig"} + """.trimIndent() + ) + drainedEvents += NdrPubSubEvent( + kind = "subscribe", + subid = "giftwrap-oob", + filterJson = """{"kinds":[1059],"#p":["peer"]}""" + ) + drainedEvents += NdrPubSubEvent( + kind = "subscribe", + subid = "messages", + filterJson = """{"authors":["peer"],"kinds":[1060]}""" + ) + } + val service = NdrNostrService( + relayManager = relayManager, + runtimeFactory = FakeNdrRuntimeFactory(runtime), + storageDirectoryProvider = { "/tmp/ndr-test" }, + deviceIdProvider = { "device-1" } + ) + + service.configureIfNeeded( + NostrIdentity( + privateKeyHex = "11".repeat(32), + publicKeyHex = "22".repeat(32), + npub = "npub-test", + createdAt = 1L + ) + ) + + assertEquals("invite1", NostrEvent.fromJsonString(service.currentInviteEventJson()!!)?.id) + assertEquals(listOf("messages"), relayManager.subscriptions.map { it.id }) + } + + @Test + fun processOutOfBandInviteReturnsGiftWrapResponseWithoutPublishingIt() { + val relayManager = FakeRelayManager() + val runtime = FakeNdrSessionManager().apply { + acceptInviteEvents += NdrPubSubEvent( + kind = "publish_signed", + eventJson = """ + {"id":"response1","pubkey":"sender","created_at":1,"kind":1059,"tags":[["p","peer"]],"content":"wrapped","sig":"sig"} + """.trimIndent() + ) + } + val service = NdrNostrService( + relayManager = relayManager, + runtimeFactory = FakeNdrRuntimeFactory(runtime), + storageDirectoryProvider = { "/tmp/ndr-test" }, + deviceIdProvider = { "device-1" } + ) + service.configureIfNeeded( + NostrIdentity( + privateKeyHex = "11".repeat(32), + publicKeyHex = "22".repeat(32), + npub = "npub-test", + createdAt = 1L + ) + ) + + val outbound = service.processOutOfBandEventJson( + """ + {"id":"invite1","pubkey":"sender","created_at":1,"kind":30078,"tags":[["l","double-ratchet/invites"]],"content":"invite","sig":"sig"} + """.trimIndent() + ) + + assertEquals(1, outbound.outboundPayloads.size) + assertEquals("response1", NostrEvent.fromJsonString(outbound.outboundPayloads.single())?.id) + assertTrue(relayManager.sentEvents.isEmpty()) + } + + @Test + fun inboundDecryptedMessageCallsCallback() { + val relayManager = FakeRelayManager() + val runtime = FakeNdrSessionManager().apply { + processEvents += NdrPubSubEvent( + kind = "decrypted_message", + senderPubkeyHex = "ab".repeat(32), + content = "bitchat1:payload", + eventId = "inner-1" + ) + } + val service = NdrNostrService( + relayManager = relayManager, + runtimeFactory = FakeNdrRuntimeFactory(runtime), + storageDirectoryProvider = { "/tmp/ndr-test" }, + deviceIdProvider = { "device-1" } + ) + service.configureIfNeeded( + NostrIdentity( + privateKeyHex = "11".repeat(32), + publicKeyHex = "22".repeat(32), + npub = "npub-test", + createdAt = 1L + ) + ) + + var message: NdrDecryptedMessage? = null + service.onDecryptedMessage = { message = it } + + service.processInboundRelayEvent( + NostrEvent( + id = "outer-1", + pubkey = "cd".repeat(32), + createdAt = 123, + kind = 1060, + tags = listOf(listOf("p", "22".repeat(32))), + content = "ciphertext", + sig = "sig" + ) + ) + + assertEquals("inner-1", message?.eventId) + assertEquals("bitchat1:payload", message?.content) + assertEquals("ab".repeat(32), message?.senderPubkeyHex) + assertNull(message?.innerEventJson) + } + + @Test + fun processOutOfBandResponseUsesAcceptedOwnerAsSessionLookupKey() { + val relayManager = FakeRelayManager() + val runtime = FakeNdrSessionManager( + activeSessionPeers = mutableSetOf("cc".repeat(32)) + ).apply { + acceptInviteEventResult = NdrAcceptInviteResult( + ownerPubkeyHex = "cc".repeat(32), + inviterDevicePubkeyHex = "aa".repeat(32), + deviceId = "device-1", + createdNewSession = true + ) + } + val service = NdrNostrService( + relayManager = relayManager, + runtimeFactory = FakeNdrRuntimeFactory(runtime), + storageDirectoryProvider = { "/tmp/ndr-test" }, + deviceIdProvider = { "device-1" } + ) + service.configureIfNeeded( + NostrIdentity( + privateKeyHex = "11".repeat(32), + publicKeyHex = "22".repeat(32), + npub = "npub-test", + createdAt = 1L + ) + ) + + val result = service.processOutOfBandEventJson( + """ + {"id":"invite1","pubkey":"${"aa".repeat(32)}","created_at":1,"kind":30078,"tags":[["l","double-ratchet/invites"]],"content":"invite","sig":"sig"} + """.trimIndent() + ) + + assertEquals("cc".repeat(32), result.sessionLookupPubkeyHex) + } + + @Test + fun sendIfPossibleReturnsFalseWhenNoActiveSessionExists() { + val relayManager = FakeRelayManager() + val runtime = FakeNdrSessionManager().apply { + sendTextResult = listOf("outer-1") + } + val service = NdrNostrService( + relayManager = relayManager, + runtimeFactory = FakeNdrRuntimeFactory(runtime), + storageDirectoryProvider = { "/tmp/ndr-test" }, + deviceIdProvider = { "device-1" } + ) + service.configureIfNeeded( + NostrIdentity( + privateKeyHex = "11".repeat(32), + publicKeyHex = "22".repeat(32), + npub = "npub-test", + createdAt = 1L + ) + ) + + assertFalse(service.sendIfPossible("hello", "aa".repeat(32))) + assertTrue(runtime.sendTextCalls.isEmpty()) + } + + @Test + fun sendIfPossibleReturnsTrueWhenActiveSessionQueuesNoRelayPublish() { + val peer = "aa".repeat(32) + val relayManager = FakeRelayManager() + val runtime = FakeNdrSessionManager(mutableSetOf(peer)).apply { + sendTextResult = emptyList() + } + val service = NdrNostrService( + relayManager = relayManager, + runtimeFactory = FakeNdrRuntimeFactory(runtime), + storageDirectoryProvider = { "/tmp/ndr-test" }, + deviceIdProvider = { "device-1" } + ) + service.configureIfNeeded( + NostrIdentity( + privateKeyHex = "11".repeat(32), + publicKeyHex = "22".repeat(32), + npub = "npub-test", + createdAt = 1L + ) + ) + + assertTrue(service.sendIfPossible("hello", peer)) + assertEquals(listOf(peer), runtime.sendTextCalls) + } + + private fun extractNostrKind(eventJson: String): Int { + return requireNotNull(NostrEvent.fromJsonString(eventJson)?.kind) + } + + private class FakeNdrRuntimeFactory( + private val runtime: FakeNdrSessionManager + ) : NdrSessionManagerFactory { + override fun newWithStoragePath( + ourPubkeyHex: String, + ourIdentityPrivkeyHex: String, + deviceId: String, + storagePath: String, + ownerPubkeyHex: String? + ): NdrSessionManager = runtime + } + + private class FakeRelayManager : NdrRelayManager { + data class Subscription(val id: String, val filter: NostrFilter) + + val subscriptions = mutableListOf() + val unsubscribed = mutableListOf() + val sentEvents = mutableListOf() + + override fun subscribe(filter: NostrFilter, id: String, handler: (NostrEvent) -> Unit) { + subscriptions += Subscription(id = id, filter = filter) + } + + override fun unsubscribe(id: String) { + unsubscribed += id + } + + override fun sendEvent(event: NostrEvent) { + sentEvents += event + } + } + + private class FakeNdrSessionManager( + private val activeSessionPeers: MutableSet = mutableSetOf() + ) : NdrSessionManager { + val drainedEvents = ArrayDeque() + val processedEvents = mutableListOf() + val acceptedInvites = mutableListOf() + val acceptedInviteUrls = mutableListOf() + val acceptInviteEvents = mutableListOf() + val acceptInviteUrlEvents = mutableListOf() + val processEvents = mutableListOf() + val acceptedInviteOwnerHints = mutableListOf() + val acceptedInviteUrlOwnerHints = mutableListOf() + val sendTextCalls = mutableListOf() + var acceptInviteEventResult = NdrAcceptInviteResult( + ownerPubkeyHex = "aa".repeat(32), + inviterDevicePubkeyHex = "bb".repeat(32), + deviceId = "device-1", + createdNewSession = true + ) + var acceptInviteUrlResult = NdrAcceptInviteResult( + ownerPubkeyHex = "aa".repeat(32), + inviterDevicePubkeyHex = "bb".repeat(32), + deviceId = "device-1", + createdNewSession = true + ) + var sendTextResult: List = listOf("outer-1") + + override fun init() = Unit + + override fun acceptInviteFromEventJson( + eventJson: String, + ownerPubkeyHintHex: String? + ): NdrAcceptInviteResult { + acceptedInvites += eventJson + acceptedInviteOwnerHints += ownerPubkeyHintHex + drainedEvents.addAll(acceptInviteEvents) + return acceptInviteEventResult + } + + override fun acceptInviteFromUrl( + inviteUrl: String, + ownerPubkeyHintHex: String? + ): NdrAcceptInviteResult { + acceptedInviteUrls += inviteUrl + acceptedInviteUrlOwnerHints += ownerPubkeyHintHex + drainedEvents.addAll(acceptInviteUrlEvents) + return acceptInviteUrlResult + } + + override fun processEvent(eventJson: String) { + processedEvents += eventJson + drainedEvents.addAll(processEvents) + } + + override fun drainEvents(): List = buildList { + while (drainedEvents.isNotEmpty()) { + add(drainedEvents.removeFirst()) + } + } + + override fun getActiveSessionState(peerPubkeyHex: String): String? { + return peerPubkeyHex.takeIf { activeSessionPeers.contains(it.lowercase()) }?.let { """{"peer":"$it"}""" } + } + + override fun sendText( + recipientPubkeyHex: String, + text: String, + expiresAtSeconds: ULong? + ): List { + sendTextCalls += recipientPubkeyHex + return sendTextResult + } + + override fun getOurPubkeyHex(): String = "22".repeat(32) + + override fun getTotalSessions(): ULong = 0u + + override fun destroy() = Unit + } +}