PeerID 21/n: UnifiedPeerService (#772)

This commit is contained in:
Islam
2025-10-07 16:37:47 +02:00
committed by GitHub
parent 7ec84857eb
commit 302c741d58
2 changed files with 50 additions and 87 deletions
+25 -62
View File
@@ -18,14 +18,14 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// MARK: - Published Properties
@Published private(set) var peers: [BitchatPeer] = []
@Published private(set) var connectedPeerIDs: Set<String> = []
@Published private(set) var connectedPeerIDs: Set<PeerID> = []
@Published private(set) var favorites: [BitchatPeer] = []
@Published private(set) var mutualFavorites: [BitchatPeer] = []
// MARK: - Private Properties
private var peerIndex: [String: BitchatPeer] = [:]
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
private var peerIndex: [PeerID: BitchatPeer] = [:]
private var fingerprintCache: [PeerID: String] = [:]
private let meshService: Transport
private let identityManager: SecureIdentityStateManagerProtocol
weak var messageRouter: MessageRouter?
@@ -97,7 +97,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Update fingerprint cache
if let publicKey = peerInfo.noisePublicKey {
fingerprintCache[peerID.id] = publicKey.sha256Fingerprint()
fingerprintCache[peerID] = publicKey.sha256Fingerprint()
}
}
@@ -114,12 +114,12 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
}
if isConnectedByNickname { continue }
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID.id)
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID)
enrichedPeers.append(peer)
addedPeerIDs.insert(peerID)
// Update fingerprint cache
fingerprintCache[peerID.id] = favoriteKey.sha256Fingerprint()
fingerprintCache[peerID] = favoriteKey.sha256Fingerprint()
}
// Phase 3: Sort peers
@@ -137,10 +137,10 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Phase 4: Build subsets and indices
var favoritesList: [BitchatPeer] = []
var mutualsList: [BitchatPeer] = []
var newIndex: [String: BitchatPeer] = [:]
var newIndex: [PeerID: BitchatPeer] = [:]
for peer in enrichedPeers {
newIndex[peer.peerID.id] = peer
newIndex[peer.peerID] = peer
if peer.isFavorite {
favoritesList.append(peer)
@@ -155,7 +155,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
p.isConnected || p.isReachable || p.isMutualFavorite
}
self.peers = filtered
self.connectedPeerIDs = Set(connected.map(\.id))
self.connectedPeerIDs = connected
self.favorites = favoritesList
self.mutualFavorites = mutualsList
self.peerIndex = newIndex
@@ -204,10 +204,10 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
private func buildPeerFromFavorite(
favorite: FavoritesPersistenceService.FavoriteRelationship,
peerID: String
peerID: PeerID
) -> BitchatPeer {
var peer = BitchatPeer(
peerID: PeerID(str: peerID),
peerID: peerID,
noisePublicKey: favorite.peerNoisePublicKey,
nickname: favorite.peerNickname,
lastSeen: favorite.lastUpdated,
@@ -224,8 +224,8 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// MARK: - Public Methods
/// Get peer by ID
func getPeer(by id: String) -> BitchatPeer? {
return peerIndex[id]
func getPeer(by peerID: PeerID) -> BitchatPeer? {
return peerIndex[peerID]
}
/// Get peer ID for nickname
@@ -238,13 +238,8 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
return nil
}
/// Check if peer is online
func isOnline(_ peerID: String) -> Bool {
return connectedPeerIDs.contains(peerID)
}
/// Check if peer is blocked
func isBlocked(_ peerID: String) -> Bool {
func isBlocked(_ peerID: PeerID) -> Bool {
// Get fingerprint
guard let fingerprint = getFingerprint(for: peerID) else { return false }
@@ -257,8 +252,8 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
}
/// Toggle favorite status
func toggleFavorite(_ peerID: String) {
guard let peer = getPeer(by: peerID) else {
func toggleFavorite(_ peerID: PeerID) {
guard let peer = getPeer(by: peerID) else {
SecureLogger.warning("⚠️ Cannot toggle favorite - peer not found: \(peerID)", category: .session)
return
}
@@ -273,7 +268,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
if actualNickname.isEmpty {
// Try to get from mesh service's current peer list
if let meshPeerNickname = meshService.peerNickname(peerID: PeerID(str: peerID)) {
if let meshPeerNickname = meshService.peerNickname(peerID: peerID) {
actualNickname = meshPeerNickname
SecureLogger.debug("🔍 Got nickname from mesh service: '\(actualNickname)'", category: .session)
}
@@ -306,10 +301,10 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Send favorite notification to the peer via router (mesh or Nostr)
if let router = messageRouter {
router.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: !wasFavorite)
router.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
} else {
// Fallback to mesh-only if router not yet wired
meshService.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: !wasFavorite)
meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
}
// Force update of peers to reflect the change
@@ -321,46 +316,14 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
}
}
/// Toggle blocked status
func toggleBlocked(_ peerID: String) {
guard let fingerprint = getFingerprint(for: peerID) else { return }
// Get or create social identity
var identity = identityManager.getSocialIdentity(for: fingerprint)
?? SocialIdentity(
fingerprint: fingerprint,
localPetname: nil,
claimedNickname: getPeer(by: peerID)?.displayName ?? "Unknown",
trustLevel: .unknown,
isFavorite: false,
isBlocked: false,
notes: nil
)
// Toggle blocked status
identity.isBlocked = !identity.isBlocked
// Can't be both favorite and blocked
if identity.isBlocked {
identity.isFavorite = false
// Also remove from favorites service
if let peer = getPeer(by: peerID) {
favoritesService.removeFavorite(peerNoisePublicKey: peer.noisePublicKey)
}
}
identityManager.updateSocialIdentity(identity)
}
/// Get fingerprint for peer ID
func getFingerprint(for peerID: String) -> String? {
func getFingerprint(for peerID: PeerID) -> String? {
// Check cache first
if let cached = fingerprintCache[peerID] {
return cached
}
// Try to get from mesh service
if let fingerprint = meshService.getFingerprint(for: PeerID(str: peerID)) {
if let fingerprint = meshService.getFingerprint(for: peerID) {
fingerprintCache[peerID] = fingerprint
return fingerprint
}
@@ -378,13 +341,13 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// MARK: - Compatibility Methods (for easy migration)
var allPeers: [BitchatPeer] { peers }
var connectedPeers: [String] { Array(connectedPeerIDs) }
var favoritePeers: Set<String> {
Set(favorites.compactMap { getFingerprint(for: $0.peerID.id) })
var connectedPeers: [PeerID] { Array(connectedPeerIDs) }
var favoritePeers: Set<String> {
Set(favorites.compactMap { getFingerprint(for: $0.peerID) })
}
var blockedUsers: Set<String> {
Set(peers.compactMap { peer in
isBlocked(peer.peerID.id) ? getFingerprint(for: peer.peerID.id) : nil
isBlocked(peer.peerID) ? getFingerprint(for: peer.peerID) : nil
})
}
}
+25 -25
View File
@@ -251,7 +251,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Computed properties for compatibility
@MainActor
var connectedPeers: [String] { Array(unifiedPeerService.connectedPeerIDs) }
var connectedPeers: [String] { Array(unifiedPeerService.connectedPeerIDs.map(\.id)) }
@Published var allPeers: [BitchatPeer] = []
var privateChats: [String: [BitchatMessage]] {
get { privateChatManager.privateChats }
@@ -1186,7 +1186,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Check if messages are stored under the stable Noise key hex
if let peer = unifiedPeerService.getPeer(by: peerID) {
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
if unreadPrivateMessages.contains(noiseKeyHex) {
return true
@@ -1233,7 +1233,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let ephemeralID = ephemeralPeerID {
// Found the ephemeral peer, use normal toggle
unifiedPeerService.toggleFavorite(ephemeralID.id)
unifiedPeerService.toggleFavorite(ephemeralID)
// Also trigger UI update
objectWillChange.send()
} else {
@@ -1278,7 +1278,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
} else {
// This is an ephemeral peer ID (16 hex chars), use normal toggle
unifiedPeerService.toggleFavorite(peerID)
unifiedPeerService.toggleFavorite(PeerID(str: peerID))
// Trigger UI update
objectWillChange.send()
}
@@ -1294,7 +1294,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
} else {
// This is an ephemeral peer ID - check with UnifiedPeerService
if let peer = unifiedPeerService.getPeer(by: peerID) {
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
return peer.isFavorite
}
}
@@ -1306,7 +1306,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor
func isPeerBlocked(_ peerID: String) -> Bool {
return unifiedPeerService.isBlocked(peerID)
return unifiedPeerService.isBlocked(PeerID(str: peerID))
}
// Helper method to find current peer ID for a fingerprint
@@ -2206,7 +2206,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
guard !content.isEmpty else { return }
// Check if blocked
if unifiedPeerService.isBlocked(peerID) {
if unifiedPeerService.isBlocked(PeerID(str: peerID)) {
let nickname = meshService.peerNickname(peerID: PeerID(str: peerID)) ?? "user"
addSystemMessage(
String(
@@ -2458,7 +2458,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let peerNickname = meshService.peerNickname(peerID: PeerID(str: peerID)) ?? "unknown"
// Check if the peer is blocked
if unifiedPeerService.isBlocked(peerID) {
if unifiedPeerService.isBlocked(PeerID(str: peerID)) {
addSystemMessage(
String(
format: String(localized: "system.chat.blocked", comment: "System message when starting chat fails because peer is blocked"),
@@ -2470,7 +2470,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Check mutual favorites for offline messaging
if let peer = unifiedPeerService.getPeer(by: peerID),
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)),
peer.isFavorite && !peer.theyFavoritedUs && !peer.isConnected {
addSystemMessage(
String(
@@ -2484,7 +2484,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Consolidate messages from stable Noise key if needed
// This ensures Nostr messages appear when opening a chat with an ephemeral peer ID
if let peer = unifiedPeerService.getPeer(by: peerID) {
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
// If we have messages stored under the stable Noise key hex but not under the ephemeral ID,
@@ -3045,7 +3045,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
peerNostrPubkey = favoriteStatus.peerNostrPublicKey
}
// Otherwise get the Noise key from the peer info
else if let peer = unifiedPeerService.getPeer(by: peerID) {
else if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
noiseKeyHex = peer.noisePublicKey.hexEncodedString()
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey)
peerNostrPubkey = favoriteStatus?.peerNostrPublicKey
@@ -3068,7 +3068,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Skip if we already sent an ACK for this message
if !sentReadReceipts.contains(message.id) {
// Use stable Noise key hex if available; else fall back to peerID
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID)
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: PeerID(str: peerID))?.noisePublicKey.hexEncodedString() ?? peerID)
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID.id, readerNickname: nickname)
messageRouter.sendReadReceipt(receipt, to: PeerID(str: recipPeer))
sentReadReceipts.insert(message.id)
@@ -3088,7 +3088,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Also include messages stored under the stable Noise key (Nostr path)
if let peer = unifiedPeerService.getPeer(by: peerID) {
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
if noiseKeyHex != peerID, let nostrMessages = privateChats[noiseKeyHex] {
combined.append(contentsOf: nostrMessages)
@@ -4247,7 +4247,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor
func getFingerprint(for peerID: String) -> String? {
return unifiedPeerService.getFingerprint(for: peerID)
return unifiedPeerService.getFingerprint(for: PeerID(str: peerID))
}
//
@@ -4337,7 +4337,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Also log any offline favorites and whether we consider them verified
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
for fav in offlineFavorites {
let fp = unifiedPeerService.getFingerprint(for: fav.peerID.id)
let fp = unifiedPeerService.getFingerprint(for: fav.peerID)
let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false
let fpShort = fp?.prefix(8) ?? "nil"
SecureLogger.info("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: .security)
@@ -4454,7 +4454,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
switch type {
case .privateMessage:
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
let senderName = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? "Unknown"
let senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
let pmMentions = parseMentions(from: pm.content)
let msg = BitchatMessage(
id: pm.messageID,
@@ -4474,7 +4474,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
case .delivered:
guard let messageID = String(data: payload, encoding: .utf8) else { return }
if let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname {
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
if let messages = privateChats[peerID.id], let idx = messages.firstIndex(where: { $0.id == messageID }) {
privateChats[peerID.id]?[idx].deliveryStatus = .delivered(to: name, at: Date())
objectWillChange.send()
@@ -4483,7 +4483,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
case .readReceipt:
guard let messageID = String(data: payload, encoding: .utf8) else { return }
if let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname {
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
if let messages = privateChats[peerID.id], let idx = messages.firstIndex(where: { $0.id == messageID }) {
privateChats[peerID.id]?[idx].deliveryStatus = .read(by: name, at: Date())
objectWillChange.send()
@@ -4507,7 +4507,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let last = lastMutualToastAt[fp] ?? .distantPast
if now.timeIntervalSince(last) > 60 { // 1-minute throttle
lastMutualToastAt[fp] = now
let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? resolveNickname(for: peerID.id)
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID.id)
NotificationService.shared.sendLocalNotification(
title: "Mutual verification",
body: "You and \(name) verified each other",
@@ -4533,7 +4533,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
identityManager.setVerified(fingerprint: fp, verified: true)
identityManager.forceSave()
verifiedFingerprints.insert(fp)
let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? resolveNickname(for: peerID.id)
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID.id)
NotificationService.shared.sendLocalNotification(
title: "Verified",
body: "You verified \(name)",
@@ -4639,7 +4639,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
objectWillChange.send()
// Cache mapping to full Noise key for session continuity on disconnect
if let peer = unifiedPeerService.getPeer(by: peerID.id) {
if let peer = unifiedPeerService.getPeer(by: peerID) {
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
shortIDToNoiseKey[peerID.id] = noiseKeyHex
}
@@ -5193,7 +5193,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if selectedPrivateChatPeer == targetPeerID {
isViewingThisChat = true
} else if let selectedPeer = selectedPrivateChatPeer,
let selectedPeerData = unifiedPeerService.getPeer(by: selectedPeer),
let selectedPeerData = unifiedPeerService.getPeer(by: PeerID(str: selectedPeer)),
let key = actualSenderNoiseKey,
selectedPeerData.noisePublicKey == key {
isViewingThisChat = true
@@ -5458,7 +5458,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
// If not a hex key, get from peer service (ephemeral ID)
if noiseKey == nil, let peer = unifiedPeerService.getPeer(by: peerID) {
if noiseKey == nil, let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
noiseKey = peer.noisePublicKey
}
@@ -5651,7 +5651,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
noiseKey = hexKey
} else {
// It's an ephemeral peer ID, get the Noise key from UnifiedPeerService
if let peer = unifiedPeerService.getPeer(by: peerID) {
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
noiseKey = peer.noisePublicKey
}
}
@@ -5841,7 +5841,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// IMPORTANT: Also consolidate messages from stable Noise key if this is an ephemeral peer
// This ensures Nostr messages appear in BLE chats
if peerID.count == 16 { // This is an ephemeral peer ID (8 bytes = 16 hex chars)
if let peer = unifiedPeerService.getPeer(by: peerID) {
if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) {
let stableKeyHex = peer.noisePublicKey.hexEncodedString()
// If we have messages stored under the stable key, merge them