diff --git a/bitchat/Models/PeerID.swift b/bitchat/Models/PeerID.swift index 6cc13f94..20489fa3 100644 --- a/bitchat/Models/PeerID.swift +++ b/bitchat/Models/PeerID.swift @@ -72,6 +72,11 @@ extension PeerID { guard let str = String(data: data, encoding: .utf8) else { return nil } self.init(str: str) } + + /// Convenience init to "hide" hex-encoding implementation detail + init(hexData: Data) { + self.init(str: hexData.hexEncodedString()) + } } // MARK: - Noise Public Key Helpers diff --git a/bitchat/Services/MessageRouter.swift b/bitchat/Services/MessageRouter.swift index 1b1b60d6..536bb734 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: [PeerID: [(content: String, nickname: String, messageID: String)]] = [:] // peerID -> queued messages init(mesh: Transport, nostr: NostrTransport) { self.mesh = mesh @@ -21,7 +21,7 @@ final class MessageRouter { ) { [weak self] note in guard let self = self else { return } if let data = note.userInfo?["peerPublicKey"] as? Data { - let peerID = PeerID(publicKey: data).id + let peerID = PeerID(publicKey: data) Task { @MainActor in self.flushOutbox(for: peerID) } @@ -29,7 +29,7 @@ final class MessageRouter { // Handle key updates if let newKey = note.userInfo?["peerPublicKey"] as? Data, let _ = note.userInfo?["isKeyUpdate"] as? Bool { - let peerID = PeerID(publicKey: newKey).id + let peerID = PeerID(publicKey: newKey) Task { @MainActor in self.flushOutbox(for: peerID) } @@ -37,64 +37,64 @@ final class MessageRouter { } } - func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) { - let reachableMesh = mesh.isPeerReachable(peerID) + func sendPrivate(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) { + let reachableMesh = mesh.isPeerReachable(peerID.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 \(peerID.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) + mesh.sendPrivateMessage(content, to: peerID.id, 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) + SecureLogger.debug("Routing PM via Nostr to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + nostr.sendPrivateMessage(content, to: peerID.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) + SecureLogger.debug("Queued PM for \(peerID.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 peerID: PeerID) { // 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(peerID.id) { + SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session) + mesh.sendReadReceipt(receipt, to: peerID.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 \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session) + nostr.sendReadReceipt(receipt, to: peerID.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 peerID: PeerID) { + if mesh.isPeerReachable(peerID.id) { + SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + mesh.sendDeliveryAck(for: messageID, to: peerID.id) } else { - nostr.sendDeliveryAck(for: messageID, to: peerID) + nostr.sendDeliveryAck(for: messageID, to: peerID.id) } } - func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { + func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) { // Route via mesh when connected; else use Nostr - if mesh.isPeerConnected(peerID) { - mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) + if mesh.isPeerConnected(peerID.id) { + mesh.sendFavoriteNotification(to: peerID.id, isFavorite: isFavorite) } else { - nostr.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) + nostr.sendFavoriteNotification(to: peerID.id, isFavorite: isFavorite) } } // MARK: - Outbox Management - private func canSendViaNostr(peerID: String) -> Bool { + private func canSendViaNostr(peerID: PeerID) -> 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 = peerID.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(forPeerID: PeerID(str: peerID)), + } else if peerID.isShort { + if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID), fav.peerNostrPublicKey != nil { return true } @@ -102,18 +102,18 @@ final class MessageRouter { return false } - func flushOutbox(for peerID: String) { + func flushOutbox(for peerID: PeerID) { guard let queued = outbox[peerID], !queued.isEmpty else { return } - SecureLogger.debug("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", category: .session) + SecureLogger.debug("Flushing outbox for \(peerID.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) + if mesh.isPeerReachable(peerID.id) { + SecureLogger.debug("Outbox -> mesh for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + mesh.sendPrivateMessage(content, to: peerID.id, 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) + SecureLogger.debug("Outbox -> Nostr for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session) + nostr.sendPrivateMessage(content, to: peerID.id, recipientNickname: nickname, messageID: messageID) } else { // Keep unsent items queued remaining.append((content, nickname, messageID)) diff --git a/bitchat/Services/PrivateChatManager.swift b/bitchat/Services/PrivateChatManager.swift index 531b117e..85dbb638 100644 --- a/bitchat/Services/PrivateChatManager.swift +++ b/bitchat/Services/PrivateChatManager.swift @@ -113,7 +113,7 @@ final class PrivateChatManager: ObservableObject { if let router = messageRouter { SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.id.prefix(8))… via router", category: .session) Task { @MainActor in - router.sendReadReceipt(receipt, to: senderPeerID.id) + router.sendReadReceipt(receipt, to: senderPeerID) } } else { // Fallback: preserve previous behavior diff --git a/bitchat/Services/UnifiedPeerService.swift b/bitchat/Services/UnifiedPeerService.swift index ed405cd4..c7e2d2d0 100644 --- a/bitchat/Services/UnifiedPeerService.swift +++ b/bitchat/Services/UnifiedPeerService.swift @@ -306,7 +306,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: PeerID(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 dffdeb10..c5428e2d 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -2258,7 +2258,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: PeerID(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 @@ -2871,7 +2871,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: PeerID(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) @@ -2994,7 +2994,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: PeerID(str: actualPeerID)) } @MainActor @@ -3055,7 +3055,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: PeerID(str: recipPeer)) sentReadReceipts.insert(message.id) } } @@ -4622,7 +4622,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Flush any queued messages for this peer via router - messageRouter.flushOutbox(for: peerID) + messageRouter.flushOutbox(for: PeerID(str: peerID)) } // @@ -5276,7 +5276,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: PeerID(hexData: key)) } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { // Fallback: no Noise mapping yet — send directly to sender's Nostr pubkey let nt = NostrTransport(keychain: keychain) @@ -5297,7 +5297,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: PeerID(hexData: key)) sentReadReceipts.insert(message.id) } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { let nt = NostrTransport(keychain: keychain) @@ -5617,8 +5617,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: PeerID(hexData: noisePublicKey), isFavorite: isFavorite) } @MainActor @@ -5638,12 +5637,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: PeerID(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: PeerID(str: recipientPeerID), isFavorite: isFavorite) } else { SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session) }