mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 15:05:21 +00:00
Wire gateway/bridge/panic features to capability ports, not BLEService
App wiring discovered mesh-only features by casting the Transport to the concrete BLEService class in nine places. Those surfaces are now three capability protocols — BluetoothStateReporting, PanicResettingTransport, and MeshBridgingTransport — discovered with as? like any optional capability, so the bootstrapper, panic flow, and lifecycle coordinator no longer name the concrete transport at all. A future second mesh transport picks up gateway/bridge wiring and the panic lifecycle by conforming, and the remaining Transport god-protocol requirements can migrate to the same pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||||
|
}
|
||||||
@@ -450,3 +450,6 @@ extension BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension BLEService: Transport {}
|
extension BLEService: Transport {}
|
||||||
|
extension BLEService: BluetoothStateReporting {}
|
||||||
|
extension BLEService: PanicResettingTransport {}
|
||||||
|
extension BLEService: MeshBridgingTransport {}
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ extension ChatViewModel: ChatLifecycleContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func refreshBluetoothState() {
|
func refreshBluetoothState() {
|
||||||
if let bleService = meshService as? BLEService {
|
if let radio = meshService as? BluetoothStateReporting {
|
||||||
updateBluetoothState(bleService.getCurrentBluetoothState())
|
updateBluetoothState(radio.getCurrentBluetoothState())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1564,8 +1564,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
|||||||
|
|
||||||
// Quiesce the mesh before clearing stores. Identity replacement below
|
// Quiesce the mesh before clearing stores. Identity replacement below
|
||||||
// deliberately stays stopped until media deletion and marker commit.
|
// deliberately stays stopped until media deletion and marker commit.
|
||||||
if let bleService = meshService as? BLEService {
|
if let panicTransport = meshService as? PanicResettingTransport {
|
||||||
bleService.suspendForPanicReset()
|
panicTransport.suspendForPanicReset()
|
||||||
} else {
|
} else {
|
||||||
meshService.emergencyDisconnectAll()
|
meshService.emergencyDisconnectAll()
|
||||||
}
|
}
|
||||||
@@ -1700,8 +1700,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
|||||||
|
|
||||||
// Replace the BLE identity while keeping the radio stopped. It may
|
// Replace the BLE identity while keeping the radio stopped. It may
|
||||||
// reopen only after the durable panic transaction commits.
|
// reopen only after the durable panic transaction commits.
|
||||||
if let bleService = meshService as? BLEService {
|
if let panicTransport = meshService as? PanicResettingTransport {
|
||||||
bleService.resetIdentityForPanic(
|
panicTransport.resetIdentityForPanic(
|
||||||
currentNickname: nickname,
|
currentNickname: nickname,
|
||||||
restartServices: false
|
restartServices: false
|
||||||
)
|
)
|
||||||
@@ -1746,18 +1746,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
|||||||
|
|
||||||
guard panicCompleted else { return false }
|
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
|
// Startup recovery reopens admission but leaves actual service
|
||||||
// start to the bootstrapper immediately after this method.
|
// start to the bootstrapper immediately after this method.
|
||||||
bleService.completePanicReset(
|
panicTransport.completePanicReset(
|
||||||
restartServices: restartServices
|
restartServices: restartServices
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if restartServices {
|
if restartServices {
|
||||||
// All persistent state and media are gone. Bring each service back
|
// All persistent state and media are gone. Bring each service back
|
||||||
// only now, under the new identity.
|
// only now, under the new identity — a panic-resetting transport
|
||||||
if !(meshService is BLEService) {
|
// owns its own restart sequencing above.
|
||||||
|
if !(meshService is PanicResettingTransport) {
|
||||||
meshService.startServices()
|
meshService.startServices()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,9 +195,8 @@ private extension ChatViewModelBootstrapper {
|
|||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak viewModel] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak viewModel] in
|
||||||
guard let viewModel,
|
guard let viewModel,
|
||||||
let bleService = viewModel.meshService as? BLEService else { return }
|
let radio = viewModel.meshService as? BluetoothStateReporting else { return }
|
||||||
let state = bleService.getCurrentBluetoothState()
|
viewModel.updateBluetoothState(radio.getCurrentBluetoothState())
|
||||||
viewModel.updateBluetoothState(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.nostrRelayManager = NostrRelayManager.shared
|
viewModel.nostrRelayManager = NostrRelayManager.shared
|
||||||
@@ -331,7 +330,7 @@ private extension ChatViewModelBootstrapper {
|
|||||||
func configureGateway() {
|
func configureGateway() {
|
||||||
// Gateway mode bridges BLE mesh <-> Nostr; a mock transport (tests)
|
// Gateway mode bridges BLE mesh <-> Nostr; a mock transport (tests)
|
||||||
// has no carrier packets to bridge.
|
// 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
|
let gateway = GatewayService.shared
|
||||||
|
|
||||||
gateway.publishToRelays = { event, geohash in
|
gateway.publishToRelays = { event, geohash in
|
||||||
@@ -410,7 +409,7 @@ private extension ChatViewModelBootstrapper {
|
|||||||
/// transport, the relay manager, location, and the public timeline. Same
|
/// transport, the relay manager, location, and the public timeline. Same
|
||||||
/// closure-injection style as `configureGateway`.
|
/// closure-injection style as `configureGateway`.
|
||||||
func configureBridge() {
|
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 bridge = BridgeService.shared
|
||||||
let idBridge = viewModel.idBridge
|
let idBridge = viewModel.idBridge
|
||||||
|
|
||||||
@@ -545,7 +544,7 @@ private extension ChatViewModelBootstrapper {
|
|||||||
/// manager, the mesh transport's sealing/opening primitives, the courier
|
/// manager, the mesh transport's sealing/opening primitives, the courier
|
||||||
/// store, and the message router's deposit path.
|
/// store, and the message router's deposit path.
|
||||||
func configureBridgeCourier() {
|
func configureBridgeCourier() {
|
||||||
guard let bleService = viewModel.meshService as? BLEService else { return }
|
guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return }
|
||||||
let courier = BridgeCourierService.shared
|
let courier = BridgeCourierService.shared
|
||||||
|
|
||||||
courier.bridgeEnabled = { BridgeService.shared.isEnabled }
|
courier.bridgeEnabled = { BridgeService.shared.isEnabled }
|
||||||
|
|||||||
Reference in New Issue
Block a user