mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +00:00
Add block/unblock user commands
Implemented user blocking functionality: - /block <nickname> - blocks a user from private messaging and ignores their messages in channels - /block - lists all blocked users - /unblock <nickname> - 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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<String> = [] // 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<String> = [] // 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 <nickname>",
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user