PeerID 30/n: Update some leftovers + case normalization (#839)

* PeerID 28/n: `ChatViewModel.getShortIDForNoiseKey`

* PeerID 29/n: `BLEService` + remove dupe funcs from #823

* `handleFileTransfer` to use PeerID

* `sendMessage` and `sendPrivateMessage`

* PeerID 30/n: Update some leftovers

* `lowercased()` inside PeerID for normalization

---------

Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com>
This commit is contained in:
Islam
2025-10-19 20:40:45 +02:00
committed by GitHub
co-authored by jack
parent 0dd999af6b
commit 0776c9813c
7 changed files with 48 additions and 60 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ struct PeerID: Equatable, Hashable {
// Private so the callers have to go through a convenience init // Private so the callers have to go through a convenience init
private init(prefix: Prefix, bare: any StringProtocol) { private init(prefix: Prefix, bare: any StringProtocol) {
self.prefix = prefix self.prefix = prefix
self.bare = String(bare) self.bare = String(bare).lowercased()
} }
} }
+3 -4
View File
@@ -2360,7 +2360,6 @@ extension BLEService {
private func configureNoiseServiceCallbacks(for service: NoiseEncryptionService) { private func configureNoiseServiceCallbacks(for service: NoiseEncryptionService) {
service.onPeerAuthenticated = { [weak self] peerID, fingerprint in service.onPeerAuthenticated = { [weak self] peerID, fingerprint in
let peerID = PeerID(str: peerID)
SecureLogger.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...") SecureLogger.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...")
self?.messageQueue.async { [weak self] in self?.messageQueue.async { [weak self] in
self?.sendPendingMessagesAfterHandshake(for: peerID) self?.sendPendingMessagesAfterHandshake(for: peerID)
@@ -3612,7 +3611,7 @@ extension BLEService {
private func checkPeerConnectivity() { private func checkPeerConnectivity() {
let now = Date() let now = Date()
var disconnectedPeers: [String] = [] var disconnectedPeers: [PeerID] = []
let peerIDsForLinkState: [PeerID] = collectionsQueue.sync { Array(peers.keys) } let peerIDsForLinkState: [PeerID] = collectionsQueue.sync { Array(peers.keys) }
var cachedLinkStates: [PeerID: (hasPeripheral: Bool, hasCentral: Bool)] = [:] var cachedLinkStates: [PeerID: (hasPeripheral: Bool, hasCentral: Bool)] = [:]
for peerID in peerIDsForLinkState { for peerID in peerIDsForLinkState {
@@ -3635,7 +3634,7 @@ extension BLEService {
var updated = peer var updated = peer
updated.isConnected = false updated.isConnected = false
peers[peerID] = updated peers[peerID] = updated
disconnectedPeers.append(peerID.id) disconnectedPeers.append(peerID)
} }
} }
// Cleanup: remove peers that are not connected and past reachability retention // Cleanup: remove peers that are not connected and past reachability retention
@@ -3660,7 +3659,7 @@ extension BLEService {
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs } let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
for peerID in disconnectedPeers { for peerID in disconnectedPeers {
self.delegate?.didDisconnectFromPeer(PeerID(str: peerID)) self.delegate?.didDisconnectFromPeer(peerID)
} }
// Publish snapshots so UnifiedPeerService updates connection/reachability icons // Publish snapshots so UnifiedPeerService updates connection/reachability icons
self.requestPeerDataPublish() self.requestPeerDataPublish()
@@ -177,18 +177,18 @@ final class NoiseEncryptionService {
private let rekeyCheckInterval: TimeInterval = 60.0 // Check every minute private let rekeyCheckInterval: TimeInterval = 60.0 // Check every minute
// Callbacks // Callbacks
private var onPeerAuthenticatedHandlers: [((String, String) -> Void)] = [] // Array of handlers for peer authentication private var onPeerAuthenticatedHandlers: [((PeerID, String) -> Void)] = [] // Array of handlers for peer authentication
var onHandshakeRequired: ((PeerID) -> Void)? // peerID needs handshake var onHandshakeRequired: ((PeerID) -> Void)? // peerID needs handshake
// Add a handler for peer authentication // Add a handler for peer authentication
func addOnPeerAuthenticatedHandler(_ handler: @escaping (String, String) -> Void) { func addOnPeerAuthenticatedHandler(_ handler: @escaping (PeerID, String) -> Void) {
serviceQueue.async(flags: .barrier) { [weak self] in serviceQueue.async(flags: .barrier) { [weak self] in
self?.onPeerAuthenticatedHandlers.append(handler) self?.onPeerAuthenticatedHandlers.append(handler)
} }
} }
// Legacy support - setting this will add to the handlers array // Legacy support - setting this will add to the handlers array
var onPeerAuthenticated: ((String, String) -> Void)? { var onPeerAuthenticated: ((PeerID, String) -> Void)? {
get { nil } // Always return nil for backward compatibility get { nil } // Always return nil for backward compatibility
set { set {
if let handler = newValue { if let handler = newValue {
@@ -546,7 +546,7 @@ final class NoiseEncryptionService {
// Notify all handlers about authentication // Notify all handlers about authentication
serviceQueue.async { [weak self] in serviceQueue.async { [weak self] in
self?.onPeerAuthenticatedHandlers.forEach { handler in self?.onPeerAuthenticatedHandlers.forEach { handler in
handler(peerID.id, fingerprint) handler(peerID, fingerprint)
} }
} }
} }
+2 -2
View File
@@ -61,11 +61,11 @@ final class NotificationService {
sendLocalNotification(title: title, body: body, identifier: identifier) sendLocalNotification(title: title, body: body, identifier: identifier)
} }
func sendPrivateMessageNotification(from sender: String, message: String, peerID: String) { func sendPrivateMessageNotification(from sender: String, message: String, peerID: PeerID) {
let title = "🔒 DM from \(sender)" let title = "🔒 DM from \(sender)"
let body = message let body = message
let identifier = "private-\(UUID().uuidString)" let identifier = "private-\(UUID().uuidString)"
let userInfo = ["peerID": peerID, "senderName": sender] let userInfo = ["peerID": peerID.id, "senderName": sender]
sendLocalNotification(title: title, body: body, identifier: identifier, userInfo: userInfo) sendLocalNotification(title: title, body: body, identifier: identifier, userInfo: userInfo)
} }
+13 -18
View File
@@ -25,7 +25,7 @@ final class GossipSyncManager {
// Storage: broadcast messages (ordered by insert), and latest announce per sender // Storage: broadcast messages (ordered by insert), and latest announce per sender
private var messages: [String: BitchatPacket] = [:] // idHex -> packet private var messages: [String: BitchatPacket] = [:] // idHex -> packet
private var messageOrder: [String] = [] private var messageOrder: [String] = []
private var latestAnnouncementByPeer: [String: (id: String, packet: BitchatPacket)] = [:] private var latestAnnouncementByPeer: [PeerID: (id: String, packet: BitchatPacket)] = [:]
// Timer // Timer
private var periodicTimer: DispatchSourceTimer? private var periodicTimer: DispatchSourceTimer?
@@ -101,8 +101,8 @@ final class GossipSyncManager {
if isAnnounce { if isAnnounce {
guard isAnnouncementFresh(packet) else { guard isAnnouncementFresh(packet) else {
let sender = packet.senderID.hexEncodedString().lowercased() let sender = PeerID(hexData: packet.senderID)
removeState(forNormalizedPeerID: sender) removeState(for: sender)
return return
} }
} }
@@ -121,7 +121,7 @@ final class GossipSyncManager {
} }
} }
} else if isAnnounce { } else if isAnnounce {
let sender = packet.senderID.hexEncodedString().lowercased() let sender = PeerID(hexData: packet.senderID)
latestAnnouncementByPeer[sender] = (id: idHex, packet: packet) latestAnnouncementByPeer[sender] = (id: idHex, packet: packet)
} }
} }
@@ -270,33 +270,28 @@ final class GossipSyncManager {
let nowMs = UInt64(now.timeIntervalSince1970 * 1000) let nowMs = UInt64(now.timeIntervalSince1970 * 1000)
guard nowMs >= timeoutMs else { return } guard nowMs >= timeoutMs else { return }
let cutoff = nowMs - timeoutMs let cutoff = nowMs - timeoutMs
let stalePeerIDs = latestAnnouncementByPeer.compactMap { (peerHex, pair) -> String? in let stalePeerIDs = latestAnnouncementByPeer.compactMap { peerID, pair in
pair.packet.timestamp < cutoff ? peerHex.lowercased() : nil pair.packet.timestamp < cutoff ? peerID : nil
} }
guard !stalePeerIDs.isEmpty else { return } guard !stalePeerIDs.isEmpty else { return }
for peerKey in stalePeerIDs { for peerKey in stalePeerIDs {
removeState(forNormalizedPeerID: peerKey) removeState(for: peerKey)
} }
} }
// Explicit removal hook for LEAVE/stale peer // Explicit removal hook for LEAVE/stale peer
func removeAnnouncementForPeer(_ peerID: PeerID) { func removeAnnouncementForPeer(_ peerID: PeerID) {
queue.async { [weak self] in queue.async { [weak self] in
self?._removeAnnouncementForPeer(peerID) self?.removeState(for: peerID)
} }
} }
private func _removeAnnouncementForPeer(_ peerID: PeerID) { private func removeState(for peerID: PeerID) {
let normalizedPeerID = peerID.id.lowercased() _ = latestAnnouncementByPeer.removeValue(forKey: peerID)
removeState(forNormalizedPeerID: normalizedPeerID)
}
private func removeState(forNormalizedPeerID normalizedPeerID: String) {
_ = latestAnnouncementByPeer.removeValue(forKey: normalizedPeerID)
// Remove messages from this peer // Remove messages from this peer
// Collect IDs to remove first to avoid concurrent modification // Collect IDs to remove first to avoid concurrent modification
let messageIdsToRemove = messages.compactMap { (id, message) -> String? in let messageIdsToRemove = messages.compactMap { (id, message) -> String? in
message.senderID.hexEncodedString().lowercased() == normalizedPeerID ? id : nil PeerID(hexData: message.senderID) == peerID ? id : nil
} }
// Remove messages and update messageOrder // Remove messages and update messageOrder
@@ -317,13 +312,13 @@ extension GossipSyncManager {
func _hasAnnouncement(for peerID: PeerID) -> Bool { func _hasAnnouncement(for peerID: PeerID) -> Bool {
queue.sync { queue.sync {
latestAnnouncementByPeer[peerID.id.lowercased()] != nil latestAnnouncementByPeer[peerID] != nil
} }
} }
func _messageCount(for peerID: PeerID) -> Int { func _messageCount(for peerID: PeerID) -> Int {
queue.sync { queue.sync {
messages.values.filter { $0.senderID.hexEncodedString().lowercased() == peerID.id.lowercased() }.count messages.values.filter { PeerID(hexData: $0.senderID) == peerID }.count
} }
} }
} }
+18 -24
View File
@@ -1155,7 +1155,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification( NotificationService.shared.sendPrivateMessageNotification(
from: senderName, from: senderName,
message: pm.content, message: pm.content,
peerID: convKey.id peerID: convKey
) )
} }
} }
@@ -1876,7 +1876,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification( NotificationService.shared.sendPrivateMessageNotification(
from: senderName, from: senderName,
message: pm.content, message: pm.content,
peerID: convKey.id peerID: convKey
) )
} }
@@ -1956,8 +1956,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
func isSelfSender(peerID: PeerID?, displayName: String?) -> Bool { func isSelfSender(peerID: PeerID?, displayName: String?) -> Bool {
guard let peerID else { return false } guard let peerID else { return false }
if peerID == meshService.myPeerID { return true } if peerID == meshService.myPeerID { return true }
let lowerPeer = peerID.id.lowercased() guard peerID.isGeoDM || peerID.isGeoChat else { return false }
guard lowerPeer.hasPrefix("nostr") else { return false }
if let mapped = nostrKeyMapping[peerID]?.lowercased(), if let mapped = nostrKeyMapping[peerID]?.lowercased(),
let gh = currentGeohash, let gh = currentGeohash,
@@ -1967,10 +1966,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let gh = currentGeohash, if let gh = currentGeohash,
let myIdentity = try? idBridge.deriveIdentity(forGeohash: gh) { let myIdentity = try? idBridge.deriveIdentity(forGeohash: gh) {
let myLower = myIdentity.publicKeyHex.lowercased() if peerID == PeerID(nostr: myIdentity.publicKeyHex) { return true }
let shortLen = TransportConfig.nostrShortKeyDisplayLength
let shortKey = "nostr:" + myLower.prefix(shortLen)
if lowerPeer == shortKey { return true }
let suffix = myIdentity.publicKeyHex.suffix(4) let suffix = myIdentity.publicKeyHex.suffix(4)
let expected = (nickname + "#" + suffix).lowercased() let expected = (nickname + "#" + suffix).lowercased()
if let display = displayName?.lowercased(), display == expected { return true } if let display = displayName?.lowercased(), display == expected { return true }
@@ -2453,7 +2449,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
let targetPeer = selectedPrivateChatPeer let targetPeer = selectedPrivateChatPeer
let message = enqueueMediaMessage(content: "[voice] \(url.lastPathComponent)", targetPeer: targetPeer?.id) let message = enqueueMediaMessage(content: "[voice] \(url.lastPathComponent)", targetPeer: targetPeer)
let messageID = message.id let messageID = message.id
let transferId = makeTransferID(messageID: messageID) let transferId = makeTransferID(messageID: messageID)
@@ -2532,7 +2528,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
) )
guard packet.encode() != nil else { throw MediaSendError.encodingFailed } guard packet.encode() != nil else { throw MediaSendError.encodingFailed }
await MainActor.run { await MainActor.run {
let message = self.enqueueMediaMessage(content: "[image] \(outputURL.lastPathComponent)", targetPeer: targetPeer?.id) let message = self.enqueueMediaMessage(content: "[image] \(outputURL.lastPathComponent)", targetPeer: targetPeer)
let messageID = message.id let messageID = message.id
let transferId = self.makeTransferID(messageID: messageID) let transferId = self.makeTransferID(messageID: messageID)
self.registerTransfer(transferId: transferId, messageID: messageID) self.registerTransfer(transferId: transferId, messageID: messageID)
@@ -2574,7 +2570,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
@MainActor @MainActor
private func enqueueMediaMessage(content: String, targetPeer: String?) -> BitchatMessage { private func enqueueMediaMessage(content: String, targetPeer: PeerID?) -> BitchatMessage {
let timestamp = Date() let timestamp = Date()
let message: BitchatMessage let message: BitchatMessage
@@ -2591,7 +2587,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
deliveryStatus: .sending deliveryStatus: .sending
) )
var chats = privateChats var chats = privateChats
chats[PeerID(str: peerID), default: []].append(message) chats[peerID, default: []].append(message)
privateChats = chats privateChats = chats
trimMessagesIfNeeded() trimMessagesIfNeeded()
} else { } else {
@@ -2604,7 +2600,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
originalSender: nil, originalSender: nil,
isPrivate: false, isPrivate: false,
recipientNickname: nil, recipientNickname: nil,
senderPeerID: PeerID(str: senderPeerID), senderPeerID: senderPeerID,
deliveryStatus: .sending deliveryStatus: .sending
) )
messages.append(message) messages.append(message)
@@ -2629,29 +2625,28 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
return message return message
} }
private func currentPublicSender() -> (name: String, peerID: String) { private func currentPublicSender() -> (name: String, peerID: PeerID) {
var displaySender = nickname var displaySender = nickname
var senderPeerID = meshService.myPeerID var senderPeerID = meshService.myPeerID
if case .location(let ch) = activeChannel, if case .location(let ch) = activeChannel,
let identity = try? idBridge.deriveIdentity(forGeohash: ch.geohash) { let identity = try? idBridge.deriveIdentity(forGeohash: ch.geohash) {
let suffix = String(identity.publicKeyHex.suffix(4)) let suffix = String(identity.publicKeyHex.suffix(4))
displaySender = nickname + "#" + suffix displaySender = nickname + "#" + suffix
let shortKey = identity.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength) senderPeerID = PeerID(nostr: identity.publicKeyHex)
senderPeerID = PeerID(str: "nostr:\(shortKey)")
} }
return (displaySender, senderPeerID.id) return (displaySender, senderPeerID)
} }
@MainActor @MainActor
private func nicknameForPeer(_ peerID: String) -> String { private func nicknameForPeer(_ peerID: PeerID) -> String {
if let name = meshService.peerNickname(peerID: PeerID(str: peerID)) { if let name = meshService.peerNickname(peerID: peerID) {
return name return name
} }
if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: PeerID(str: peerID)), if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
!favorite.peerNickname.isEmpty { !favorite.peerNickname.isEmpty {
return favorite.peerNickname return favorite.peerNickname
} }
if let noiseKey = Data(hexString: peerID), if let noiseKey = Data(hexString: peerID.id),
let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
!favorite.peerNickname.isEmpty { !favorite.peerNickname.isEmpty {
return favorite.peerNickname return favorite.peerNickname
@@ -4881,7 +4876,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in
DispatchQueue.main.async { DispatchQueue.main.async {
guard let self = self else { return } guard let self = self else { return }
let peerID = PeerID(str: peerID)
SecureLogger.debug("🔐 Authenticated: \(peerID)", category: .security) SecureLogger.debug("🔐 Authenticated: \(peerID)", category: .security)
@@ -5874,7 +5868,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification( NotificationService.shared.sendPrivateMessageNotification(
from: senderNickname, from: senderNickname,
message: messageContent, message: messageContent,
peerID: targetPeerID.id peerID: targetPeerID
) )
} }
} }
@@ -6267,7 +6261,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification( NotificationService.shared.sendPrivateMessageNotification(
from: message.sender, from: message.sender,
message: message.content, message: message.content,
peerID: peerID.id peerID: peerID
) )
} }
} else { } else {
+7 -7
View File
@@ -47,7 +47,7 @@ struct ContentView: View {
@State private var commandSuggestions: [String] = [] @State private var commandSuggestions: [String] = []
@State private var showMessageActions = false @State private var showMessageActions = false
@State private var selectedMessageSender: String? @State private var selectedMessageSender: String?
@State private var selectedMessageSenderID: String? @State private var selectedMessageSenderID: PeerID?
@FocusState private var isNicknameFieldFocused: Bool @FocusState private var isNicknameFieldFocused: Bool
@State private var isAtBottomPublic: Bool = true @State private var isAtBottomPublic: Bool = true
@State private var isAtBottomPrivate: Bool = true @State private var isAtBottomPrivate: Bool = true
@@ -285,12 +285,12 @@ struct ContentView: View {
Button("content.actions.direct_message") { Button("content.actions.direct_message") {
if let peerID = selectedMessageSenderID { if let peerID = selectedMessageSenderID {
if peerID.hasPrefix("nostr:") { if peerID.isGeoChat {
if let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)) { if let full = viewModel.fullNostrHex(forSenderPeerID: peerID) {
viewModel.startGeohashDM(withPubkeyHex: full) viewModel.startGeohashDM(withPubkeyHex: full)
} }
} else { } else {
viewModel.startPrivateChat(with: PeerID(str: peerID)) viewModel.startPrivateChat(with: peerID)
} }
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar = true showSidebar = true
@@ -312,8 +312,8 @@ struct ContentView: View {
Button("content.actions.block", role: .destructive) { Button("content.actions.block", role: .destructive) {
// Prefer direct geohash block when we have a Nostr sender ID // Prefer direct geohash block when we have a Nostr sender ID
if let peerID = selectedMessageSenderID, peerID.hasPrefix("nostr:"), if let peerID = selectedMessageSenderID, peerID.isGeoChat,
let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)), let full = viewModel.fullNostrHex(forSenderPeerID: peerID),
let sender = selectedMessageSender { let sender = selectedMessageSender {
viewModel.blockGeohashUser(pubkeyHexLowercased: full, displayName: sender) viewModel.blockGeohashUser(pubkeyHexLowercased: full, displayName: sender)
} else if let sender = selectedMessageSender { } else if let sender = selectedMessageSender {
@@ -794,7 +794,7 @@ struct ContentView: View {
case "user": case "user":
let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let peerID = PeerID(str: id.removingPercentEncoding ?? id) let peerID = PeerID(str: id.removingPercentEncoding ?? id)
selectedMessageSenderID = peerID.id selectedMessageSenderID = peerID
if peerID.isGeoDM || peerID.isGeoChat { if peerID.isGeoDM || peerID.isGeoChat {
selectedMessageSender = viewModel.geohashDisplayName(for: peerID) selectedMessageSender = viewModel.geohashDisplayName(for: peerID)