diff --git a/bitchat/Services/MeshTransportCapabilities.swift b/bitchat/Services/MeshTransportCapabilities.swift new file mode 100644 index 00000000..37535021 --- /dev/null +++ b/bitchat/Services/MeshTransportCapabilities.swift @@ -0,0 +1,61 @@ +import BitFoundation +import CoreBluetooth +import Foundation + +/// Optional transport capabilities, discovered with `as?` instead of casting +/// to a concrete transport class. `Transport` stays the contract every +/// transport genuinely implements; a capability protocol here is the +/// contract for one mesh-only feature surface, so app wiring depends on the +/// feature it needs rather than on `BLEService` itself. + +/// Radio-state reporting for transports backed by a local radio. +protocol BluetoothStateReporting: AnyObject { + func getCurrentBluetoothState() -> CBManagerState +} + +/// Panic-mode lifecycle for transports that own durable identity state. +/// A transport implementing this owns its own restart sequencing: +/// `completePanicReset` decides whether services come back, so generic +/// `startServices()` calls after a panic belong only to transports that +/// don't implement it. +protocol PanicResettingTransport: AnyObject { + /// Quiesces the radio and drains in-flight work ahead of a panic wipe. + func suspendForPanicReset() + /// Finishes a panic wipe, optionally restarting services. + func completePanicReset(restartServices: Bool) + /// Rotates the transport identity as part of a panic reset. + func resetIdentityForPanic(currentNickname: String, restartServices: Bool) +} + +/// Internet-gateway and geohash-bridge wiring surface (BLE mesh today). +/// Everything the gateway/bridge/courier services need from the mesh +/// transport, so their bootstrap wiring never touches the concrete class. +protocol MeshBridgingTransport: AnyObject { + // Runtime-advertised capability bits + func setLocalCapability(_ capability: PeerCapabilities, enabled: Bool) + func setLocalBridgeGeohash(_ cell: String?) + func advertisedBridgeGeohash() -> String? + + // Peers currently advertising bridging roles + func reachableGatewayPeers() -> [PeerID] + func reachableBridgePeers() -> [PeerID] + + // Gateway carrier packets (mesh <-> Nostr uplink/downlink) + @discardableResult + func sendNostrCarrier(_ payload: Data, to gatewayPeer: PeerID) -> Bool + func broadcastNostrCarrier(_ payload: Data) + /// Sink for received carrier packets (set once by app wiring; called on + /// the main actor after transport-level checks). + var onNostrCarrierPacket: (@MainActor (_ payload: Data, _ from: PeerID, _ directedToUs: Bool) -> Void)? { get set } + + // Bridge courier drops (sealed envelopes carried across the bridge) + func sealBridgeCourierEnvelope(_ content: String, messageID: String, recipientNoiseKey: Data) -> CourierEnvelope? + @discardableResult + func openBridgedCourierEnvelope(_ envelope: CourierEnvelope) -> Bool + @discardableResult + func deliverBridgedEnvelope(_ envelope: CourierEnvelope, to peerID: PeerID) -> Bool + func myNoiseStaticPublicKey() -> Data + func verifiedPeersWithNoiseKeys() -> [(peerID: PeerID, noiseKey: Data)] + /// Fired (off-main) when a signature-verified announce is processed. + var onVerifiedPeerAnnounce: ((_ peerID: PeerID) -> Void)? { get set } +} diff --git a/bitchat/Services/Transport.swift b/bitchat/Services/Transport.swift index 76e6406f..3c50ae37 100644 --- a/bitchat/Services/Transport.swift +++ b/bitchat/Services/Transport.swift @@ -450,3 +450,6 @@ extension BitchatDelegate { } extension BLEService: Transport {} +extension BLEService: BluetoothStateReporting {} +extension BLEService: PanicResettingTransport {} +extension BLEService: MeshBridgingTransport {} diff --git a/bitchat/ViewModels/ChatLifecycleCoordinator.swift b/bitchat/ViewModels/ChatLifecycleCoordinator.swift index 9b0fab0d..1ea87a41 100644 --- a/bitchat/ViewModels/ChatLifecycleCoordinator.swift +++ b/bitchat/ViewModels/ChatLifecycleCoordinator.swift @@ -106,8 +106,8 @@ extension ChatViewModel: ChatLifecycleContext { } func refreshBluetoothState() { - if let bleService = meshService as? BLEService { - updateBluetoothState(bleService.getCurrentBluetoothState()) + if let radio = meshService as? BluetoothStateReporting { + updateBluetoothState(radio.getCurrentBluetoothState()) } } diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 197616e7..d86a57fd 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -1564,8 +1564,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage // Quiesce the mesh before clearing stores. Identity replacement below // deliberately stays stopped until media deletion and marker commit. - if let bleService = meshService as? BLEService { - bleService.suspendForPanicReset() + if let panicTransport = meshService as? PanicResettingTransport { + panicTransport.suspendForPanicReset() } else { meshService.emergencyDisconnectAll() } @@ -1700,8 +1700,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage // Replace the BLE identity while keeping the radio stopped. It may // reopen only after the durable panic transaction commits. - if let bleService = meshService as? BLEService { - bleService.resetIdentityForPanic( + if let panicTransport = meshService as? PanicResettingTransport { + panicTransport.resetIdentityForPanic( currentNickname: nickname, restartServices: false ) @@ -1746,18 +1746,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage guard panicCompleted else { return false } - if let bleService = meshService as? BLEService { + if let panicTransport = meshService as? PanicResettingTransport { // Startup recovery reopens admission but leaves actual service // start to the bootstrapper immediately after this method. - bleService.completePanicReset( + panicTransport.completePanicReset( restartServices: restartServices ) } if restartServices { // All persistent state and media are gone. Bring each service back - // only now, under the new identity. - if !(meshService is BLEService) { + // only now, under the new identity — a panic-resetting transport + // owns its own restart sequencing above. + if !(meshService is PanicResettingTransport) { meshService.startServices() } diff --git a/bitchat/ViewModels/ChatViewModelBootstrapper.swift b/bitchat/ViewModels/ChatViewModelBootstrapper.swift index 99ca2886..997474dd 100644 --- a/bitchat/ViewModels/ChatViewModelBootstrapper.swift +++ b/bitchat/ViewModels/ChatViewModelBootstrapper.swift @@ -195,9 +195,8 @@ private extension ChatViewModelBootstrapper { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak viewModel] in guard let viewModel, - let bleService = viewModel.meshService as? BLEService else { return } - let state = bleService.getCurrentBluetoothState() - viewModel.updateBluetoothState(state) + let radio = viewModel.meshService as? BluetoothStateReporting else { return } + viewModel.updateBluetoothState(radio.getCurrentBluetoothState()) } viewModel.nostrRelayManager = NostrRelayManager.shared @@ -331,7 +330,7 @@ private extension ChatViewModelBootstrapper { func configureGateway() { // Gateway mode bridges BLE mesh <-> Nostr; a mock transport (tests) // has no carrier packets to bridge. - guard let bleService = viewModel.meshService as? BLEService else { return } + guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return } let gateway = GatewayService.shared gateway.publishToRelays = { event, geohash in @@ -410,7 +409,7 @@ private extension ChatViewModelBootstrapper { /// transport, the relay manager, location, and the public timeline. Same /// closure-injection style as `configureGateway`. func configureBridge() { - guard let bleService = viewModel.meshService as? BLEService else { return } + guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return } let bridge = BridgeService.shared let idBridge = viewModel.idBridge @@ -545,7 +544,7 @@ private extension ChatViewModelBootstrapper { /// manager, the mesh transport's sealing/opening primitives, the courier /// store, and the message router's deposit path. func configureBridgeCourier() { - guard let bleService = viewModel.meshService as? BLEService else { return } + guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return } let courier = BridgeCourierService.shared courier.bridgeEnabled = { BridgeService.shared.isEnabled }