Files
bitchat/bitchat/Services/FavoritesPersistenceService.swift
T
islam 5d6aecfc83 Replace .log w/ explicit .debug/.error functions
This would make the intention more explicit so we can overload different logging types as well like keychain, security events, etc…

Search/Replace Strategies:

1.
Search regex: `SecureLogger\.log\(\s*(.*?),\s*category:\s*(.*?),\s*level:\s*\.(\w+)\s*\)`
Replace regex: `SecureLogger.$3($1, category: $2)`

Sample input:
```
SecureLogger.log(
    "🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
    category: .session,
    level: .debug
)
```

Sample output:
`SecureLogger.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session)`

---

2.
Search regex: `SecureLogger\.log\((.*?)\)`
Replace regex: `SecureLogger.debug($1)` (as it’s the default level)

Sample input:
`SecureLogger.log("some text")`

Sample output:
`SecureLogger.debug("some text")`

---

3
Manual changes:
ChatViewModel line 5393 (commented code)
NostrRelayManager line 196 (commented code)
NostrRelayManager lines 346-350 (if/else logic)
NostrRelayManager line 371 (commented code)
2025-09-11 19:03:08 +01:00

417 lines
16 KiB
Swift

import Foundation
import Combine
/// Manages persistent favorite relationships between peers
@MainActor
final class FavoritesPersistenceService: ObservableObject {
struct FavoriteRelationship: Codable {
let peerNoisePublicKey: Data
let peerNostrPublicKey: String?
let peerNickname: String
let isFavorite: Bool
let theyFavoritedUs: Bool
let favoritedAt: Date
let lastUpdated: Date
var isMutual: Bool {
isFavorite && theyFavoritedUs
}
}
private static let storageKey = "chat.bitchat.favorites"
private static let keychainService = "chat.bitchat.favorites"
@Published private(set) var favorites: [Data: FavoriteRelationship] = [:] // Noise pubkey -> relationship
@Published private(set) var mutualFavorites: Set<Data> = []
private let userDefaults = UserDefaults.standard
private var cancellables = Set<AnyCancellable>()
static let shared = FavoritesPersistenceService()
private init() {
loadFavorites()
// Update mutual favorites when favorites change
$favorites
.map { favorites in
Set(favorites.compactMap { $0.value.isMutual ? $0.key : nil })
}
.assign(to: &$mutualFavorites)
}
/// Add or update a favorite
func addFavorite(
peerNoisePublicKey: Data,
peerNostrPublicKey: String? = nil,
peerNickname: String
) {
SecureLogger.info("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", category: .session)
let existing = favorites[peerNoisePublicKey]
let relationship = FavoriteRelationship(
peerNoisePublicKey: peerNoisePublicKey,
peerNostrPublicKey: peerNostrPublicKey ?? existing?.peerNostrPublicKey,
peerNickname: peerNickname,
isFavorite: true,
theyFavoritedUs: existing?.theyFavoritedUs ?? false,
favoritedAt: existing?.favoritedAt ?? Date(),
lastUpdated: Date()
)
// Log if this creates a mutual favorite
if relationship.isMutual {
SecureLogger.info("💕 Mutual favorite relationship established with \(peerNickname)!", category: .session)
}
favorites[peerNoisePublicKey] = relationship
saveFavorites()
// Notify observers
NotificationCenter.default.post(
name: .favoriteStatusChanged,
object: nil,
userInfo: ["peerPublicKey": peerNoisePublicKey]
)
}
/// Remove a favorite
func removeFavorite(peerNoisePublicKey: Data) {
guard let existing = favorites[peerNoisePublicKey] else { return }
SecureLogger.info("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", category: .session)
// If they still favorite us, keep the record but mark us as not favoriting
if existing.theyFavoritedUs {
let updated = FavoriteRelationship(
peerNoisePublicKey: existing.peerNoisePublicKey,
peerNostrPublicKey: existing.peerNostrPublicKey,
peerNickname: existing.peerNickname,
isFavorite: false,
theyFavoritedUs: true,
favoritedAt: existing.favoritedAt,
lastUpdated: Date()
)
favorites[peerNoisePublicKey] = updated
// Keeping record - they still favorite us
} else {
// Neither side favorites, remove completely
favorites.removeValue(forKey: peerNoisePublicKey)
// Completely removed from favorites
}
saveFavorites()
// Notify observers
NotificationCenter.default.post(
name: .favoriteStatusChanged,
object: nil,
userInfo: ["peerPublicKey": peerNoisePublicKey]
)
}
/// Update when we learn a peer favorited/unfavorited us
func updatePeerFavoritedUs(
peerNoisePublicKey: Data,
favorited: Bool,
peerNickname: String? = nil,
peerNostrPublicKey: String? = nil
) {
let existing = favorites[peerNoisePublicKey]
let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown"
SecureLogger.info("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us", category: .session)
let relationship = FavoriteRelationship(
peerNoisePublicKey: peerNoisePublicKey,
peerNostrPublicKey: peerNostrPublicKey ?? existing?.peerNostrPublicKey,
peerNickname: displayName,
isFavorite: existing?.isFavorite ?? false,
theyFavoritedUs: favorited,
favoritedAt: existing?.favoritedAt ?? Date(),
lastUpdated: Date()
)
if !relationship.isFavorite && !relationship.theyFavoritedUs {
// Neither side favorites, remove completely
favorites.removeValue(forKey: peerNoisePublicKey)
// Removed - neither side favorites anymore
} else {
favorites[peerNoisePublicKey] = relationship
// Check if this creates a mutual favorite
if relationship.isMutual {
SecureLogger.info("💕 Mutual favorite relationship established with \(displayName)!", category: .session)
}
}
saveFavorites()
// Notify observers
NotificationCenter.default.post(
name: .favoriteStatusChanged,
object: nil,
userInfo: ["peerPublicKey": peerNoisePublicKey]
)
}
/// Check if a peer is favorited by us
func isFavorite(_ peerNoisePublicKey: Data) -> Bool {
favorites[peerNoisePublicKey]?.isFavorite ?? false
}
/// Check if we have a mutual favorite relationship
func isMutualFavorite(_ peerNoisePublicKey: Data) -> Bool {
favorites[peerNoisePublicKey]?.isMutual ?? false
}
/// Get favorite status for a peer
func getFavoriteStatus(for peerNoisePublicKey: Data) -> FavoriteRelationship? {
favorites[peerNoisePublicKey]
}
/// Resolve favorite status by short peer ID (16-hex derived from Noise pubkey)
/// Falls back to scanning favorites and matching on derived peer ID.
func getFavoriteStatus(forPeerID peerID: String) -> FavoriteRelationship? {
// Quick sanity: peerID should be 16 hex chars (8 bytes)
guard peerID.count == 16 else { return nil }
for (pubkey, rel) in favorites {
let derived = PeerIDUtils.derivePeerID(fromPublicKey: pubkey)
if derived == peerID { return rel }
}
return nil
}
/// Update Nostr public key for a peer
func updateNostrPublicKey(for peerNoisePublicKey: Data, nostrPubkey: String) {
guard let existing = favorites[peerNoisePublicKey] else { return }
let updated = FavoriteRelationship(
peerNoisePublicKey: existing.peerNoisePublicKey,
peerNostrPublicKey: nostrPubkey,
peerNickname: existing.peerNickname,
isFavorite: existing.isFavorite,
theyFavoritedUs: existing.theyFavoritedUs,
favoritedAt: existing.favoritedAt,
lastUpdated: Date()
)
favorites[peerNoisePublicKey] = updated
saveFavorites()
}
/// Update nickname for an existing favorite
func updateNickname(for peerNoisePublicKey: Data, newNickname: String) {
guard let existing = favorites[peerNoisePublicKey] else { return }
// Skip if nickname hasn't changed
if existing.peerNickname == newNickname { return }
// Updating nickname for favorite
let updated = FavoriteRelationship(
peerNoisePublicKey: existing.peerNoisePublicKey,
peerNostrPublicKey: existing.peerNostrPublicKey,
peerNickname: newNickname,
isFavorite: existing.isFavorite,
theyFavoritedUs: existing.theyFavoritedUs,
favoritedAt: existing.favoritedAt,
lastUpdated: Date()
)
favorites[peerNoisePublicKey] = updated
saveFavorites()
// Notify observers
NotificationCenter.default.post(
name: .favoriteStatusChanged,
object: nil,
userInfo: ["peerPublicKey": peerNoisePublicKey]
)
}
/// Update noise public key when peer reconnects with new ID
func updateNoisePublicKey(from oldKey: Data, to newKey: Data, peerNickname: String) {
guard let existing = favorites[oldKey] else {
SecureLogger.warning("⚠️ Cannot update noise key - no favorite found for \(oldKey.hexEncodedString())", category: .session)
return
}
// Check if we already have a favorite with the new key
if favorites[newKey] != nil {
SecureLogger.warning("⚠️ Favorite already exists with new key \(newKey.hexEncodedString()), removing old entry", category: .session)
favorites.removeValue(forKey: oldKey)
saveFavorites()
return
}
// Updating noise public key
// Remove old entry
favorites.removeValue(forKey: oldKey)
// Add with new key
let updated = FavoriteRelationship(
peerNoisePublicKey: newKey,
peerNostrPublicKey: existing.peerNostrPublicKey,
peerNickname: peerNickname,
isFavorite: existing.isFavorite,
theyFavoritedUs: existing.theyFavoritedUs,
favoritedAt: existing.favoritedAt,
lastUpdated: Date()
)
favorites[newKey] = updated
saveFavorites()
// Notify observers with both old and new keys
NotificationCenter.default.post(
name: .favoriteStatusChanged,
object: nil,
userInfo: [
"peerPublicKey": newKey,
"oldPeerPublicKey": oldKey,
"isKeyUpdate": true
]
)
}
/// Get all favorites (including non-mutual)
func getAllFavorites() -> [FavoriteRelationship] {
favorites.values.filter { $0.isFavorite }
}
/// Get only mutual favorites
func getMutualFavorites() -> [FavoriteRelationship] {
favorites.values.filter { $0.isMutual }
}
/// Get all favorite relationships (including where they favorited us)
func getAllRelationships() -> [FavoriteRelationship] {
Array(favorites.values)
}
/// Clear all favorites - used for panic mode
func clearAllFavorites() {
SecureLogger.warning("🧹 Clearing all favorites (panic mode)", category: .session)
favorites.removeAll()
saveFavorites()
// Delete from keychain directly
KeychainHelper.delete(
key: Self.storageKey,
service: Self.keychainService
)
// Post notification for UI update
NotificationCenter.default.post(name: .favoriteStatusChanged, object: nil)
}
// MARK: - Persistence
private func saveFavorites() {
let relationships = Array(favorites.values)
// Saving favorite relationships to keychain
do {
let encoder = JSONEncoder()
let data = try encoder.encode(relationships)
// Store in keychain for security
KeychainHelper.save(
key: Self.storageKey,
data: data,
service: Self.keychainService
)
// Successfully saved favorites
} catch {
SecureLogger.error("Failed to save favorites: \(error)", category: .session)
}
}
private func loadFavorites() {
// Loading favorites from keychain
guard let data = KeychainHelper.load(
key: Self.storageKey,
service: Self.keychainService
) else {
return
}
do {
let decoder = JSONDecoder()
let relationships = try decoder.decode([FavoriteRelationship].self, from: data)
SecureLogger.info("✅ Loaded \(relationships.count) favorite relationships", category: .session)
// Log Nostr public key info
for relationship in relationships {
if relationship.peerNostrPublicKey == nil {
SecureLogger.warning("⚠️ No Nostr public key stored for '\(relationship.peerNickname)'", category: .session)
}
}
// Convert to dictionary, cleaning up duplicates by public key (not nickname)
var seenPublicKeys: [Data: FavoriteRelationship] = [:]
var cleanedRelationships: [FavoriteRelationship] = []
for relationship in relationships {
// Check for duplicates by public key (the actual unique identifier)
if let existing = seenPublicKeys[relationship.peerNoisePublicKey] {
SecureLogger.warning("⚠️ Duplicate favorite found for public key \(relationship.peerNoisePublicKey.hexEncodedString()) - nicknames: '\(existing.peerNickname)' vs '\(relationship.peerNickname)'", category: .session)
// Keep the most recent or most complete relationship
if relationship.lastUpdated > existing.lastUpdated ||
(relationship.peerNostrPublicKey != nil && existing.peerNostrPublicKey == nil) {
// Replace with newer/more complete entry
seenPublicKeys[relationship.peerNoisePublicKey] = relationship
cleanedRelationships.removeAll { $0.peerNoisePublicKey == relationship.peerNoisePublicKey }
cleanedRelationships.append(relationship)
}
} else {
seenPublicKeys[relationship.peerNoisePublicKey] = relationship
cleanedRelationships.append(relationship)
}
}
// If we cleaned up duplicates, save the cleaned list
if cleanedRelationships.count < relationships.count {
// Cleaned up duplicates
// Clear and rebuild favorites dictionary
favorites.removeAll()
for relationship in cleanedRelationships {
favorites[relationship.peerNoisePublicKey] = relationship
}
// Save cleaned favorites
saveFavorites()
// Notify that favorites have been cleaned up (synchronously since we're already on main actor)
NotificationCenter.default.post(name: .favoriteStatusChanged, object: nil)
} else {
// No duplicates, just populate normally
for relationship in cleanedRelationships {
favorites[relationship.peerNoisePublicKey] = relationship
}
}
// Log loaded relationships
// Loaded relationships successfully
} catch {
SecureLogger.error("Failed to load favorites: \(error)", category: .session)
}
}
}
// MARK: - Notification Names
extension Notification.Name {
static let favoriteStatusChanged = Notification.Name("FavoriteStatusChanged")
}