From 8daacb2dce9fadd208c6023812e3850bae769133 Mon Sep 17 00:00:00 2001 From: islam <2553451+qalandarov@users.noreply.github.com> Date: Thu, 18 Sep 2025 01:49:37 +0100 Subject: [PATCH] Update `MessageRouter` to use `Peer` --- bitchat/Models/Peer.swift | 8 +- bitchat/Services/MessageRouter.swift | 90 +++++++++++------------ bitchat/Services/PrivateChatManager.swift | 2 +- bitchat/Services/UnifiedPeerService.swift | 2 +- bitchat/ViewModels/ChatViewModel.swift | 20 ++--- 5 files changed, 64 insertions(+), 58 deletions(-) diff --git a/bitchat/Models/Peer.swift b/bitchat/Models/Peer.swift index 07c0b52c..c67079d6 100644 --- a/bitchat/Models/Peer.swift +++ b/bitchat/Models/Peer.swift @@ -61,7 +61,13 @@ extension Peer { /// Full Noise key hex (exact 64-hex) var isNoiseKeyHex: Bool { - id.count == Constants.maxIDLength && Data(hexString: id) != nil + noiseKey != nil + } + + /// Full Noise key (exact 64-hex) as Data + var noiseKey: Data? { + guard id.count == Constants.maxIDLength else { return nil } + return Data(hexString: id) } } diff --git a/bitchat/Services/MessageRouter.swift b/bitchat/Services/MessageRouter.swift index c30c0e74..8174264f 100644 --- a/bitchat/Services/MessageRouter.swift +++ b/bitchat/Services/MessageRouter.swift @@ -6,7 +6,7 @@ import Foundation final class MessageRouter { private let mesh: Transport private let nostr: NostrTransport - private var outbox: [String: [(content: String, nickname: String, messageID: String)]] = [:] // peerID -> queued messages + private var outbox: [Peer: [(content: String, nickname: String, messageID: String)]] = [:] // Peer -> queued messages init(mesh: Transport, nostr: NostrTransport) { self.mesh = mesh @@ -21,80 +21,80 @@ final class MessageRouter { ) { [weak self] note in guard let self = self else { return } if let data = note.userInfo?["peerPublicKey"] as? Data { - let peerID = Peer(publicKey: data).id + let peer = Peer(publicKey: data) Task { @MainActor in - self.flushOutbox(for: peerID) + self.flushOutbox(for: peer) } } // Handle key updates if let newKey = note.userInfo?["peerPublicKey"] as? Data, let _ = note.userInfo?["isKeyUpdate"] as? Bool { - let peerID = Peer(publicKey: newKey).id + let peer = Peer(publicKey: newKey) Task { @MainActor in - self.flushOutbox(for: peerID) + self.flushOutbox(for: peer) } } } } - func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) { - let reachableMesh = mesh.isPeerReachable(peerID) + func sendPrivate(_ content: String, to peer: Peer, recipientNickname: String, messageID: String) { + let reachableMesh = mesh.isPeerReachable(peer.id) if reachableMesh { - SecureLogger.debug("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + SecureLogger.debug("Routing PM via mesh (reachable) to \(peer.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) // BLEService will initiate a handshake if needed and queue the message - mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) - } else if canSendViaNostr(peerID: peerID) { - SecureLogger.debug("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session) - nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) + mesh.sendPrivateMessage(content, to: peer.id, recipientNickname: recipientNickname, messageID: messageID) + } else if canSendViaNostr(peer: peer) { + SecureLogger.debug("Routing PM via Nostr to \(peer.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + nostr.sendPrivateMessage(content, to: peer.id, recipientNickname: recipientNickname, messageID: messageID) } else { // Queue for later (when mesh connects or Nostr mapping appears) - if outbox[peerID] == nil { outbox[peerID] = [] } - outbox[peerID]?.append((content, recipientNickname, messageID)) - SecureLogger.debug("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))…", category: .session) + if outbox[peer] == nil { outbox[peer] = [] } + outbox[peer]?.append((content, recipientNickname, messageID)) + SecureLogger.debug("Queued PM for \(peer.id.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))…", category: .session) } } - func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) { + func sendReadReceipt(_ receipt: ReadReceipt, to peer: Peer) { // Prefer mesh for reachable peers; BLE will queue if handshake is needed - if mesh.isPeerReachable(peerID) { - SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session) - mesh.sendReadReceipt(receipt, to: peerID) + if mesh.isPeerReachable(peer.id) { + SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peer.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session) + mesh.sendReadReceipt(receipt, to: peer.id) } else { - SecureLogger.debug("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session) - nostr.sendReadReceipt(receipt, to: peerID) + SecureLogger.debug("Routing READ ack via Nostr to \(peer.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session) + nostr.sendReadReceipt(receipt, to: peer.id) } } - func sendDeliveryAck(_ messageID: String, to peerID: String) { - if mesh.isPeerReachable(peerID) { - SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session) - mesh.sendDeliveryAck(for: messageID, to: peerID) + func sendDeliveryAck(_ messageID: String, to peer: Peer) { + if mesh.isPeerReachable(peer.id) { + SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peer.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + mesh.sendDeliveryAck(for: messageID, to: peer.id) } else { - nostr.sendDeliveryAck(for: messageID, to: peerID) + nostr.sendDeliveryAck(for: messageID, to: peer.id) } } - func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { + func sendFavoriteNotification(to peer: Peer, isFavorite: Bool) { // Route via mesh when connected; else use Nostr - if mesh.isPeerConnected(peerID) { - mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) + if mesh.isPeerConnected(peer.id) { + mesh.sendFavoriteNotification(to: peer.id, isFavorite: isFavorite) } else { - nostr.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) + nostr.sendFavoriteNotification(to: peer.id, isFavorite: isFavorite) } } // MARK: - Outbox Management - private func canSendViaNostr(peerID: String) -> Bool { + private func canSendViaNostr(peer: Peer) -> Bool { // Two forms are supported: // - 64-hex Noise public key (32 bytes) // - 16-hex short peer ID (derived from Noise pubkey) - if peerID.count == 64, let noiseKey = Data(hexString: peerID) { + if let noiseKey = peer.noiseKey { if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), fav.peerNostrPublicKey != nil { return true } - } else if peerID.count == 16 { - if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Peer(str: peerID)), + } else if peer.isShort { + if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer), fav.peerNostrPublicKey != nil { return true } @@ -102,18 +102,18 @@ final class MessageRouter { return false } - func flushOutbox(for peerID: String) { - guard let queued = outbox[peerID], !queued.isEmpty else { return } - SecureLogger.debug("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", category: .session) + func flushOutbox(for peer: Peer) { + guard let queued = outbox[peer], !queued.isEmpty else { return } + SecureLogger.debug("Flushing outbox for \(peer.id.prefix(8))… count=\(queued.count)", category: .session) var remaining: [(content: String, nickname: String, messageID: String)] = [] // Prefer mesh if connected; else try Nostr if mapping exists for (content, nickname, messageID) in queued { - if mesh.isPeerReachable(peerID) { - SecureLogger.debug("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session) - mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) - } else if canSendViaNostr(peerID: peerID) { - SecureLogger.debug("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session) - nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) + if mesh.isPeerReachable(peer.id) { + SecureLogger.debug("Outbox -> mesh for \(peer.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + mesh.sendPrivateMessage(content, to: peer.id, recipientNickname: nickname, messageID: messageID) + } else if canSendViaNostr(peer: peer) { + SecureLogger.debug("Outbox -> Nostr for \(peer.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + nostr.sendPrivateMessage(content, to: peer.id, recipientNickname: nickname, messageID: messageID) } else { // Keep unsent items queued remaining.append((content, nickname, messageID)) @@ -121,9 +121,9 @@ final class MessageRouter { } // Persist only items we could not send if remaining.isEmpty { - outbox.removeValue(forKey: peerID) + outbox.removeValue(forKey: peer) } else { - outbox[peerID] = remaining + outbox[peer] = remaining } } diff --git a/bitchat/Services/PrivateChatManager.swift b/bitchat/Services/PrivateChatManager.swift index 7ed12bd4..0619ef58 100644 --- a/bitchat/Services/PrivateChatManager.swift +++ b/bitchat/Services/PrivateChatManager.swift @@ -231,7 +231,7 @@ final class PrivateChatManager: ObservableObject { if let router = messageRouter { SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router", category: .session) Task { @MainActor in - router.sendReadReceipt(receipt, to: senderPeerID) + router.sendReadReceipt(receipt, to: Peer(str: senderPeerID)) } } else { // Fallback: preserve previous behavior diff --git a/bitchat/Services/UnifiedPeerService.swift b/bitchat/Services/UnifiedPeerService.swift index 962c1c19..38a943e0 100644 --- a/bitchat/Services/UnifiedPeerService.swift +++ b/bitchat/Services/UnifiedPeerService.swift @@ -307,7 +307,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { // Send favorite notification to the peer via router (mesh or Nostr) if let router = messageRouter { - router.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite) + router.sendFavoriteNotification(to: Peer(str: peerID), isFavorite: !wasFavorite) } else { // Fallback to mesh-only if router not yet wired meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index d346dd70..6ca698e5 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -2284,7 +2284,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Send via appropriate transport (BLE if connected/reachable, else Nostr when possible) if isConnected || isReachable || (isMutualFavorite && hasNostrKey) { - messageRouter.sendPrivate(content, to: peerID, recipientNickname: recipientNickname ?? "user", messageID: messageID) + messageRouter.sendPrivate(content, to: Peer(str: peerID), recipientNickname: recipientNickname ?? "user", messageID: messageID) // Optimistically mark as sent for both transports; delivery/read will update subsequently if let idx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) { privateChats[peerID]?[idx].deliveryStatus = .sent @@ -2866,7 +2866,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { switch sessionState { case .established: // Send the message directly without going through sendPrivateMessage to avoid local echo - messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString) + messageRouter.sendPrivate(screenshotMessage, to: Peer(str: peerID), recipientNickname: peerNickname, messageID: UUID().uuidString) default: // Don't send screenshot notification if no session exists SecureLogger.debug("Skipping screenshot notification to \(peerID) - no established session", category: .security) @@ -2988,7 +2988,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return } // Use router to decide (mesh if reachable, else Nostr if available) - messageRouter.sendReadReceipt(receipt, to: actualPeerID) + messageRouter.sendReadReceipt(receipt, to: Peer(str: actualPeerID)) } @MainActor @@ -3049,7 +3049,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Use stable Noise key hex if available; else fall back to peerID let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID) let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname) - messageRouter.sendReadReceipt(receipt, to: recipPeer) + messageRouter.sendReadReceipt(receipt, to: Peer(str: recipPeer)) sentReadReceipts.insert(message.id) } } @@ -4623,7 +4623,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Flush any queued messages for this peer via router - messageRouter.flushOutbox(for: peerID) + messageRouter.flushOutbox(for: Peer(str: peerID)) } // @@ -5312,7 +5312,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { if let key { SecureLogger.debug("Sending DELIVERED ack for \(message.id.prefix(8))… via router", category: .session) - messageRouter.sendDeliveryAck(message.id, to: key.hexEncodedString()) + messageRouter.sendDeliveryAck(message.id, to: Peer(str: key.hexEncodedString())) } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { // Fallback: no Noise mapping yet — send directly to sender's Nostr pubkey let nt = NostrTransport(keychain: keychain) @@ -5333,7 +5333,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { if let key { let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname) SecureLogger.debug("Viewing chat; sending READ ack for \(message.id.prefix(8))… via router", category: .session) - messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString()) + messageRouter.sendReadReceipt(receipt, to: Peer(str: key.hexEncodedString())) sentReadReceipts.insert(message.id) } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { let nt = NostrTransport(keychain: keychain) @@ -5655,7 +5655,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor private func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) { let peerIDHex = noisePublicKey.hexEncodedString() - messageRouter.sendFavoriteNotification(to: peerIDHex, isFavorite: isFavorite) + messageRouter.sendFavoriteNotification(to: Peer(str: peerIDHex), isFavorite: isFavorite) } @MainActor @@ -5675,12 +5675,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Try mesh first for connected peers if meshService.isPeerConnected(peerID) { - messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) + messageRouter.sendFavoriteNotification(to: Peer(str: peerID), isFavorite: isFavorite) SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session) } else if let key = noiseKey { // Send via Nostr for offline peers (using router) let recipientPeerID = key.hexEncodedString() - messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite) + messageRouter.sendFavoriteNotification(to: Peer(str: recipientPeerID), isFavorite: isFavorite) } else { SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session) }