Implement indefinite store-and-forward for favorite peers

- Change message cache timeout from 5 minutes to 12 hours for regular peers
- Add indefinite message storage for favorite peers with 1000 message limit
- Cache relayed messages for offline favorites
- Automatically send cached messages when favorites come online
- Use public key fingerprints to identify favorites persistently
- Add isFavorite delegate method to check favorites by fingerprint
This commit is contained in:
jack
2025-07-04 01:51:37 +02:00
parent fe986ed397
commit c7baf31f03
4 changed files with 205 additions and 35 deletions
+3
View File
@@ -92,4 +92,7 @@ protocol BitchatDelegate: AnyObject {
func didConnectToPeer(_ peerID: String)
func didDisconnectFromPeer(_ peerID: String)
func didUpdatePeerList(_ peers: [String])
// Optional method to check if a fingerprint belongs to a favorite peer
@objc optional func isFavorite(fingerprint: String) -> Bool
}
+180 -4
View File
@@ -1,6 +1,7 @@
import Foundation
import CoreBluetooth
import Combine
import CryptoKit
#if os(macOS)
import AppKit
#else
@@ -37,10 +38,23 @@ class BluetoothMeshService: NSObject {
private let encryptionService = EncryptionService()
private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent)
private var processedMessages = Set<String>()
private let maxTTL: UInt8 = 3 // Reduced for efficiency
private let maxTTL: UInt8 = 5 // Increased for better reach
private var announcedToPeers = Set<String>() // Track which peers we've announced to
private var announcedPeers = Set<String>() // Track peers who have already been announced
// Store-and-forward message cache
private struct StoredMessage {
let packet: BitchatPacket
let timestamp: Date
let messageID: String
let isForFavorite: Bool // Messages for favorites stored indefinitely
}
private var messageCache: [StoredMessage] = []
private let messageCacheTimeout: TimeInterval = 43200 // 12 hours for regular peers
private let maxCachedMessages = 100 // For regular peers
private let maxCachedMessagesForFavorites = 1000 // Much larger cache for favorites
private var favoriteMessageQueue: [String: [StoredMessage]] = [:] // Per-favorite message queues
// Battery and range optimizations
private var scanDutyCycleTimer: Timer?
private var isActivelyScanning = true
@@ -55,6 +69,16 @@ class BluetoothMeshService: NSObject {
let myPeerID: String
// Helper method to get fingerprint from public key data
private func getPublicKeyFingerprint(_ publicKeyData: Data) -> String {
let fingerprint = SHA256.hash(data: publicKeyData)
.compactMap { String(format: "%02x", $0) }
.joined()
.prefix(16) // Use first 16 chars for brevity
.lowercased()
return String(fingerprint)
}
override init() {
// Generate ephemeral peer ID for each session to prevent tracking
// Use random bytes instead of UUID for better anonymity
@@ -475,6 +499,131 @@ class BluetoothMeshService: NSObject {
return Array(announcedPeers).sorted()
}
// MARK: - Store-and-Forward Methods
private func cacheMessage(_ packet: BitchatPacket, messageID: String) {
messageQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
// Don't cache certain message types
guard packet.type != MessageType.keyExchange.rawValue,
packet.type != MessageType.announce.rawValue,
packet.type != MessageType.leave.rawValue,
packet.type != MessageType.ack.rawValue,
packet.type != MessageType.fragmentStart.rawValue,
packet.type != MessageType.fragmentContinue.rawValue,
packet.type != MessageType.fragmentEnd.rawValue else {
return
}
// Check if this is a private message for a favorite
var isForFavorite = false
if packet.type == MessageType.privateMessage.rawValue,
let recipientID = packet.recipientID,
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
// Check if recipient is a favorite via their public key fingerprint
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
isForFavorite = self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false
}
}
// Create stored message
let storedMessage = StoredMessage(
packet: packet,
timestamp: Date(),
messageID: messageID,
isForFavorite: isForFavorite
)
if isForFavorite {
// Store in favorite-specific queue
if let recipientID = packet.recipientID,
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
if self.favoriteMessageQueue[recipientPeerID] == nil {
self.favoriteMessageQueue[recipientPeerID] = []
}
self.favoriteMessageQueue[recipientPeerID]?.append(storedMessage)
// Limit favorite queue size
if let count = self.favoriteMessageQueue[recipientPeerID]?.count,
count > self.maxCachedMessagesForFavorites {
self.favoriteMessageQueue[recipientPeerID]?.removeFirst()
}
print("[CACHE] Cached message for favorite \(recipientPeerID), queue size: \(self.favoriteMessageQueue[recipientPeerID]?.count ?? 0)")
}
} else {
// Clean up old messages first (only for regular cache)
self.cleanupMessageCache()
// Add to regular cache
self.messageCache.append(storedMessage)
// Limit cache size
if self.messageCache.count > self.maxCachedMessages {
self.messageCache.removeFirst()
}
print("[CACHE] Cached message (type: \(packet.type)), cache size: \(self.messageCache.count)")
}
}
}
private func cleanupMessageCache() {
let cutoffTime = Date().addingTimeInterval(-messageCacheTimeout)
// Only remove non-favorite messages that are older than timeout
messageCache.removeAll { !$0.isForFavorite && $0.timestamp < cutoffTime }
}
private func sendCachedMessages(to peerID: String) {
messageQueue.async { [weak self] in
guard let self = self,
let peripheral = self.connectedPeripherals[peerID],
let characteristic = self.peripheralCharacteristics[peripheral] else {
return
}
// Clean up old messages first
self.cleanupMessageCache()
var messagesToSend: [StoredMessage] = []
// First, check if this peer has any favorite messages waiting
if let favoriteMessages = self.favoriteMessageQueue[peerID] {
messagesToSend.append(contentsOf: favoriteMessages)
// Clear the favorite queue after adding to send list
self.favoriteMessageQueue[peerID] = nil
print("[CACHE] Found \(favoriteMessages.count) favorite messages for \(peerID)")
}
// Then add regular cached messages
messagesToSend.append(contentsOf: self.messageCache)
print("[CACHE] Sending \(messagesToSend.count) total cached messages to \(peerID)")
// Send cached messages with slight delay between each
for (index, storedMessage) in messagesToSend.enumerated() {
let delay = Double(index) * 0.1 // 100ms between messages
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak peripheral] in
guard let self = self,
let peripheral = peripheral,
peripheral.state == .connected else { return }
// Create a new packet with fresh timestamp
var packet = storedMessage.packet
packet.timestamp = UInt64(Date().timeIntervalSince1970)
if let data = packet.toBinaryData() {
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
print("[CACHE] Sent cached message \(index + 1)/\(messagesToSend.count) to \(peerID)")
}
}
}
}
}
private func estimateDistance(rssi: Int) -> Int {
// Rough distance estimation based on RSSI
// Using path loss formula: RSSI = TxPower - 10 * n * log10(distance)
@@ -683,6 +832,9 @@ class BluetoothMeshService: NSObject {
// Send announce with our nickname immediately
print("[KEY_EXCHANGE] Calling sendAnnouncementToPeer for \(senderID)")
self.sendAnnouncementToPeer(senderID)
// Check if this peer has cached messages (especially for favorites)
self.sendCachedMessages(to: senderID)
}
}
@@ -724,9 +876,22 @@ class BluetoothMeshService: NSObject {
// Check if this is a favorite peer and send notification
// Note: This might not work immediately if key exchange hasn't happened yet
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let viewModel = self.delegate as? ChatViewModel,
viewModel.isFavorite(peerID: senderID) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
// Check if this is a favorite using their public key fingerprint
if let publicKeyData = self.encryptionService.getPeerIdentityKey(senderID) {
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
if self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false {
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
// Send any cached messages for this favorite
print("[CACHE] Favorite peer \(senderID) came online, checking for cached messages")
self.sendCachedMessages(to: senderID)
}
} else if let viewModel = self.delegate as? ChatViewModel,
viewModel.isFavorite(peerID: senderID) {
// Fallback for backwards compatibility
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
}
}
@@ -845,6 +1010,17 @@ class BluetoothMeshService: NSObject {
print("[PRIVATE] Relaying message not meant for us (TTL: \(packet.ttl))")
var relayPacket = packet
relayPacket.ttl -= 1
// Check if this message is for an offline favorite and cache it
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
if self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false {
// This is for a favorite peer - cache it even if they're offline
print("[CACHE] Caching relayed message for offline favorite: \(recipientIDString)")
self.cacheMessage(relayPacket, messageID: messageID)
}
}
self.broadcastPacket(relayPacket)
}
} else {
+18 -31
View File
@@ -51,43 +51,30 @@ class EncryptionService {
// Add peer's combined public keys
func addPeerPublicKey(_ peerID: String, publicKeyData: Data) throws {
print("[CRYPTO] Received public key data of size: \(publicKeyData.count)")
// Convert to array for safe access
let keyBytes = [UInt8](publicKeyData)
if keyBytes.count == 96 {
// New format: 32 for key agreement + 32 for signing + 32 for identity
let keyAgreementData = Data(keyBytes[0..<32])
let signingKeyData = Data(keyBytes[32..<64])
let identityKeyData = Data(keyBytes[64..<96])
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData)
peerPublicKeys[peerID] = publicKey
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData)
peerSigningKeys[peerID] = signingKey
let identityKey = try Curve25519.Signing.PublicKey(rawRepresentation: identityKeyData)
peerIdentityKeys[peerID] = identityKey
print("[CRYPTO] Stored all three keys for peer \(peerID)")
} else if keyBytes.count == 64 {
// Handle old format (64 bytes) for backward compatibility
print("[CRYPTO] Received old key format (64 bytes), no identity key")
let keyAgreementData = Data(keyBytes[0..<32])
let signingKeyData = Data(keyBytes[32..<64])
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData)
peerPublicKeys[peerID] = publicKey
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData)
peerSigningKeys[peerID] = signingKey
} else {
print("[CRYPTO] Invalid public key data size: \(keyBytes.count)")
guard keyBytes.count == 96 else {
print("[CRYPTO] Invalid public key data size: \(keyBytes.count), expected 96")
throw EncryptionError.invalidPublicKey
}
// Extract all three keys: 32 for key agreement + 32 for signing + 32 for identity
let keyAgreementData = Data(keyBytes[0..<32])
let signingKeyData = Data(keyBytes[32..<64])
let identityKeyData = Data(keyBytes[64..<96])
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData)
peerPublicKeys[peerID] = publicKey
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData)
peerSigningKeys[peerID] = signingKey
let identityKey = try Curve25519.Signing.PublicKey(rawRepresentation: identityKeyData)
peerIdentityKeys[peerID] = identityKey
print("[CRYPTO] Stored all three keys for peer \(peerID)")
// Generate shared secret for encryption
if let publicKey = peerPublicKeys[peerID] {
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)
+4
View File
@@ -652,4 +652,8 @@ extension ChatViewModel: BitchatDelegate {
return Array(Set(mentions)) // Remove duplicates
}
func isFavorite(fingerprint: String) -> Bool {
return favoritePeers.contains(fingerprint)
}
}