mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:45:20 +00:00
PeerID 18/n: BitchatDelegate + Tests (#769)
This commit is contained in:
@@ -162,9 +162,9 @@ enum DeliveryStatus: Codable, Equatable, Hashable {
|
|||||||
|
|
||||||
protocol BitchatDelegate: AnyObject {
|
protocol BitchatDelegate: AnyObject {
|
||||||
func didReceiveMessage(_ message: BitchatMessage)
|
func didReceiveMessage(_ message: BitchatMessage)
|
||||||
func didConnectToPeer(_ peerID: String)
|
func didConnectToPeer(_ peerID: PeerID)
|
||||||
func didDisconnectFromPeer(_ peerID: String)
|
func didDisconnectFromPeer(_ peerID: PeerID)
|
||||||
func didUpdatePeerList(_ peers: [String])
|
func didUpdatePeerList(_ peers: [PeerID])
|
||||||
|
|
||||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||||
func isFavorite(fingerprint: String) -> Bool
|
func isFavorite(fingerprint: String) -> Bool
|
||||||
@@ -172,11 +172,11 @@ protocol BitchatDelegate: AnyObject {
|
|||||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
||||||
|
|
||||||
// Low-level events for better separation of concerns
|
// Low-level events for better separation of concerns
|
||||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date)
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date)
|
||||||
|
|
||||||
// Bluetooth state updates for user notifications
|
// Bluetooth state updates for user notifications
|
||||||
func didUpdateBluetoothState(_ state: CBManagerState)
|
func didUpdateBluetoothState(_ state: CBManagerState)
|
||||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date)
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide default implementation to make it effectively optional
|
// Provide default implementation to make it effectively optional
|
||||||
@@ -189,11 +189,11 @@ extension BitchatDelegate {
|
|||||||
// Default empty implementation
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||||
// Default empty implementation
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {
|
||||||
// Default empty implementation
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ final class BLEService: NSObject {
|
|||||||
var lastSeen: Date
|
var lastSeen: Date
|
||||||
}
|
}
|
||||||
private var peers: [String: PeerInfo] = [:]
|
private var peers: [String: PeerInfo] = [:]
|
||||||
|
private var currentPeerIDs: [PeerID] {
|
||||||
|
peers.keys.map { PeerID(str: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Efficient Message Deduplication
|
// 4. Efficient Message Deduplication
|
||||||
private let messageDeduplicator = MessageDeduplicator()
|
private let messageDeduplicator = MessageDeduplicator()
|
||||||
@@ -934,7 +937,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Get current peer list (after removal)
|
// Get current peer list (after removal)
|
||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||||
|
|
||||||
if let peerID = peerID {
|
if let peerID = peerID {
|
||||||
self.notifyPeerDisconnectedDebounced(peerID.id)
|
self.notifyPeerDisconnectedDebounced(peerID.id)
|
||||||
@@ -1355,7 +1358,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Get current peer list (after removal)
|
// Get current peer list (after removal)
|
||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||||
|
|
||||||
self.notifyPeerDisconnectedDebounced(peerID.id)
|
self.notifyPeerDisconnectedDebounced(peerID.id)
|
||||||
// Publish snapshots so UnifiedPeerService can refresh icons promptly
|
// Publish snapshots so UnifiedPeerService can refresh icons promptly
|
||||||
@@ -2549,11 +2552,11 @@ extension BLEService {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Get current peer list (after addition)
|
// Get current peer list (after addition)
|
||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||||
|
|
||||||
// Only notify of connection for new or reconnected peers when it is a direct announce
|
// Only notify of connection for new or reconnected peers when it is a direct announce
|
||||||
if (packet.ttl == self.messageTTL) && (isNewPeer || isReconnectedPeer) {
|
if (packet.ttl == self.messageTTL) && (isNewPeer || isReconnectedPeer) {
|
||||||
self.delegate?.didConnectToPeer(peerID)
|
self.delegate?.didConnectToPeer(PeerID(str: peerID))
|
||||||
// Schedule initial unicast sync to this peer
|
// Schedule initial unicast sync to this peer
|
||||||
self.gossipSyncManager?.scheduleInitialSyncToPeer(PeerID(str: peerID), delaySeconds: 1.0)
|
self.gossipSyncManager?.scheduleInitialSyncToPeer(PeerID(str: peerID), delaySeconds: 1.0)
|
||||||
}
|
}
|
||||||
@@ -2700,7 +2703,7 @@ extension BLEService {
|
|||||||
|
|
||||||
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
self?.delegate?.didReceivePublicMessage(from: peerID, nickname: senderNickname, content: content, timestamp: ts)
|
self?.delegate?.didReceivePublicMessage(from: PeerID(str: peerID), nickname: senderNickname, content: content, timestamp: ts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2738,6 +2741,7 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) {
|
private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) {
|
||||||
|
let peerID = PeerID(str: peerID)
|
||||||
SecureLogger.debug("🔐 handleNoiseEncrypted called for packet from \(peerID)")
|
SecureLogger.debug("🔐 handleNoiseEncrypted called for packet from \(peerID)")
|
||||||
|
|
||||||
guard let recipientID = packet.recipientID else {
|
guard let recipientID = packet.recipientID else {
|
||||||
@@ -2752,10 +2756,10 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update lastSeen for the peer we received from (important for private messages)
|
// Update lastSeen for the peer we received from (important for private messages)
|
||||||
updatePeerLastSeen(peerID)
|
updatePeerLastSeen(peerID.id)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let decrypted = try noiseService.decrypt(packet.payload, from: PeerID(str: peerID))
|
let decrypted = try noiseService.decrypt(packet.payload, from: peerID)
|
||||||
guard decrypted.count > 0 else { return }
|
guard decrypted.count > 0 else { return }
|
||||||
|
|
||||||
// First byte indicates the payload type
|
// First byte indicates the payload type
|
||||||
@@ -2795,8 +2799,8 @@ extension BLEService {
|
|||||||
// We received an encrypted message before establishing a session with this peer.
|
// We received an encrypted message before establishing a session with this peer.
|
||||||
// Trigger a handshake so future messages can be decrypted.
|
// Trigger a handshake so future messages can be decrypted.
|
||||||
SecureLogger.debug("🔑 Encrypted message from \(peerID) without session; initiating handshake")
|
SecureLogger.debug("🔑 Encrypted message from \(peerID) without session; initiating handshake")
|
||||||
if !noiseService.hasSession(with: PeerID(str: peerID)) {
|
if !noiseService.hasSession(with: peerID) {
|
||||||
initiateNoiseHandshake(with: peerID)
|
initiateNoiseHandshake(with: peerID.id)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("❌ Failed to decrypt message from \(peerID): \(error)")
|
SecureLogger.error("❌ Failed to decrypt message from \(peerID): \(error)")
|
||||||
@@ -2815,9 +2819,9 @@ extension BLEService {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Get current peer list (after removal)
|
// Get current peer list (after removal)
|
||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||||
|
|
||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(PeerID(str: peerID))
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2937,7 +2941,7 @@ extension BLEService {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
let last = recentDisconnectNotifies[peerID]
|
let last = recentDisconnectNotifies[peerID]
|
||||||
if last == nil || now.timeIntervalSince(last!) >= TransportConfig.bleDisconnectNotifyDebounceSeconds {
|
if last == nil || now.timeIntervalSince(last!) >= TransportConfig.bleDisconnectNotifyDebounceSeconds {
|
||||||
delegate?.didDisconnectFromPeer(peerID)
|
delegate?.didDisconnectFromPeer(PeerID(str: peerID))
|
||||||
recentDisconnectNotifies[peerID] = now
|
recentDisconnectNotifies[peerID] = now
|
||||||
} else {
|
} else {
|
||||||
// Suppressed duplicate disconnect notification
|
// Suppressed duplicate disconnect notification
|
||||||
@@ -3078,13 +3082,13 @@ extension BLEService {
|
|||||||
// Update UI if there were direct disconnections or offline removals
|
// Update UI if there were direct disconnections or offline removals
|
||||||
if !disconnectedPeers.isEmpty || removedOfflineCount > 0 {
|
if !disconnectedPeers.isEmpty || removedOfflineCount > 0 {
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
// Get current peer list (after removal)
|
// Get current peer list (after removal)
|
||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||||
|
|
||||||
for peerID in disconnectedPeers {
|
for peerID in disconnectedPeers {
|
||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(PeerID(str: peerID))
|
||||||
}
|
}
|
||||||
// Publish snapshots so UnifiedPeerService updates connection/reachability icons
|
// Publish snapshots so UnifiedPeerService updates connection/reachability icons
|
||||||
self.requestPeerDataPublish()
|
self.requestPeerDataPublish()
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
private let maxMessages = TransportConfig.meshTimelineCap // Maximum messages before oldest are removed
|
private let maxMessages = TransportConfig.meshTimelineCap // Maximum messages before oldest are removed
|
||||||
@Published var isConnected = false
|
@Published var isConnected = false
|
||||||
private var hasNotifiedNetworkAvailable = false
|
private var hasNotifiedNetworkAvailable = false
|
||||||
private var recentlySeenPeers: Set<String> = []
|
private var recentlySeenPeers: Set<PeerID> = []
|
||||||
private var lastNetworkNotificationTime = Date.distantPast
|
private var lastNetworkNotificationTime = Date.distantPast
|
||||||
private var networkResetTimer: Timer? = nil
|
private var networkResetTimer: Timer? = nil
|
||||||
private let networkResetGraceSeconds: TimeInterval = TransportConfig.networkResetGraceSeconds // avoid refiring on short drops/reconnects
|
private let networkResetGraceSeconds: TimeInterval = TransportConfig.networkResetGraceSeconds // avoid refiring on short drops/reconnects
|
||||||
@@ -4449,12 +4449,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Low-level BLE events
|
// Low-level BLE events
|
||||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch type {
|
switch type {
|
||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
|
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
|
||||||
let senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
|
let senderName = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? "Unknown"
|
||||||
let pmMentions = parseMentions(from: pm.content)
|
let pmMentions = parseMentions(from: pm.content)
|
||||||
let msg = BitchatMessage(
|
let msg = BitchatMessage(
|
||||||
id: pm.messageID,
|
id: pm.messageID,
|
||||||
@@ -4465,27 +4465,27 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: nickname,
|
recipientNickname: nickname,
|
||||||
senderPeerID: PeerID(str: peerID),
|
senderPeerID: peerID,
|
||||||
mentions: pmMentions.isEmpty ? nil : pmMentions
|
mentions: pmMentions.isEmpty ? nil : pmMentions
|
||||||
)
|
)
|
||||||
handlePrivateMessage(msg)
|
handlePrivateMessage(msg)
|
||||||
// Send delivery ACK back over BLE
|
// Send delivery ACK back over BLE
|
||||||
meshService.sendDeliveryAck(for: pm.messageID, to: PeerID(str: peerID))
|
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
||||||
|
|
||||||
case .delivered:
|
case .delivered:
|
||||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
if let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname {
|
||||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
if let messages = privateChats[peerID.id], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||||
privateChats[peerID]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
privateChats[peerID.id]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .readReceipt:
|
case .readReceipt:
|
||||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
if let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname {
|
||||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
if let messages = privateChats[peerID.id], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||||
privateChats[peerID]?[idx].deliveryStatus = .read(by: name, at: Date())
|
privateChats[peerID.id]?[idx].deliveryStatus = .read(by: name, at: Date())
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4496,10 +4496,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let myNoiseHex = meshService.getNoiseService().getStaticPublicKeyData().hexEncodedString().lowercased()
|
let myNoiseHex = meshService.getNoiseService().getStaticPublicKeyData().hexEncodedString().lowercased()
|
||||||
guard tlv.noiseKeyHex.lowercased() == myNoiseHex else { return }
|
guard tlv.noiseKeyHex.lowercased() == myNoiseHex else { return }
|
||||||
// Deduplicate: ignore if we've already responded to this nonce for this peer
|
// Deduplicate: ignore if we've already responded to this nonce for this peer
|
||||||
if let last = lastVerifyNonceByPeer[peerID], last == tlv.nonceA { return }
|
if let last = lastVerifyNonceByPeer[peerID.id], last == tlv.nonceA { return }
|
||||||
lastVerifyNonceByPeer[peerID] = tlv.nonceA
|
lastVerifyNonceByPeer[peerID.id] = tlv.nonceA
|
||||||
// Record inbound challenge time keyed by stable fingerprint if available
|
// Record inbound challenge time keyed by stable fingerprint if available
|
||||||
if let fp = getFingerprint(for: peerID) {
|
if let fp = getFingerprint(for: peerID.id) {
|
||||||
lastInboundVerifyChallengeAt[fp] = Date()
|
lastInboundVerifyChallengeAt[fp] = Date()
|
||||||
// If we've already verified this fingerprint locally, treat this as mutual and toast immediately (responder side)
|
// If we've already verified this fingerprint locally, treat this as mutual and toast immediately (responder side)
|
||||||
if verifiedFingerprints.contains(fp) {
|
if verifiedFingerprints.contains(fp) {
|
||||||
@@ -4507,7 +4507,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let last = lastMutualToastAt[fp] ?? .distantPast
|
let last = lastMutualToastAt[fp] ?? .distantPast
|
||||||
if now.timeIntervalSince(last) > 60 { // 1-minute throttle
|
if now.timeIntervalSince(last) > 60 { // 1-minute throttle
|
||||||
lastMutualToastAt[fp] = now
|
lastMutualToastAt[fp] = now
|
||||||
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? resolveNickname(for: peerID.id)
|
||||||
NotificationService.shared.sendLocalNotification(
|
NotificationService.shared.sendLocalNotification(
|
||||||
title: "Mutual verification",
|
title: "Mutual verification",
|
||||||
body: "You and \(name) verified each other",
|
body: "You and \(name) verified each other",
|
||||||
@@ -4516,24 +4516,24 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meshService.sendVerifyResponse(to: PeerID(str: peerID), noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
meshService.sendVerifyResponse(to: peerID, noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
||||||
// Silent response: no toast needed on responder
|
// Silent response: no toast needed on responder
|
||||||
case .verifyResponse:
|
case .verifyResponse:
|
||||||
guard let resp = VerificationService.shared.parseVerifyResponse(payload) else { return }
|
guard let resp = VerificationService.shared.parseVerifyResponse(payload) else { return }
|
||||||
// Check pending for this peer
|
// Check pending for this peer
|
||||||
guard let pending = pendingQRVerifications[peerID] else { return }
|
guard let pending = pendingQRVerifications[peerID.id] else { return }
|
||||||
guard resp.noiseKeyHex.lowercased() == pending.noiseKeyHex.lowercased(), resp.nonceA == pending.nonceA else { return }
|
guard resp.noiseKeyHex.lowercased() == pending.noiseKeyHex.lowercased(), resp.nonceA == pending.nonceA else { return }
|
||||||
// Verify signature with expected sign key
|
// Verify signature with expected sign key
|
||||||
let ok = VerificationService.shared.verifyResponseSignature(noiseKeyHex: resp.noiseKeyHex, nonceA: resp.nonceA, signature: resp.signature, signerPublicKeyHex: pending.signKeyHex)
|
let ok = VerificationService.shared.verifyResponseSignature(noiseKeyHex: resp.noiseKeyHex, nonceA: resp.nonceA, signature: resp.signature, signerPublicKeyHex: pending.signKeyHex)
|
||||||
if ok {
|
if ok {
|
||||||
pendingQRVerifications.removeValue(forKey: peerID)
|
pendingQRVerifications.removeValue(forKey: peerID.id)
|
||||||
if let fp = getFingerprint(for: peerID) {
|
if let fp = getFingerprint(for: peerID.id) {
|
||||||
let short = fp.prefix(8)
|
let short = fp.prefix(8)
|
||||||
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
||||||
identityManager.setVerified(fingerprint: fp, verified: true)
|
identityManager.setVerified(fingerprint: fp, verified: true)
|
||||||
identityManager.forceSave()
|
identityManager.forceSave()
|
||||||
verifiedFingerprints.insert(fp)
|
verifiedFingerprints.insert(fp)
|
||||||
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? resolveNickname(for: peerID.id)
|
||||||
NotificationService.shared.sendLocalNotification(
|
NotificationService.shared.sendLocalNotification(
|
||||||
title: "Verified",
|
title: "Verified",
|
||||||
body: "You verified \(name)",
|
body: "You verified \(name)",
|
||||||
@@ -4552,14 +4552,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateEncryptionStatus(for: peerID)
|
updateEncryptionStatus(for: peerID.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let normalized = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
let normalized = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let publicMentions = parseMentions(from: normalized)
|
let publicMentions = parseMentions(from: normalized)
|
||||||
@@ -4572,7 +4572,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
recipientNickname: nil,
|
recipientNickname: nil,
|
||||||
senderPeerID: PeerID(str: peerID),
|
senderPeerID: peerID,
|
||||||
mentions: publicMentions.isEmpty ? nil : publicMentions
|
mentions: publicMentions.isEmpty ? nil : publicMentions
|
||||||
)
|
)
|
||||||
handlePublicMessage(msg)
|
handlePublicMessage(msg)
|
||||||
@@ -4622,7 +4622,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// MARK: - Peer Connection Events
|
// MARK: - Peer Connection Events
|
||||||
|
|
||||||
func didConnectToPeer(_ peerID: String) {
|
func didConnectToPeer(_ peerID: PeerID) {
|
||||||
SecureLogger.debug("🤝 Peer connected: \(peerID)", category: .session)
|
SecureLogger.debug("🤝 Peer connected: \(peerID)", category: .session)
|
||||||
|
|
||||||
// Handle all main actor work async
|
// Handle all main actor work async
|
||||||
@@ -4630,7 +4630,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isConnected = true
|
isConnected = true
|
||||||
|
|
||||||
// Register ephemeral session with identity manager
|
// Register ephemeral session with identity manager
|
||||||
identityManager.registerEphemeralSession(peerID: PeerID(str: peerID), handshakeState: .none)
|
identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
||||||
|
|
||||||
// Intentionally do not resend favorites on reconnect.
|
// Intentionally do not resend favorites on reconnect.
|
||||||
// We only send our npub when a favorite is toggled on, or if our npub changes.
|
// We only send our npub when a favorite is toggled on, or if our npub changes.
|
||||||
@@ -4639,36 +4639,34 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
||||||
// Cache mapping to full Noise key for session continuity on disconnect
|
// Cache mapping to full Noise key for session continuity on disconnect
|
||||||
if let peer = unifiedPeerService.getPeer(by: peerID) {
|
if let peer = unifiedPeerService.getPeer(by: peerID.id) {
|
||||||
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
|
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
|
||||||
shortIDToNoiseKey[peerID] = noiseKeyHex
|
shortIDToNoiseKey[peerID.id] = noiseKeyHex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush any queued messages for this peer via router
|
// Flush any queued messages for this peer via router
|
||||||
messageRouter.flushOutbox(for: PeerID(str: peerID))
|
messageRouter.flushOutbox(for: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didDisconnectFromPeer(_ peerID: String) {
|
func didDisconnectFromPeer(_ peerID: PeerID) {
|
||||||
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
||||||
|
|
||||||
// Remove ephemeral session from identity manager
|
// Remove ephemeral session from identity manager
|
||||||
identityManager.removeEphemeralSession(peerID: PeerID(str: peerID))
|
identityManager.removeEphemeralSession(peerID: peerID)
|
||||||
|
|
||||||
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
|
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
|
||||||
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID]
|
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID.id]
|
||||||
if derivedStableKeyHex == nil,
|
if derivedStableKeyHex == nil,
|
||||||
let key = meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: peerID)) {
|
let key = meshService.getNoiseService().getPeerPublicKeyData(peerID) {
|
||||||
derivedStableKeyHex = key.hexEncodedString()
|
derivedStableKeyHex = key.hexEncodedString()
|
||||||
shortIDToNoiseKey[peerID] = derivedStableKeyHex
|
shortIDToNoiseKey[peerID.id] = derivedStableKeyHex
|
||||||
}
|
}
|
||||||
|
|
||||||
if let current = selectedPrivateChatPeer, current == peerID,
|
if let current = selectedPrivateChatPeer, current == peerID,
|
||||||
let stableKeyHex = derivedStableKeyHex {
|
let stableKeyHex = derivedStableKeyHex {
|
||||||
// Migrate messages view context to stable key so header shows favorite + Nostr globe
|
// Migrate messages view context to stable key so header shows favorite + Nostr globe
|
||||||
if let messages = privateChats[peerID] {
|
if let messages = privateChats[peerID.id] {
|
||||||
if privateChats[stableKeyHex] == nil { privateChats[stableKeyHex] = [] }
|
if privateChats[stableKeyHex] == nil { privateChats[stableKeyHex] = [] }
|
||||||
let existing = Set(privateChats[stableKeyHex]!.map { $0.id })
|
let existing = Set(privateChats[stableKeyHex]!.map { $0.id })
|
||||||
for msg in messages where !existing.contains(msg.id) {
|
for msg in messages where !existing.contains(msg.id) {
|
||||||
@@ -4688,10 +4686,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
privateChats[stableKeyHex]?.append(updated)
|
privateChats[stableKeyHex]?.append(updated)
|
||||||
}
|
}
|
||||||
privateChats[stableKeyHex]?.sort { $0.timestamp < $1.timestamp }
|
privateChats[stableKeyHex]?.sort { $0.timestamp < $1.timestamp }
|
||||||
privateChats.removeValue(forKey: peerID)
|
privateChats.removeValue(forKey: peerID.id)
|
||||||
}
|
}
|
||||||
if unreadPrivateMessages.contains(peerID) {
|
if unreadPrivateMessages.contains(peerID.id) {
|
||||||
unreadPrivateMessages.remove(peerID)
|
unreadPrivateMessages.remove(peerID.id)
|
||||||
unreadPrivateMessages.insert(stableKeyHex)
|
unreadPrivateMessages.insert(stableKeyHex)
|
||||||
}
|
}
|
||||||
selectedPrivateChatPeer = stableKeyHex
|
selectedPrivateChatPeer = stableKeyHex
|
||||||
@@ -4706,7 +4704,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Clear sent read receipts for this peer since they'll need to be resent after reconnection
|
// Clear sent read receipts for this peer since they'll need to be resent after reconnection
|
||||||
// Only clear receipts for messages from this specific peer
|
// Only clear receipts for messages from this specific peer
|
||||||
if let messages = privateChats[peerID] {
|
if let messages = privateChats[peerID.id] {
|
||||||
for message in messages {
|
for message in messages {
|
||||||
// Remove read receipts for messages FROM this peer (not TO this peer)
|
// Remove read receipts for messages FROM this peer (not TO this peer)
|
||||||
if message.senderPeerID == peerID {
|
if message.senderPeerID == peerID {
|
||||||
@@ -4718,7 +4716,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
func didUpdatePeerList(_ peers: [String]) {
|
func didUpdatePeerList(_ peers: [PeerID]) {
|
||||||
// UI updates must run on the main thread.
|
// UI updates must run on the main thread.
|
||||||
// The delegate callback is not guaranteed to be on the main thread.
|
// The delegate callback is not guaranteed to be on the main thread.
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -4736,7 +4734,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.networkResetTimer = nil
|
self.networkResetTimer = nil
|
||||||
// Count mesh peers that are connected OR recently reachable via mesh relays
|
// Count mesh peers that are connected OR recently reachable via mesh relays
|
||||||
let meshPeers = peers.filter { peerID in
|
let meshPeers = peers.filter { peerID in
|
||||||
self.meshService.isPeerConnected(PeerID(str: peerID)) || self.meshService.isPeerReachable(PeerID(str: peerID))
|
self.meshService.isPeerConnected(peerID) || self.meshService.isPeerReachable(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rising-edge only: previously zero peers, now > 0 peers
|
// Rising-edge only: previously zero peers, now > 0 peers
|
||||||
@@ -4762,7 +4760,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Register ephemeral sessions for all connected peers
|
// Register ephemeral sessions for all connected peers
|
||||||
for peerID in peers {
|
for peerID in peers {
|
||||||
self.identityManager.registerEphemeralSession(peerID: PeerID(str: peerID), handshakeState: .none)
|
self.identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule UI refresh to ensure offline favorites are shown
|
// Schedule UI refresh to ensure offline favorites are shown
|
||||||
|
|||||||
@@ -281,12 +281,12 @@ private final class MockBitchatDelegate: BitchatDelegate {
|
|||||||
messageHandler(message)
|
messageHandler(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func didConnectToPeer(_ peerID: String) {}
|
func didConnectToPeer(_ peerID: PeerID) {}
|
||||||
func didDisconnectFromPeer(_ peerID: String) {}
|
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||||
func didUpdatePeerList(_ peers: [String]) {}
|
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||||
func isFavorite(fingerprint: String) -> Bool { return false }
|
func isFavorite(fingerprint: String) -> Bool { return false }
|
||||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
||||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
||||||
func didUpdateBluetoothState(_ state: CBManagerState) {}
|
func didUpdateBluetoothState(_ state: CBManagerState) {}
|
||||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {}
|
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ struct PrivateChatE2ETests {
|
|||||||
// Alice sends private message to Bob
|
// Alice sends private message to Bob
|
||||||
alice.sendPrivateMessage(
|
alice.sendPrivateMessage(
|
||||||
TestConstants.testMessage1,
|
TestConstants.testMessage1,
|
||||||
to: PeerID(str: bob.peerID),
|
to: bob.peerID,
|
||||||
recipientNickname: TestConstants.testNickname2
|
recipientNickname: TestConstants.testNickname2
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ struct PrivateChatE2ETests {
|
|||||||
// Alice sends private message to Bob
|
// Alice sends private message to Bob
|
||||||
alice.sendPrivateMessage(
|
alice.sendPrivateMessage(
|
||||||
TestConstants.testMessage1,
|
TestConstants.testMessage1,
|
||||||
to: PeerID(str: bob.peerID),
|
to: bob.peerID,
|
||||||
recipientNickname: TestConstants.testNickname2
|
recipientNickname: TestConstants.testNickname2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ struct PrivateChatE2ETests {
|
|||||||
|
|
||||||
alice.sendPrivateMessage(
|
alice.sendPrivateMessage(
|
||||||
TestConstants.testMessage1,
|
TestConstants.testMessage1,
|
||||||
to: PeerID(str: bob.peerID),
|
to: bob.peerID,
|
||||||
recipientNickname: TestConstants.testNickname2
|
recipientNickname: TestConstants.testNickname2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -119,10 +119,10 @@ struct PrivateChatE2ETests {
|
|||||||
|
|
||||||
// Establish encrypted session
|
// Establish encrypted session
|
||||||
do {
|
do {
|
||||||
let handshake1 = try aliceManager.initiateHandshake(with: PeerID(str: bob.peerID))
|
let handshake1 = try aliceManager.initiateHandshake(with: bob.peerID)
|
||||||
let handshake2 = try bobManager.handleIncomingHandshake(from: PeerID(str: alice.peerID), message: handshake1)!
|
let handshake2 = try bobManager.handleIncomingHandshake(from: alice.peerID, message: handshake1)!
|
||||||
let handshake3 = try aliceManager.handleIncomingHandshake(from: PeerID(str: bob.peerID), message: handshake2)!
|
let handshake3 = try aliceManager.handleIncomingHandshake(from: bob.peerID, message: handshake2)!
|
||||||
_ = try bobManager.handleIncomingHandshake(from: PeerID(str: alice.peerID), message: handshake3)
|
_ = try bobManager.handleIncomingHandshake(from: alice.peerID, message: handshake3)
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Failed to establish Noise session: \(error)")
|
Issue.record("Failed to establish Noise session: \(error)")
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ struct PrivateChatE2ETests {
|
|||||||
let message = BitchatMessage(packet.payload),
|
let message = BitchatMessage(packet.payload),
|
||||||
message.isPrivate {
|
message.isPrivate {
|
||||||
do {
|
do {
|
||||||
let encrypted = try aliceManager.encrypt(packet.payload, for: PeerID(str: bob.peerID))
|
let encrypted = try aliceManager.encrypt(packet.payload, for: bob.peerID)
|
||||||
let encryptedPacket = BitchatPacket(
|
let encryptedPacket = BitchatPacket(
|
||||||
type: 0x02, // Encrypted message type
|
type: 0x02, // Encrypted message type
|
||||||
senderID: packet.senderID,
|
senderID: packet.senderID,
|
||||||
@@ -156,7 +156,7 @@ struct PrivateChatE2ETests {
|
|||||||
// Decrypt incoming encrypted messages
|
// Decrypt incoming encrypted messages
|
||||||
if packet.type == 0x02 {
|
if packet.type == 0x02 {
|
||||||
do {
|
do {
|
||||||
let decrypted = try bobManager.decrypt(packet.payload, from: PeerID(str: alice.peerID))
|
let decrypted = try bobManager.decrypt(packet.payload, from: alice.peerID)
|
||||||
if let message = BitchatMessage(decrypted) {
|
if let message = BitchatMessage(decrypted) {
|
||||||
#expect(message.content == TestConstants.testMessage1)
|
#expect(message.content == TestConstants.testMessage1)
|
||||||
#expect(message.isPrivate)
|
#expect(message.isPrivate)
|
||||||
@@ -207,7 +207,7 @@ struct PrivateChatE2ETests {
|
|||||||
// Alice sends private message to Charlie (through Bob)
|
// Alice sends private message to Charlie (through Bob)
|
||||||
alice.sendPrivateMessage(
|
alice.sendPrivateMessage(
|
||||||
TestConstants.testMessage1,
|
TestConstants.testMessage1,
|
||||||
to: PeerID(str: charlie.peerID),
|
to: charlie.peerID,
|
||||||
recipientNickname: TestConstants.testNickname3
|
recipientNickname: TestConstants.testNickname3
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,11 +183,11 @@ struct PublicChatE2ETests {
|
|||||||
let msg = TestHelpers.createTestMessage(
|
let msg = TestHelpers.createTestMessage(
|
||||||
content: TestConstants.testMessage1,
|
content: TestConstants.testMessage1,
|
||||||
sender: TestConstants.testNickname1,
|
sender: TestConstants.testNickname1,
|
||||||
senderPeerID: PeerID(str: alice.peerID)
|
senderPeerID: alice.peerID
|
||||||
)
|
)
|
||||||
|
|
||||||
if let payload = msg.toBinaryPayload() {
|
if let payload = msg.toBinaryPayload() {
|
||||||
let pkt = TestHelpers.createTestPacket(senderID: PeerID(str: alice.peerID), payload: payload, ttl: 2)
|
let pkt = TestHelpers.createTestPacket(senderID: alice.peerID, payload: payload, ttl: 2)
|
||||||
bob.simulateIncomingPacket(pkt)
|
bob.simulateIncomingPacket(pkt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,7 +407,7 @@ struct PublicChatE2ETests {
|
|||||||
if let relayPayload = relayMessage.toBinaryPayload() {
|
if let relayPayload = relayMessage.toBinaryPayload() {
|
||||||
let relayPacket = BitchatPacket(
|
let relayPacket = BitchatPacket(
|
||||||
type: packet.type,
|
type: packet.type,
|
||||||
senderID: node.peerID.data(using: .utf8)!,
|
senderID: node.peerID.id.data(using: .utf8)!,
|
||||||
recipientID: packet.recipientID,
|
recipientID: packet.recipientID,
|
||||||
timestamp: packet.timestamp,
|
timestamp: packet.timestamp,
|
||||||
payload: relayPayload,
|
payload: relayPayload,
|
||||||
|
|||||||
@@ -127,19 +127,19 @@ struct FragmentationTests {
|
|||||||
|
|
||||||
extension FragmentationTests {
|
extension FragmentationTests {
|
||||||
private final class CaptureDelegate: BitchatDelegate {
|
private final class CaptureDelegate: BitchatDelegate {
|
||||||
var publicMessages: [(peerID: String, nickname: String, content: String)] = []
|
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
||||||
func didReceiveMessage(_ message: BitchatMessage) {}
|
func didReceiveMessage(_ message: BitchatMessage) {}
|
||||||
func didConnectToPeer(_ peerID: String) {}
|
func didConnectToPeer(_ peerID: PeerID) {}
|
||||||
func didDisconnectFromPeer(_ peerID: String) {}
|
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||||
func didUpdatePeerList(_ peers: [String]) {}
|
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||||
func isFavorite(fingerprint: String) -> Bool { false }
|
func isFavorite(fingerprint: String) -> Bool { false }
|
||||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
||||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
||||||
func didUpdateBluetoothState(_ state: CBManagerState) {}
|
func didUpdateBluetoothState(_ state: CBManagerState) {}
|
||||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {
|
||||||
publicMessages.append((peerID, nickname, content))
|
publicMessages.append((peerID, nickname, content))
|
||||||
}
|
}
|
||||||
func didReceiveRegionalPublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {}
|
func didReceiveRegionalPublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: build a large message packet (unencrypted public message)
|
// Helper: build a large message packet (unencrypted public message)
|
||||||
|
|||||||
@@ -484,14 +484,11 @@ final class IntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
guard let aliceManager = noiseManagers["Alice"],
|
guard let aliceManager = noiseManagers["Alice"],
|
||||||
let bobManager = noiseManagers["Bob"],
|
let bobManager = noiseManagers["Bob"],
|
||||||
let aliceStringPeerID = nodes["Alice"]?.peerID,
|
let alicePeerID = nodes["Alice"]?.peerID,
|
||||||
let bobStringPeerID = nodes["Bob"]?.peerID else {
|
let bobPeerID = nodes["Bob"]?.peerID else {
|
||||||
return XCTFail("Missing managers or peer IDs")
|
return XCTFail("Missing managers or peer IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
let alicePeerID = PeerID(str: aliceStringPeerID)
|
|
||||||
let bobPeerID = PeerID(str: bobStringPeerID)
|
|
||||||
|
|
||||||
// Baseline: encrypt from Alice, decrypt at Bob
|
// Baseline: encrypt from Alice, decrypt at Bob
|
||||||
let plaintext1 = Data("hello-secure".utf8)
|
let plaintext1 = Data("hello-secure".utf8)
|
||||||
let encrypted1 = try aliceManager.encrypt(plaintext1, for: bobPeerID)
|
let encrypted1 = try aliceManager.encrypt(plaintext1, for: bobPeerID)
|
||||||
@@ -592,7 +589,7 @@ final class IntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
private func createNode(_ name: String, peerID: PeerID) {
|
private func createNode(_ name: String, peerID: PeerID) {
|
||||||
let node = MockBluetoothMeshService()
|
let node = MockBluetoothMeshService()
|
||||||
node.myPeerID = peerID.id
|
node.myPeerID = peerID
|
||||||
node.mockNickname = name
|
node.mockNickname = name
|
||||||
nodes[name] = node
|
nodes[name] = node
|
||||||
|
|
||||||
@@ -669,9 +666,9 @@ final class IntegrationTests: XCTestCase {
|
|||||||
let peer1ID = nodes[node1]?.peerID,
|
let peer1ID = nodes[node1]?.peerID,
|
||||||
let peer2ID = nodes[node2]?.peerID else { return }
|
let peer2ID = nodes[node2]?.peerID else { return }
|
||||||
|
|
||||||
let msg1 = try manager1.initiateHandshake(with: PeerID(str: peer2ID))
|
let msg1 = try manager1.initiateHandshake(with: peer2ID)
|
||||||
let msg2 = try manager2.handleIncomingHandshake(from: PeerID(str: peer1ID), message: msg1)!
|
let msg2 = try manager2.handleIncomingHandshake(from: peer1ID, message: msg1)!
|
||||||
let msg3 = try manager1.handleIncomingHandshake(from: PeerID(str: peer2ID), message: msg2)!
|
let msg3 = try manager1.handleIncomingHandshake(from: peer2ID, message: msg2)!
|
||||||
_ = try manager2.handleIncomingHandshake(from: PeerID(str: peer1ID), message: msg3)
|
_ = try manager2.handleIncomingHandshake(from: peer1ID, message: msg3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class MockBLEService: NSObject {
|
|||||||
// MARK: - Properties matching BLEService
|
// MARK: - Properties matching BLEService
|
||||||
|
|
||||||
weak var delegate: BitchatDelegate?
|
weak var delegate: BitchatDelegate?
|
||||||
var myPeerID: String = "MOCK1234"
|
var myPeerID: PeerID = "MOCK1234"
|
||||||
var myNickname: String = "MockUser"
|
var myNickname: String = "MockUser"
|
||||||
|
|
||||||
private let mockKeychain = MockKeychain()
|
private let mockKeychain = MockKeychain()
|
||||||
@@ -40,7 +40,7 @@ final class MockBLEService: NSObject {
|
|||||||
// Test-specific properties
|
// Test-specific properties
|
||||||
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
|
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
|
||||||
var sentPackets: [BitchatPacket] = []
|
var sentPackets: [BitchatPacket] = []
|
||||||
var connectedPeers: Set<String> = []
|
var connectedPeers: Set<PeerID> = []
|
||||||
var messageDeliveryHandler: ((BitchatMessage) -> Void)?
|
var messageDeliveryHandler: ((BitchatMessage) -> Void)?
|
||||||
var packetDeliveryHandler: ((BitchatPacket) -> Void)?
|
var packetDeliveryHandler: ((BitchatPacket) -> Void)?
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ final class MockBLEService: NSObject {
|
|||||||
return myNickname
|
return myNickname
|
||||||
}
|
}
|
||||||
|
|
||||||
var peerID: String {
|
var peerID: PeerID {
|
||||||
return myPeerID
|
return myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +72,8 @@ final class MockBLEService: NSObject {
|
|||||||
|
|
||||||
// MARK: - In-memory test bus (for E2E/Integration)
|
// MARK: - In-memory test bus (for E2E/Integration)
|
||||||
/// Global per-process bus for deterministic routing in tests.
|
/// Global per-process bus for deterministic routing in tests.
|
||||||
private static var registry: [String: MockBLEService] = [:]
|
private static var registry: [PeerID: MockBLEService] = [:]
|
||||||
private static var adjacency: [String: Set<String>] = [:]
|
private static var adjacency: [PeerID: Set<PeerID>] = [:]
|
||||||
|
|
||||||
/// Clears global bus state. Call from test `setUp()`.
|
/// Clears global bus state. Call from test `setUp()`.
|
||||||
static func resetTestBus() {
|
static func resetTestBus() {
|
||||||
@@ -94,7 +94,7 @@ final class MockBLEService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an undirected edge between two peerIDs.
|
/// Adds an undirected edge between two peerIDs.
|
||||||
private static func connectPeers(_ a: String, _ b: String) {
|
private static func connectPeers(_ a: PeerID, _ b: PeerID) {
|
||||||
var setA = adjacency[a] ?? []
|
var setA = adjacency[a] ?? []
|
||||||
setA.insert(b)
|
setA.insert(b)
|
||||||
adjacency[a] = setA
|
adjacency[a] = setA
|
||||||
@@ -104,7 +104,7 @@ final class MockBLEService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Removes an undirected edge between two peerIDs.
|
/// Removes an undirected edge between two peerIDs.
|
||||||
private static func disconnectPeers(_ a: String, _ b: String) {
|
private static func disconnectPeers(_ a: PeerID, _ b: PeerID) {
|
||||||
if var setA = adjacency[a] { setA.remove(b); adjacency[a] = setA }
|
if var setA = adjacency[a] { setA.remove(b); adjacency[a] = setA }
|
||||||
if var setB = adjacency[b] { setB.remove(a); adjacency[b] = setB }
|
if var setB = adjacency[b] { setB.remove(a); adjacency[b] = setB }
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ final class MockBLEService: NSObject {
|
|||||||
// Mock implementation - do nothing
|
// Mock implementation - do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPeerConnected(_ peerID: String) -> Bool {
|
func isPeerConnected(_ peerID: PeerID) -> Bool {
|
||||||
return connectedPeers.contains(peerID)
|
return connectedPeers.contains(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,15 +125,15 @@ final class MockBLEService: NSObject {
|
|||||||
"MockPeer_\(peerID)"
|
"MockPeer_\(peerID)"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPeerNicknames() -> [String: String] {
|
func getPeerNicknames() -> [PeerID: String] {
|
||||||
var nicknames: [String: String] = [:]
|
var nicknames: [PeerID: String] = [:]
|
||||||
for peer in connectedPeers {
|
for peer in connectedPeers {
|
||||||
nicknames[peer] = "MockPeer_\(peer)"
|
nicknames[peer] = "MockPeer_\(peer)"
|
||||||
}
|
}
|
||||||
return nicknames
|
return nicknames
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPeers() -> [String: String] {
|
func getPeers() -> [PeerID: String] {
|
||||||
return getPeerNicknames()
|
return getPeerNicknames()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +147,14 @@ final class MockBLEService: NSObject {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: recipientID != nil,
|
isPrivate: recipientID != nil,
|
||||||
recipientNickname: nil,
|
recipientNickname: nil,
|
||||||
senderPeerID: PeerID(str: myPeerID),
|
senderPeerID: myPeerID,
|
||||||
mentions: mentions.isEmpty ? nil : mentions
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
|
|
||||||
if let payload = message.toBinaryPayload() {
|
if let payload = message.toBinaryPayload() {
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: 0x01,
|
type: 0x01,
|
||||||
senderID: myPeerID.data(using: .utf8)!,
|
senderID: myPeerID.id.data(using: .utf8)!,
|
||||||
recipientID: recipientID?.data(using: .utf8),
|
recipientID: recipientID?.data(using: .utf8),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -182,7 +182,7 @@ final class MockBLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String) {
|
func sendPrivateMessage(_ content: String, to recipientPeerID: PeerID, recipientNickname: String, messageID: String) {
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sender: myNickname,
|
sender: myNickname,
|
||||||
@@ -192,15 +192,15 @@ final class MockBLEService: NSObject {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: recipientNickname,
|
recipientNickname: recipientNickname,
|
||||||
senderPeerID: PeerID(str: myPeerID),
|
senderPeerID: myPeerID,
|
||||||
mentions: nil
|
mentions: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
if let payload = message.toBinaryPayload() {
|
if let payload = message.toBinaryPayload() {
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: 0x01,
|
type: 0x01,
|
||||||
senderID: myPeerID.data(using: .utf8)!,
|
senderID: myPeerID.id.data(using: .utf8)!,
|
||||||
recipientID: recipientPeerID.data(using: .utf8)!,
|
recipientID: recipientPeerID.id.data(using: .utf8)!,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
@@ -277,7 +277,7 @@ final class MockBLEService: NSObject {
|
|||||||
|
|
||||||
// MARK: - Test Helper Methods
|
// MARK: - Test Helper Methods
|
||||||
|
|
||||||
func simulateConnectedPeer(_ peerID: String) {
|
func simulateConnectedPeer(_ peerID: PeerID) {
|
||||||
registerIfNeeded()
|
registerIfNeeded()
|
||||||
MockBLEService.connectPeers(myPeerID, peerID)
|
MockBLEService.connectPeers(myPeerID, peerID)
|
||||||
connectedPeers.insert(peerID)
|
connectedPeers.insert(peerID)
|
||||||
@@ -285,7 +285,7 @@ final class MockBLEService: NSObject {
|
|||||||
delegate?.didUpdatePeerList(Array(connectedPeers))
|
delegate?.didUpdatePeerList(Array(connectedPeers))
|
||||||
}
|
}
|
||||||
|
|
||||||
func simulateDisconnectedPeer(_ peerID: String) {
|
func simulateDisconnectedPeer(_ peerID: PeerID) {
|
||||||
MockBLEService.disconnectPeers(myPeerID, peerID)
|
MockBLEService.disconnectPeers(myPeerID, peerID)
|
||||||
connectedPeers.remove(peerID)
|
connectedPeers.remove(peerID)
|
||||||
delegate?.didDisconnectFromPeer(peerID)
|
delegate?.didDisconnectFromPeer(peerID)
|
||||||
@@ -336,14 +336,14 @@ final class MockBLEService: NSObject {
|
|||||||
packetDeliveryHandler?(packet)
|
packetDeliveryHandler?(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConnectedPeers() -> [String] {
|
func getConnectedPeers() -> [PeerID] {
|
||||||
return Array(connectedPeers)
|
return Array(connectedPeers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Compatibility methods for old tests
|
// MARK: - Compatibility methods for old tests
|
||||||
|
|
||||||
func sendPrivateMessage(_ content: String, to recipientPeerID: PeerID, recipientNickname: String, messageID: String? = nil) {
|
func sendPrivateMessage(_ content: String, to recipientPeerID: PeerID, recipientNickname: String, messageID: String? = nil) {
|
||||||
sendPrivateMessage(content, to: recipientPeerID.id, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString)
|
sendPrivateMessage(content, to: recipientPeerID, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +355,7 @@ typealias MockSimplifiedBluetoothService = MockBLEService
|
|||||||
extension MockBLEService {
|
extension MockBLEService {
|
||||||
convenience init(peerID: PeerID, nickname: String) {
|
convenience init(peerID: PeerID, nickname: String) {
|
||||||
self.init()
|
self.init()
|
||||||
myPeerID = peerID.id
|
myPeerID = peerID
|
||||||
mockNickname = nickname
|
mockNickname = nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user