mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 07:25:18 +00:00
Merge pull request #29 from jackjackbits/feature/block-users
Add block/unblock commands
This commit is contained in:
@@ -63,12 +63,15 @@ This project is released into the public domain. See the [LICENSE](LICENSE) file
|
|||||||
### Basic Commands
|
### Basic Commands
|
||||||
|
|
||||||
- `/j #channel` - Join or create a channel
|
- `/j #channel` - Join or create a channel
|
||||||
- `/m @user message` - Send a private message
|
- `/m @name message` - Send a private message
|
||||||
- `/w` - List online users
|
- `/w` - List online users
|
||||||
- `/channels` - Show all discovered channels
|
- `/channels` - Show all discovered channels
|
||||||
|
- `/block @name` - Block a peer from messaging you
|
||||||
|
- `/block` - List all blocked peers
|
||||||
|
- `/unblock @name` - Unblock a peer
|
||||||
- `/clear` - Clear chat messages
|
- `/clear` - Clear chat messages
|
||||||
- `/pass [password]` - Set/change channel password (owner only)
|
- `/pass [password]` - Set/change channel password (owner only)
|
||||||
- `/transfer @user` - Transfer channel ownership
|
- `/transfer @name` - Transfer channel ownership
|
||||||
- `/save` - Toggle message retention for channel (owner only)
|
- `/save` - Toggle message retention for channel (owner only)
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|||||||
@@ -249,6 +249,11 @@ class BluetoothMeshService: NSObject {
|
|||||||
return String(fingerprint)
|
return String(fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public method to get peer's public key data
|
||||||
|
func getPeerPublicKey(_ peerID: String) -> Data? {
|
||||||
|
return encryptionService.getPeerIdentityKey(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
// Generate ephemeral peer ID for each session to prevent tracking
|
// Generate ephemeral peer ID for each session to prevent tracking
|
||||||
// Use random bytes instead of UUID for better anonymity
|
// 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 channelPasswordsKey = "bitchat.channelPasswords" // Now using Keychain
|
||||||
private let channelKeyCommitmentsKey = "bitchat.channelKeyCommitments"
|
private let channelKeyCommitmentsKey = "bitchat.channelKeyCommitments"
|
||||||
private let retentionEnabledChannelsKey = "bitchat.retentionEnabledChannels"
|
private let retentionEnabledChannelsKey = "bitchat.retentionEnabledChannels"
|
||||||
|
private let blockedUsersKey = "bitchat.blockedUsers"
|
||||||
private var nicknameSaveTimer: Timer?
|
private var nicknameSaveTimer: Timer?
|
||||||
|
|
||||||
@Published var favoritePeers: Set<String> = [] // Now stores public key fingerprints instead of peer IDs
|
@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 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
|
// Messages are naturally ephemeral - no persistent storage
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ class ChatViewModel: ObservableObject {
|
|||||||
loadFavorites()
|
loadFavorites()
|
||||||
loadJoinedChannels()
|
loadJoinedChannels()
|
||||||
loadChannelData()
|
loadChannelData()
|
||||||
|
loadBlockedUsers()
|
||||||
// Load saved channels state
|
// Load saved channels state
|
||||||
savedChannels = MessageRetentionService.shared.getFavoriteChannels()
|
savedChannels = MessageRetentionService.shared.getFavoriteChannels()
|
||||||
meshService.delegate = self
|
meshService.delegate = self
|
||||||
@@ -158,6 +161,17 @@ class ChatViewModel: ObservableObject {
|
|||||||
userDefaults.synchronize()
|
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() {
|
private func loadJoinedChannels() {
|
||||||
if let savedChannelsList = userDefaults.stringArray(forKey: joinedChannelsKey) {
|
if let savedChannelsList = userDefaults.stringArray(forKey: joinedChannelsKey) {
|
||||||
joinedChannels = Set(savedChannelsList)
|
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) {
|
func sendMessage(_ content: String) {
|
||||||
guard !content.isEmpty else { return }
|
guard !content.isEmpty else { return }
|
||||||
|
|
||||||
@@ -879,6 +912,18 @@ class ChatViewModel: ObservableObject {
|
|||||||
guard !content.isEmpty else { return }
|
guard !content.isEmpty else { return }
|
||||||
guard let recipientNickname = meshService.getPeerNicknames()[peerID] 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
|
// IMPORTANT: When sending a message, it means we're viewing this chat
|
||||||
// Send read receipts for any delivered messages from this peer
|
// Send read receipts for any delivered messages from this peer
|
||||||
markPrivateMessagesAsRead(from: peerID)
|
markPrivateMessagesAsRead(from: peerID)
|
||||||
@@ -915,6 +960,19 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
func startPrivateChat(with peerID: String) {
|
func startPrivateChat(with peerID: String) {
|
||||||
let peerNickname = meshService.getPeerNicknames()[peerID] ?? "unknown"
|
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
|
selectedPrivateChatPeer = peerID
|
||||||
unreadPrivateMessages.remove(peerID)
|
unreadPrivateMessages.remove(peerID)
|
||||||
|
|
||||||
@@ -2132,6 +2190,172 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
messages.append(usageMessage)
|
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 peers.",
|
||||||
|
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 peers (not currently online)" : blockedNicknames.sorted().joined(separator: ", ")
|
||||||
|
let systemMessage = BitchatMessage(
|
||||||
|
sender: "system",
|
||||||
|
content: "blocked peers: \(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:
|
default:
|
||||||
// Unknown command
|
// Unknown command
|
||||||
let systemMessage = BitchatMessage(
|
let systemMessage = BitchatMessage(
|
||||||
@@ -2146,6 +2370,19 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
|
|
||||||
func didReceiveMessage(_ message: BitchatMessage) {
|
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 {
|
if message.isPrivate {
|
||||||
// Handle private message
|
// Handle private message
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,26 @@ struct AppInfoView: View {
|
|||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Commands")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("/j #channel - join or create a channel")
|
||||||
|
Text("/m @name - send private message")
|
||||||
|
Text("/w - see who's online")
|
||||||
|
Text("/channels - show all discovered channels")
|
||||||
|
Text("/block @name - block a peer")
|
||||||
|
Text("/block - list blocked peers")
|
||||||
|
Text("/unblock @name - unblock a peer")
|
||||||
|
Text("/clear - clear current chat")
|
||||||
|
Text("/hug @name - send someone a hug")
|
||||||
|
Text("/slap @name - slap with a trout")
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
// Technical Details
|
// Technical Details
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
SectionHeader("Technical Details")
|
SectionHeader("Technical Details")
|
||||||
@@ -208,6 +228,26 @@ struct AppInfoView: View {
|
|||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Commands")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("/j #channel - join or create a channel")
|
||||||
|
Text("/m @name - send private message")
|
||||||
|
Text("/w - see who's online")
|
||||||
|
Text("/channels - show all discovered channels")
|
||||||
|
Text("/block @name - block a peer")
|
||||||
|
Text("/block - list blocked peers")
|
||||||
|
Text("/unblock @name - unblock a peer")
|
||||||
|
Text("/clear - clear current chat")
|
||||||
|
Text("/hug @name - send someone a hug")
|
||||||
|
Text("/slap @name - slap with a trout")
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
// Technical Details
|
// Technical Details
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
SectionHeader("Technical Details")
|
SectionHeader("Technical Details")
|
||||||
|
|||||||
@@ -519,12 +519,14 @@ struct ContentView: View {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Define commands with aliases and syntax
|
// Define commands with aliases and syntax
|
||||||
let commandInfo: [(commands: [String], syntax: String?, description: String)] = [
|
let commandInfo: [(commands: [String], syntax: String?, description: String)] = [
|
||||||
|
(["/block"], "[nickname]", "block or list blocked peers"),
|
||||||
(["/clear"], nil, "clear chat messages"),
|
(["/clear"], nil, "clear chat messages"),
|
||||||
(["/hug"], "<nickname>", "send someone a warm hug"),
|
(["/hug"], "<nickname>", "send someone a warm hug"),
|
||||||
(["/j", "/join"], "<channel>", "join or create a channel"),
|
(["/j", "/join"], "<channel>", "join or create a channel"),
|
||||||
(["/m", "/msg"], "<nickname> [message]", "send private message"),
|
(["/m", "/msg"], "<nickname> [message]", "send private message"),
|
||||||
(["/channels"], nil, "show all discovered channels"),
|
(["/channels"], nil, "show all discovered channels"),
|
||||||
(["/slap"], "<nickname>", "slap someone with a trout"),
|
(["/slap"], "<nickname>", "slap someone with a trout"),
|
||||||
|
(["/unblock"], "<nickname>", "unblock a peer"),
|
||||||
(["/w"], nil, "see who's online")
|
(["/w"], nil, "see who's online")
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -625,12 +627,14 @@ struct ContentView: View {
|
|||||||
if newValue.hasPrefix("/") && newValue.count >= 1 {
|
if newValue.hasPrefix("/") && newValue.count >= 1 {
|
||||||
// Build context-aware command list
|
// Build context-aware command list
|
||||||
var commandDescriptions = [
|
var commandDescriptions = [
|
||||||
|
("/block", "block or list blocked peers"),
|
||||||
|
("/channels", "show all discovered channels"),
|
||||||
("/clear", "clear chat messages"),
|
("/clear", "clear chat messages"),
|
||||||
("/hug", "send someone a warm hug"),
|
("/hug", "send someone a warm hug"),
|
||||||
("/j", "join or create a channel"),
|
("/j", "join or create a channel"),
|
||||||
("/m", "send private message"),
|
("/m", "send private message"),
|
||||||
("/channels", "show all discovered channels"),
|
|
||||||
("/slap", "slap someone with a trout"),
|
("/slap", "slap someone with a trout"),
|
||||||
|
("/unblock", "unblock a peer"),
|
||||||
("/w", "see who's online")
|
("/w", "see who's online")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user