mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 08:05:21 +00:00
Refactor/robustness (#446)
* Refactor BitChat for improved robustness and performance Major refactoring to simplify architecture and fix critical issues: Architecture Improvements: - Replace complex BluetoothMeshService with simplified SimplifiedBluetoothService - Consolidate message routing and peer management into unified services - Remove redundant caching layers and optimize performance Bug Fixes: - Fix critical BLE peer mapping corruption in mesh networks - Fix encrypted message routing failures in multi-peer scenarios - Fix app freezes and Main Thread Checker warnings - Fix BLE message delivery in dual-role connections - Fix favorite toggle UI not updating instantly - Fix Nostr offline messaging with 24-hour message filtering Features: - Add command processor for chat commands - Add autocomplete service for mentions and commands - Improve private chat management with dedicated service - Add unified peer service for consistent state management Performance: - Optimize BLE reconnection speed - Reduce excessive logging throughout codebase - Improve message deduplication efficiency - Optimize UI updates and state management * Improvements refactor robust (#441) * remove unused code * remove more * TLV for announcement * restore * restore * restore? * messages tlv too (#442) * Fix Nostr notification and read receipt issues, add TLV encoding, cleanup unused code ## Notification & Read Receipt Fixes - Fixed toolbar notification icon appearing incorrectly on app restart for already-read messages - Fixed read receipts being incorrectly deleted on startup when privateChats was empty - Fixed messages not being marked as read when opening chat for first time - Fixed senderPeerID not being updated during message consolidation - Added startup phase logic to block old messages (>30s) while allowing recent ones - Fixed unread status checking across all storage locations (ephemeral, stable Noise keys, temporary Nostr IDs) ## TLV Encoding Implementation - Implemented Type-Length-Value encoding for private message payloads - Added PrivateMessagePacket struct with TLV encode/decode methods - Enhanced message structure for better extensibility and robustness ## Code Cleanup - Removed unused PeerStateManager class and related dependencies - Removed dead protocol types (DeliveryAck, ProtocolAck/Nack, NoiseIdentityAnnouncement) - Cleaned up BitchatDelegate by removing unused methods - Removed excessive debug logging throughout ChatViewModel - Added test-only ProtocolNack helper for integration tests ## Technical Details - Messages stored under three ID types: ephemeral peer IDs, stable Noise key hexes, temporary Nostr IDs - Fixed cleanupOldReadReceipts() to skip when privateChats is empty or during startup - Updated message consolidation to properly update senderPeerID - Restored NIP-17 timestamp randomization (±15 minutes) for privacy --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
@@ -1,655 +0,0 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Routes messages through the appropriate transport (Bluetooth mesh or Nostr)
|
||||
@MainActor
|
||||
class MessageRouter: ObservableObject {
|
||||
|
||||
enum Transport {
|
||||
case bluetoothMesh
|
||||
case nostr
|
||||
}
|
||||
|
||||
enum DeliveryStatus {
|
||||
case pending
|
||||
case sent
|
||||
case delivered
|
||||
case failed(Error)
|
||||
}
|
||||
|
||||
struct RoutedMessage {
|
||||
let id: String
|
||||
let content: String
|
||||
let recipientNoisePublicKey: Data
|
||||
let transport: Transport
|
||||
let timestamp: Date
|
||||
var status: DeliveryStatus
|
||||
}
|
||||
|
||||
@Published private(set) var pendingMessages: [String: RoutedMessage] = [:]
|
||||
|
||||
private let meshService: BluetoothMeshService
|
||||
private let nostrRelay: NostrRelayManager
|
||||
private let favoritesService: FavoritesPersistenceService
|
||||
private let processedMessagesService = ProcessedMessagesService.shared
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let messageDeduplication = LRUCache<String, Date>(maxSize: 1000)
|
||||
|
||||
init(
|
||||
meshService: BluetoothMeshService,
|
||||
nostrRelay: NostrRelayManager
|
||||
) {
|
||||
self.meshService = meshService
|
||||
self.nostrRelay = nostrRelay
|
||||
self.favoritesService = FavoritesPersistenceService.shared
|
||||
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
/// Send a message to a peer, automatically selecting the best transport
|
||||
func sendMessage(
|
||||
_ content: String,
|
||||
to recipientNoisePublicKey: Data,
|
||||
preferredTransport: Transport? = nil,
|
||||
messageId: String? = nil
|
||||
) async throws {
|
||||
|
||||
let finalMessageId = messageId ?? UUID().uuidString
|
||||
|
||||
// Check if peer is available on mesh (actually connected, not just known)
|
||||
let recipientHexID = recipientNoisePublicKey.hexEncodedString()
|
||||
let peerAvailableOnMesh = meshService.isPeerConnected(recipientHexID)
|
||||
|
||||
// Check if this is a mutual favorite
|
||||
let isMutualFavorite = favoritesService.isMutualFavorite(recipientNoisePublicKey)
|
||||
|
||||
// Determine transport
|
||||
let transport: Transport
|
||||
if let preferred = preferredTransport {
|
||||
transport = preferred
|
||||
} else if peerAvailableOnMesh {
|
||||
// Always prefer mesh when available
|
||||
transport = .bluetoothMesh
|
||||
} else if isMutualFavorite {
|
||||
// Use Nostr for mutual favorites when not on mesh
|
||||
transport = .nostr
|
||||
} else {
|
||||
throw MessageRouterError.peerNotReachable
|
||||
}
|
||||
|
||||
// Create routed message
|
||||
let routedMessage = RoutedMessage(
|
||||
id: finalMessageId,
|
||||
content: content,
|
||||
recipientNoisePublicKey: recipientNoisePublicKey,
|
||||
transport: transport,
|
||||
timestamp: Date(),
|
||||
status: .pending
|
||||
)
|
||||
|
||||
pendingMessages[finalMessageId] = routedMessage
|
||||
|
||||
// Route based on transport
|
||||
switch transport {
|
||||
case .bluetoothMesh:
|
||||
try await sendViaMesh(routedMessage)
|
||||
|
||||
case .nostr:
|
||||
try await sendViaNostr(routedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a favorite/unfavorite notification
|
||||
func sendFavoriteNotification(
|
||||
to recipientNoisePublicKey: Data,
|
||||
isFavorite: Bool
|
||||
) async throws {
|
||||
|
||||
// messageType is used for logging below
|
||||
// let messageType: MessageType = isFavorite ? .favorited : .unfavorited
|
||||
let recipientHexID = recipientNoisePublicKey.hexEncodedString()
|
||||
let action = isFavorite ? "favorite" : "unfavorite"
|
||||
|
||||
|
||||
// Try mesh first
|
||||
if meshService.getPeerNicknames()[recipientHexID] != nil {
|
||||
|
||||
// Send via mesh as a system message
|
||||
meshService.sendFavoriteNotification(to: recipientHexID, isFavorite: isFavorite)
|
||||
|
||||
} else if let favoriteStatus = favoritesService.getFavoriteStatus(for: recipientNoisePublicKey),
|
||||
let recipientNostrPubkey = favoriteStatus.peerNostrPublicKey {
|
||||
|
||||
|
||||
// Send via Nostr as a special message
|
||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
||||
throw MessageRouterError.noNostrIdentity
|
||||
}
|
||||
|
||||
// Include our npub in the content
|
||||
let content = isFavorite ? "FAVORITED:\(senderIdentity.npub)" : "UNFAVORITED:\(senderIdentity.npub)"
|
||||
let event = try NostrProtocol.createPrivateMessage(
|
||||
content: content,
|
||||
recipientPubkey: recipientNostrPubkey,
|
||||
senderIdentity: senderIdentity
|
||||
)
|
||||
|
||||
nostrRelay.sendEvent(event)
|
||||
} else {
|
||||
SecureLogger.log("⚠️ Cannot send \(action) notification - peer not reachable via mesh or Nostr",
|
||||
category: SecureLogger.session, level: .warning)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func sendViaMesh(_ message: RoutedMessage) async throws {
|
||||
// Send the message through mesh - using sendPrivateMessage for now
|
||||
let recipientHexID = message.recipientNoisePublicKey.hexEncodedString()
|
||||
if let recipientNickname = meshService.getPeerNicknames()[recipientHexID] {
|
||||
meshService.sendPrivateMessage(message.content, to: recipientHexID, recipientNickname: recipientNickname, messageID: message.id)
|
||||
}
|
||||
|
||||
// Update status
|
||||
pendingMessages[message.id]?.status = .sent
|
||||
}
|
||||
|
||||
private func sendViaNostr(_ message: RoutedMessage) async throws {
|
||||
// Get recipient's Nostr public key
|
||||
let favoriteStatus = favoritesService.getFavoriteStatus(for: message.recipientNoisePublicKey)
|
||||
|
||||
// Looking up Nostr key for recipient
|
||||
|
||||
if favoriteStatus != nil {
|
||||
// Found favorite relationship
|
||||
} else {
|
||||
SecureLogger.log("❌ No favorite relationship found",
|
||||
category: SecureLogger.session, level: .error)
|
||||
}
|
||||
|
||||
guard let favoriteStatus = favoriteStatus,
|
||||
let recipientNostrPubkey = favoriteStatus.peerNostrPublicKey else {
|
||||
throw MessageRouterError.noNostrPublicKey
|
||||
}
|
||||
|
||||
// Get sender's Nostr identity
|
||||
guard let senderIdentity = try NostrIdentityBridge.getCurrentNostrIdentity() else {
|
||||
throw MessageRouterError.noNostrIdentity
|
||||
}
|
||||
|
||||
// Create NIP-17 encrypted message with structured content
|
||||
let structuredContent = "MSG:\(message.id):\(message.content)"
|
||||
let event = try NostrProtocol.createPrivateMessage(
|
||||
content: structuredContent,
|
||||
recipientPubkey: recipientNostrPubkey,
|
||||
senderIdentity: senderIdentity
|
||||
)
|
||||
|
||||
// Created gift wrap event
|
||||
|
||||
// Send via relay
|
||||
nostrRelay.sendEvent(event)
|
||||
|
||||
// Update status
|
||||
pendingMessages[message.id]?.status = .sent
|
||||
}
|
||||
|
||||
private func setupBindings() {
|
||||
// Monitor Nostr messages
|
||||
setupNostrMessageHandling()
|
||||
|
||||
// Clean up old pending messages periodically
|
||||
Timer.publish(every: 60, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
self?.cleanupOldMessages()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Listen for app becoming active to check for messages
|
||||
NotificationCenter.default.publisher(for: .appDidBecomeActive)
|
||||
.sink { [weak self] _ in
|
||||
self?.checkForNostrMessages()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func setupNostrMessageHandling() {
|
||||
guard (try? NostrIdentityBridge.getCurrentNostrIdentity()) != nil else {
|
||||
SecureLogger.log("⚠️ No Nostr identity available for initial setup", category: SecureLogger.session, level: .warning)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Connect to relays if not already connected
|
||||
if !nostrRelay.isConnected {
|
||||
nostrRelay.connect()
|
||||
|
||||
// Wait for connections to establish before subscribing
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
||||
self?.subscribeToNostrMessages()
|
||||
}
|
||||
} else {
|
||||
// Already connected, subscribe immediately
|
||||
subscribeToNostrMessages()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for Nostr messages when app becomes active
|
||||
func checkForNostrMessages() {
|
||||
// Checking for Nostr messages
|
||||
|
||||
guard (try? NostrIdentityBridge.getCurrentNostrIdentity()) != nil else {
|
||||
SecureLogger.log("⚠️ No Nostr identity available for message check", category: SecureLogger.session, level: .warning)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure we're connected to relays first
|
||||
if !nostrRelay.isConnected {
|
||||
// Connecting to Nostr relays
|
||||
nostrRelay.connect()
|
||||
|
||||
// Wait a bit for connections to establish before subscribing
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||
self?.subscribeToNostrMessages()
|
||||
}
|
||||
} else {
|
||||
// Already connected, subscribe immediately
|
||||
subscribeToNostrMessages()
|
||||
}
|
||||
}
|
||||
|
||||
private func subscribeToNostrMessages() {
|
||||
guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||
|
||||
// Subscribing to Nostr messages
|
||||
// Full pubkey recorded
|
||||
// Pubkey length verified
|
||||
|
||||
// Unsubscribe existing subscription to refresh
|
||||
nostrRelay.unsubscribe(id: "router-messages")
|
||||
|
||||
// Create a new subscription for recent messages
|
||||
let sinceDate = processedMessagesService.getSubscriptionSinceDate()
|
||||
let filter = NostrFilter.giftWrapsFor(
|
||||
pubkey: currentIdentity.publicKeyHex,
|
||||
since: sinceDate
|
||||
)
|
||||
|
||||
// Subscribing to messages since date
|
||||
|
||||
// Subscribing to gift wraps
|
||||
|
||||
nostrRelay.subscribe(filter: filter, id: "router-messages") { [weak self] event in
|
||||
// Received Nostr event
|
||||
self?.handleNostrMessage(event)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleNostrMessage(_ giftWrap: NostrEvent) {
|
||||
// Check if we've already processed this event
|
||||
if processedMessagesService.isMessageProcessed(giftWrap.id) {
|
||||
// Skipping already processed event
|
||||
return
|
||||
}
|
||||
|
||||
// Attempting to decrypt gift wrap
|
||||
// Full event ID recorded
|
||||
// Event timestamp recorded
|
||||
// Event tags recorded
|
||||
|
||||
// Decrypt the message
|
||||
guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
||||
SecureLogger.log("❌ No current Nostr identity available",
|
||||
category: SecureLogger.session, level: .error)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this event is actually tagged for us
|
||||
let ourPubkey = currentIdentity.publicKeyHex
|
||||
let isTaggedForUs = giftWrap.tags.contains { tag in
|
||||
tag.count >= 2 && tag[0] == "p" && tag[1] == ourPubkey
|
||||
}
|
||||
|
||||
if !isTaggedForUs {
|
||||
SecureLogger.log("⚠️ Gift wrap not tagged for us! Our pubkey: \(ourPubkey.prefix(8))..., Event tags: \(giftWrap.tags)",
|
||||
category: SecureLogger.session, level: .warning)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let (content, senderPubkey) = try NostrProtocol.decryptPrivateMessage(
|
||||
giftWrap: giftWrap,
|
||||
recipientIdentity: currentIdentity
|
||||
)
|
||||
|
||||
|
||||
// Mark this event as processed to avoid duplicates on app restart
|
||||
let eventTimestamp = Date(timeIntervalSince1970: TimeInterval(giftWrap.created_at))
|
||||
processedMessagesService.markMessageAsProcessed(giftWrap.id, timestamp: eventTimestamp)
|
||||
|
||||
// Check for deduplication within current session
|
||||
let messageHash = "\(senderPubkey)-\(content)-\(giftWrap.created_at)"
|
||||
if messageDeduplication.get(messageHash) != nil {
|
||||
return // Already processed in this session
|
||||
}
|
||||
messageDeduplication.set(messageHash, value: Date())
|
||||
|
||||
// Handle special messages
|
||||
if content.hasPrefix("FAVORITED") || content.hasPrefix("UNFAVORITED") {
|
||||
let parts = content.split(separator: ":")
|
||||
let isFavorite = parts.first == "FAVORITED"
|
||||
let nostrNpub = parts.count > 1 ? String(parts[1]) : nil
|
||||
handleFavoriteNotification(from: senderPubkey, isFavorite: isFavorite, nostrNpub: nostrNpub)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle delivery acknowledgments
|
||||
if content.hasPrefix("DELIVERED:") {
|
||||
let parts = content.split(separator: ":")
|
||||
if parts.count > 1 {
|
||||
let messageId = String(parts[1])
|
||||
handleDeliveryAcknowledgment(messageId: messageId, from: senderPubkey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle read receipts
|
||||
if content.hasPrefix("READ:") {
|
||||
let parts = content.split(separator: ":", maxSplits: 1)
|
||||
if parts.count > 1 {
|
||||
let receiptDataString = String(parts[1])
|
||||
if let receiptData = Data(base64Encoded: receiptDataString),
|
||||
let receipt = ReadReceipt.fromBinaryData(receiptData) {
|
||||
handleReadReceipt(receipt, from: senderPubkey)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Find the sender's Noise public key
|
||||
guard let senderNoiseKey = findNoisePublicKey(for: senderPubkey) else { return }
|
||||
|
||||
// Parse structured message content
|
||||
var messageId = UUID().uuidString
|
||||
var messageContent = content
|
||||
|
||||
if content.hasPrefix("MSG:") {
|
||||
let parts = content.split(separator: ":", maxSplits: 2)
|
||||
if parts.count >= 3 {
|
||||
messageId = String(parts[1])
|
||||
messageContent = String(parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
// Create a BitchatMessage and inject into the stream
|
||||
let chatMessage = BitchatMessage(
|
||||
id: messageId,
|
||||
sender: favoritesService.getFavoriteStatus(for: senderNoiseKey)?.peerNickname ?? "Unknown",
|
||||
content: messageContent,
|
||||
timestamp: Date(timeIntervalSince1970: TimeInterval(giftWrap.created_at)),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: true,
|
||||
recipientNickname: nil,
|
||||
senderPeerID: senderNoiseKey.hexEncodedString(),
|
||||
mentions: nil,
|
||||
deliveryStatus: .delivered(to: "nostr", at: Date())
|
||||
)
|
||||
|
||||
// Post notification for ChatViewModel to handle
|
||||
NotificationCenter.default.post(
|
||||
name: .nostrMessageReceived,
|
||||
object: nil,
|
||||
userInfo: ["message": chatMessage]
|
||||
)
|
||||
|
||||
// Send delivery acknowledgment back to sender
|
||||
sendDeliveryAcknowledgment(for: chatMessage.id, to: senderPubkey)
|
||||
|
||||
} catch {
|
||||
SecureLogger.log("❌ Failed to decrypt gift wrap: \(error)",
|
||||
category: SecureLogger.session, level: .error)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFavoriteNotification(from nostrPubkey: String, isFavorite: Bool, nostrNpub: String? = nil) {
|
||||
// Find the sender's Noise public key
|
||||
guard let senderNoiseKey = findNoisePublicKey(for: nostrPubkey) else { return }
|
||||
|
||||
// Update favorites service - nostrPubkey is already the hex public key
|
||||
favoritesService.updatePeerFavoritedUs(
|
||||
peerNoisePublicKey: senderNoiseKey,
|
||||
favorited: isFavorite,
|
||||
peerNostrPublicKey: nostrPubkey
|
||||
)
|
||||
|
||||
// Post notification for UI update
|
||||
NotificationCenter.default.post(
|
||||
name: .favoriteStatusChanged,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"peerPublicKey": senderNoiseKey,
|
||||
"isFavorite": isFavorite
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
private func findNoisePublicKey(for nostrPubkey: String) -> Data? {
|
||||
// Search through favorites for matching Nostr pubkey
|
||||
for (noiseKey, relationship) in favoritesService.favorites {
|
||||
if relationship.peerNostrPublicKey == nostrPubkey {
|
||||
return noiseKey
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func handleDeliveryAcknowledgment(messageId: String, from senderPubkey: String) {
|
||||
|
||||
// Find the sender's Noise public key
|
||||
guard let senderNoiseKey = findNoisePublicKey(for: senderPubkey) else { return }
|
||||
|
||||
// Post notification for ChatViewModel to update delivery status
|
||||
NotificationCenter.default.post(
|
||||
name: .messageDeliveryAcknowledged,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"messageId": messageId,
|
||||
"senderNoiseKey": senderNoiseKey
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
private func handleReadReceipt(_ receipt: ReadReceipt, from senderPubkey: String) {
|
||||
|
||||
// Find the sender's Noise public key
|
||||
guard let senderNoiseKey = findNoisePublicKey(for: senderPubkey) else { return }
|
||||
let senderHexID = senderNoiseKey.hexEncodedString()
|
||||
|
||||
// Update the receipt with the correct sender ID
|
||||
var updatedReceipt = receipt
|
||||
updatedReceipt.readerID = senderHexID
|
||||
|
||||
// Post notification for ChatViewModel to process
|
||||
NotificationCenter.default.post(
|
||||
name: .readReceiptReceived,
|
||||
object: nil,
|
||||
userInfo: ["receipt": updatedReceipt]
|
||||
)
|
||||
}
|
||||
|
||||
func sendReadReceipt(
|
||||
for originalMessageID: String,
|
||||
to recipientNoisePublicKey: Data,
|
||||
preferredTransport: Transport? = nil
|
||||
) async throws {
|
||||
|
||||
// Get nickname from delegate or use default
|
||||
let nickname = (meshService.delegate as? ChatViewModel)?.nickname ?? "Anonymous"
|
||||
|
||||
// Create read receipt
|
||||
let receipt = ReadReceipt(
|
||||
originalMessageID: originalMessageID,
|
||||
readerID: meshService.myPeerID,
|
||||
readerNickname: nickname
|
||||
)
|
||||
|
||||
// Encode receipt
|
||||
let receiptData = receipt.toBinaryData()
|
||||
let content = "READ:\(receiptData.base64EncodedString())"
|
||||
|
||||
// Check if peer is connected via mesh (mesh takes precedence)
|
||||
let recipientHexID = recipientNoisePublicKey.hexEncodedString()
|
||||
|
||||
// First check if the peer is currently connected with the given ID
|
||||
var actualRecipientHexID = recipientHexID
|
||||
var actualRecipientNoiseKey = recipientNoisePublicKey
|
||||
|
||||
// Always check if they reconnected with a new ID, even if preferredTransport is specified
|
||||
if let favoriteStatus = favoritesService.getFavoriteStatus(for: recipientNoisePublicKey) {
|
||||
let peerNickname = favoriteStatus.peerNickname
|
||||
|
||||
// Search through all current peers to find one with the same nickname
|
||||
for (currentPeerID, currentNickname) in meshService.getPeerNicknames() {
|
||||
if currentNickname == peerNickname,
|
||||
currentPeerID != recipientHexID,
|
||||
let currentNoiseKey = Data(hexString: currentPeerID) {
|
||||
SecureLogger.log("🔄 Found updated peer ID for \(peerNickname): \(recipientHexID) -> \(currentPeerID)",
|
||||
category: SecureLogger.session, level: .info)
|
||||
actualRecipientHexID = currentPeerID
|
||||
actualRecipientNoiseKey = currentNoiseKey
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If still not found in connected peers, check all favorites for the current key
|
||||
if meshService.getPeerNicknames()[actualRecipientHexID] == nil {
|
||||
// Search through all favorites to find the current noise key for this nickname
|
||||
for (noiseKey, relationship) in favoritesService.favorites {
|
||||
if relationship.peerNickname == peerNickname && relationship.peerNostrPublicKey != nil {
|
||||
SecureLogger.log("🔄 Using current favorite key for \(peerNickname): \(recipientHexID) -> \(noiseKey.hexEncodedString())",
|
||||
category: SecureLogger.session, level: .info)
|
||||
actualRecipientHexID = noiseKey.hexEncodedString()
|
||||
actualRecipientNoiseKey = noiseKey
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isConnectedOnMesh = meshService.isPeerConnected(actualRecipientHexID)
|
||||
|
||||
if isConnectedOnMesh && preferredTransport != .nostr {
|
||||
// Send via mesh
|
||||
SecureLogger.log("📡 Sending read receipt via mesh to \(actualRecipientHexID)",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
meshService.sendReadReceipt(receipt, to: actualRecipientHexID)
|
||||
} else {
|
||||
// Send via Nostr
|
||||
SecureLogger.log("🌐 Sending read receipt via Nostr to \(actualRecipientHexID)",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
|
||||
// Get recipient's Nostr public key using the actual current noise key
|
||||
let favoriteStatus = favoritesService.getFavoriteStatus(for: actualRecipientNoiseKey)
|
||||
guard let recipientNostrPubkey = favoriteStatus?.peerNostrPublicKey else {
|
||||
SecureLogger.log("❌ Cannot send read receipt - no Nostr key for recipient",
|
||||
category: SecureLogger.session, level: .error)
|
||||
throw MessageRouterError.noNostrKey
|
||||
}
|
||||
|
||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
||||
SecureLogger.log("⚠️ No Nostr identity available for read receipt",
|
||||
category: SecureLogger.session, level: .warning)
|
||||
throw MessageRouterError.noIdentity
|
||||
}
|
||||
|
||||
// Create read receipt message
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(
|
||||
content: content,
|
||||
recipientPubkey: recipientNostrPubkey,
|
||||
senderIdentity: senderIdentity
|
||||
) else {
|
||||
SecureLogger.log("❌ Failed to create read receipt",
|
||||
category: SecureLogger.session, level: .error)
|
||||
throw MessageRouterError.encryptionFailed
|
||||
}
|
||||
|
||||
// Send via relay
|
||||
nostrRelay.sendEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendDeliveryAcknowledgment(for messageId: String, to recipientNostrPubkey: String) {
|
||||
SecureLogger.log("📤 Sending delivery acknowledgment for message \(messageId)",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
|
||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
||||
SecureLogger.log("⚠️ No Nostr identity available for acknowledgment",
|
||||
category: SecureLogger.session, level: .warning)
|
||||
return
|
||||
}
|
||||
|
||||
// Create acknowledgment message
|
||||
let content = "DELIVERED:\(messageId)"
|
||||
guard let event = try? NostrProtocol.createPrivateMessage(
|
||||
content: content,
|
||||
recipientPubkey: recipientNostrPubkey,
|
||||
senderIdentity: senderIdentity
|
||||
) else {
|
||||
SecureLogger.log("❌ Failed to create delivery acknowledgment",
|
||||
category: SecureLogger.session, level: .error)
|
||||
return
|
||||
}
|
||||
|
||||
// Send via relay
|
||||
nostrRelay.sendEvent(event)
|
||||
}
|
||||
|
||||
private func cleanupOldMessages() {
|
||||
let cutoff = Date().addingTimeInterval(-300) // 5 minutes
|
||||
pendingMessages = pendingMessages.filter { $0.value.timestamp > cutoff }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
enum MessageRouterError: LocalizedError {
|
||||
case peerNotReachable
|
||||
case noNostrPublicKey
|
||||
case noNostrIdentity
|
||||
case transportFailed
|
||||
case noNostrKey
|
||||
case noIdentity
|
||||
case encryptionFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .peerNotReachable:
|
||||
return "Peer is not reachable via mesh or Nostr"
|
||||
case .noNostrPublicKey:
|
||||
return "Peer's Nostr public key is unknown"
|
||||
case .noNostrIdentity:
|
||||
return "No Nostr identity available"
|
||||
case .transportFailed:
|
||||
return "Failed to send message"
|
||||
case .noNostrKey:
|
||||
return "No Nostr key available for recipient"
|
||||
case .noIdentity:
|
||||
return "No identity available"
|
||||
case .encryptionFailed:
|
||||
return "Failed to encrypt message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Names
|
||||
|
||||
extension Notification.Name {
|
||||
static let nostrMessageReceived = Notification.Name("NostrMessageReceived")
|
||||
static let messageDeliveryAcknowledged = Notification.Name("MessageDeliveryAcknowledged")
|
||||
static let readReceiptReceived = Notification.Name("ReadReceiptReceived")
|
||||
static let appDidBecomeActive = Notification.Name("AppDidBecomeActive")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user