From e0ce234d087079538a209a007066a4fb75646fbc Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 8 Jul 2025 02:36:23 +0200 Subject: [PATCH] Add block/unblock user commands Implemented user blocking functionality: - /block - blocks a user from private messaging and ignores their messages in channels - /block - lists all blocked users - /unblock - unblocks a user Implementation details: - Blocks are stored by public key fingerprint for persistence across sessions - Blocked users cannot send private messages or have their public messages displayed - Blocked users are silently ignored (they don't know they're blocked) - Blocking a user automatically removes them from favorites - Added getPeerPublicKey() method to BluetoothMeshService for accessing peer identity keys --- bitchat/Services/BluetoothMeshService.swift | 5 + bitchat/ViewModels/ChatViewModel.swift | 237 ++++++++++++++++++++ 2 files changed, 242 insertions(+) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 09942d2e..9be8c266 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -249,6 +249,11 @@ class BluetoothMeshService: NSObject { return String(fingerprint) } + // Public method to get peer's public key data + func getPeerPublicKey(_ peerID: String) -> Data? { + return encryptionService.getPeerIdentityKey(peerID) + } + override init() { // Generate ephemeral peer ID for each session to prevent tracking // Use random bytes instead of UUID for better anonymity diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 7b465fc5..f5a6f374 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -61,10 +61,12 @@ class ChatViewModel: ObservableObject { // private let channelPasswordsKey = "bitchat.channelPasswords" // Now using Keychain private let channelKeyCommitmentsKey = "bitchat.channelKeyCommitments" private let retentionEnabledChannelsKey = "bitchat.retentionEnabledChannels" + private let blockedUsersKey = "bitchat.blockedUsers" private var nicknameSaveTimer: Timer? @Published var favoritePeers: Set = [] // Now stores public key fingerprints instead of peer IDs private var peerIDToPublicKeyFingerprint: [String: String] = [:] // Maps ephemeral peer IDs to persistent fingerprints + private var blockedUsers: Set = [] // Stores public key fingerprints of blocked users // Messages are naturally ephemeral - no persistent storage @@ -76,6 +78,7 @@ class ChatViewModel: ObservableObject { loadFavorites() loadJoinedChannels() loadChannelData() + loadBlockedUsers() // Load saved channels state savedChannels = MessageRetentionService.shared.getFavoriteChannels() meshService.delegate = self @@ -158,6 +161,17 @@ class ChatViewModel: ObservableObject { userDefaults.synchronize() } + private func loadBlockedUsers() { + if let savedBlockedUsers = userDefaults.stringArray(forKey: blockedUsersKey) { + blockedUsers = Set(savedBlockedUsers) + } + } + + private func saveBlockedUsers() { + userDefaults.set(Array(blockedUsers), forKey: blockedUsersKey) + userDefaults.synchronize() + } + private func loadJoinedChannels() { if let savedChannelsList = userDefaults.stringArray(forKey: joinedChannelsKey) { joinedChannels = Set(savedChannelsList) @@ -794,6 +808,25 @@ class ChatViewModel: ObservableObject { } } + private func isPeerBlocked(_ peerID: String) -> Bool { + // Check if we have the public key fingerprint for this peer + if let fingerprint = peerIDToPublicKeyFingerprint[peerID] { + return blockedUsers.contains(fingerprint) + } + + // Try to get public key from mesh service + if let publicKeyData = meshService.getPeerPublicKey(peerID) { + let fingerprint = SHA256.hash(data: publicKeyData) + .compactMap { String(format: "%02x", $0) } + .joined() + .prefix(16) + .lowercased() + return blockedUsers.contains(String(fingerprint)) + } + + return false + } + func sendMessage(_ content: String) { guard !content.isEmpty else { return } @@ -879,6 +912,18 @@ class ChatViewModel: ObservableObject { guard !content.isEmpty else { return } guard let recipientNickname = meshService.getPeerNicknames()[peerID] else { return } + // Check if the recipient is blocked + if isPeerBlocked(peerID) { + let systemMessage = BitchatMessage( + sender: "system", + content: "cannot send message to \(recipientNickname): user is blocked.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + return + } + // IMPORTANT: When sending a message, it means we're viewing this chat // Send read receipts for any delivered messages from this peer markPrivateMessagesAsRead(from: peerID) @@ -915,6 +960,19 @@ class ChatViewModel: ObservableObject { func startPrivateChat(with peerID: String) { let peerNickname = meshService.getPeerNicknames()[peerID] ?? "unknown" + + // Check if the peer is blocked + if isPeerBlocked(peerID) { + let systemMessage = BitchatMessage( + sender: "system", + content: "cannot start chat with \(peerNickname): user is blocked.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + return + } + selectedPrivateChatPeer = peerID unreadPrivateMessages.remove(peerID) @@ -2132,6 +2190,172 @@ extension ChatViewModel: BitchatDelegate { messages.append(usageMessage) } + case "/block": + if parts.count > 1 { + let targetName = String(parts[1]) + // Remove @ if present + let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName + + // Find peer ID for this nickname + if let peerID = getPeerIDForNickname(nickname) { + // Get public key fingerprint for persistent blocking + if let publicKeyData = meshService.getPeerPublicKey(peerID) { + let fingerprint = SHA256.hash(data: publicKeyData) + .compactMap { String(format: "%02x", $0) } + .joined() + .prefix(16) + .lowercased() + let fingerprintStr = String(fingerprint) + + if blockedUsers.contains(fingerprintStr) { + let systemMessage = BitchatMessage( + sender: "system", + content: "\(nickname) is already blocked.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } else { + blockedUsers.insert(fingerprintStr) + saveBlockedUsers() + + // Remove from favorites if blocked + if favoritePeers.contains(fingerprintStr) { + favoritePeers.remove(fingerprintStr) + saveFavorites() + } + + let systemMessage = BitchatMessage( + sender: "system", + content: "blocked \(nickname). you will no longer receive messages from them.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } else { + let systemMessage = BitchatMessage( + sender: "system", + content: "cannot block \(nickname): unable to verify identity.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } else { + let systemMessage = BitchatMessage( + sender: "system", + content: "cannot block \(nickname): user not found.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } else { + // List blocked users + if blockedUsers.isEmpty { + let systemMessage = BitchatMessage( + sender: "system", + content: "no blocked users.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } else { + // Find nicknames for blocked users + var blockedNicknames: [String] = [] + for (peerID, _) in meshService.getPeerNicknames() { + if let publicKeyData = meshService.getPeerPublicKey(peerID) { + let fingerprint = SHA256.hash(data: publicKeyData) + .compactMap { String(format: "%02x", $0) } + .joined() + .prefix(16) + .lowercased() + let fingerprintStr = String(fingerprint) + if blockedUsers.contains(fingerprintStr) { + if let nickname = meshService.getPeerNicknames()[peerID] { + blockedNicknames.append(nickname) + } + } + } + } + + let blockedList = blockedNicknames.isEmpty ? "blocked users (not currently online)" : blockedNicknames.sorted().joined(separator: ", ") + let systemMessage = BitchatMessage( + sender: "system", + content: "blocked users: \(blockedList)", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } + + case "/unblock": + if parts.count > 1 { + let targetName = String(parts[1]) + // Remove @ if present + let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName + + // Find peer ID for this nickname + if let peerID = getPeerIDForNickname(nickname) { + // Get public key fingerprint + if let publicKeyData = meshService.getPeerPublicKey(peerID) { + let fingerprint = SHA256.hash(data: publicKeyData) + .compactMap { String(format: "%02x", $0) } + .joined() + .prefix(16) + .lowercased() + let fingerprintStr = String(fingerprint) + + if blockedUsers.contains(fingerprintStr) { + blockedUsers.remove(fingerprintStr) + saveBlockedUsers() + + let systemMessage = BitchatMessage( + sender: "system", + content: "unblocked \(nickname).", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } else { + let systemMessage = BitchatMessage( + sender: "system", + content: "\(nickname) is not blocked.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } else { + let systemMessage = BitchatMessage( + sender: "system", + content: "cannot unblock \(nickname): unable to verify identity.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } else { + let systemMessage = BitchatMessage( + sender: "system", + content: "cannot unblock \(nickname): user not found.", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + } else { + let systemMessage = BitchatMessage( + sender: "system", + content: "usage: /unblock ", + timestamp: Date(), + isRelay: false + ) + messages.append(systemMessage) + } + default: // Unknown command let systemMessage = BitchatMessage( @@ -2146,6 +2370,19 @@ extension ChatViewModel: BitchatDelegate { func didReceiveMessage(_ message: BitchatMessage) { + // Check if sender is blocked (for both private and public messages) + if let senderPeerID = message.senderPeerID { + if isPeerBlocked(senderPeerID) { + // Silently ignore messages from blocked users + return + } + } else if let peerID = getPeerIDForNickname(message.sender) { + if isPeerBlocked(peerID) { + // Silently ignore messages from blocked users + return + } + } + if message.isPrivate { // Handle private message