Refactor/robustness (#446)

* Refactor BitChat for improved robustness and performance

Major refactoring to simplify architecture and fix critical issues:

Architecture Improvements:
- Replace complex BluetoothMeshService with simplified SimplifiedBluetoothService
- Consolidate message routing and peer management into unified services
- Remove redundant caching layers and optimize performance

Bug Fixes:
- Fix critical BLE peer mapping corruption in mesh networks
- Fix encrypted message routing failures in multi-peer scenarios
- Fix app freezes and Main Thread Checker warnings
- Fix BLE message delivery in dual-role connections
- Fix favorite toggle UI not updating instantly
- Fix Nostr offline messaging with 24-hour message filtering

Features:
- Add command processor for chat commands
- Add autocomplete service for mentions and commands
- Improve private chat management with dedicated service
- Add unified peer service for consistent state management

Performance:
- Optimize BLE reconnection speed
- Reduce excessive logging throughout codebase
- Improve message deduplication efficiency
- Optimize UI updates and state management

* Improvements refactor robust (#441)

* remove unused code

* remove more

* TLV for announcement

* restore

* restore

* restore?

* messages tlv too (#442)

* Fix Nostr notification and read receipt issues, add TLV encoding, cleanup unused code

## Notification & Read Receipt Fixes
- Fixed toolbar notification icon appearing incorrectly on app restart for already-read messages
- Fixed read receipts being incorrectly deleted on startup when privateChats was empty
- Fixed messages not being marked as read when opening chat for first time
- Fixed senderPeerID not being updated during message consolidation
- Added startup phase logic to block old messages (>30s) while allowing recent ones
- Fixed unread status checking across all storage locations (ephemeral, stable Noise keys, temporary Nostr IDs)

## TLV Encoding Implementation
- Implemented Type-Length-Value encoding for private message payloads
- Added PrivateMessagePacket struct with TLV encode/decode methods
- Enhanced message structure for better extensibility and robustness

## Code Cleanup
- Removed unused PeerStateManager class and related dependencies
- Removed dead protocol types (DeliveryAck, ProtocolAck/Nack, NoiseIdentityAnnouncement)
- Cleaned up BitchatDelegate by removing unused methods
- Removed excessive debug logging throughout ChatViewModel
- Added test-only ProtocolNack helper for integration tests

## Technical Details
- Messages stored under three ID types: ephemeral peer IDs, stable Noise key hexes, temporary Nostr IDs
- Fixed cleanupOldReadReceipts() to skip when privateChats is empty or during startup
- Updated message consolidation to properly update senderPeerID
- Restored NIP-17 timestamp randomization (±15 minutes) for privacy

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
jack
2025-08-17 01:51:54 +02:00
committed by GitHub
co-authored by jack callebtc
parent 47b0829685
commit 845ffc601b
38 changed files with 6423 additions and 12246 deletions
+10
View File
@@ -140,6 +140,16 @@ struct NostrIdentityBridge {
}
return pubkey
}
/// Clear all Nostr identity associations and current identity
static func clearAllAssociations() {
// Delete current Nostr identity
KeychainHelper.delete(key: currentIdentityKey, service: keychainService)
// Note: We can't efficiently delete all noise-nostr associations
// without tracking them, but they'll be orphaned and eventually cleaned up
// The important part is deleting the current identity so a new one is generated
}
}
// Bech32 encoding for Nostr (minimal implementation)
+6 -4
View File
@@ -59,10 +59,11 @@ struct NostrProtocol {
}
/// Decrypt a received NIP-17 message
/// Returns the content, sender pubkey, and the actual message timestamp (not the randomized gift wrap timestamp)
static func decryptPrivateMessage(
giftWrap: NostrEvent,
recipientIdentity: NostrIdentity
) throws -> (content: String, senderPubkey: String) {
) throws -> (content: String, senderPubkey: String, timestamp: Int) {
// Starting decryption
@@ -94,7 +95,7 @@ struct NostrProtocol {
throw error
}
return (content: rumor.content, senderPubkey: rumor.pubkey)
return (content: rumor.content, senderPubkey: rumor.pubkey, timestamp: rumor.created_at)
}
// MARK: - Private Methods
@@ -438,8 +439,9 @@ struct NostrProtocol {
private static func randomizedTimestamp() -> Date {
// Add random offset to current time for privacy
// TEMPORARY: Reduced range to debug timestamp issue
let offset = TimeInterval.random(in: -60...60) // +/- 1 minute (was +/- 15 minutes)
// This prevents timing correlation attacks while the actual message timestamp
// is preserved in the encrypted rumor
let offset = TimeInterval.random(in: -900...900) // +/- 15 minutes
let now = Date()
let randomized = now.addingTimeInterval(offset)
+24 -9
View File
@@ -57,6 +57,7 @@ class NostrRelayManager: ObservableObject {
/// Connect to all configured relays
func connect() {
SecureLogger.log("🌐 Connecting to \(relays.count) Nostr relays", category: SecureLogger.session, level: .debug)
for relay in relays {
connectToRelay(relay.url)
}
@@ -96,6 +97,9 @@ class NostrRelayManager: ObservableObject {
) {
messageHandlers[id] = handler
SecureLogger.log("📡 Setting up subscription '\(id)' with filter - kinds: \(filter.kinds ?? []), since: \(filter.since ?? 0)",
category: SecureLogger.session, level: .debug)
let req = NostrRequest.subscribe(id: id, filters: [filter])
do {
@@ -107,9 +111,8 @@ class NostrRelayManager: ObservableObject {
return
}
// Sending subscription to relays
// Filter JSON prepared
// Full filter JSON logged
// SecureLogger.log("📋 Subscription filter JSON: \(messageString.prefix(200))...",
// category: SecureLogger.session, level: .debug)
// Send subscription to all connected relays
for (relayUrl, connection) in connections {
@@ -118,6 +121,8 @@ class NostrRelayManager: ObservableObject {
SecureLogger.log("❌ Failed to send subscription to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error)
} else {
// SecureLogger.log(" Subscription '\(id)' sent to relay: \(relayUrl)",
// category: SecureLogger.session, level: .debug)
// Subscription sent successfully
Task { @MainActor in
var subs = self.subscriptions[relayUrl] ?? Set<String>()
@@ -188,10 +193,11 @@ class NostrRelayManager: ObservableObject {
task.sendPing { [weak self] error in
DispatchQueue.main.async {
if error == nil {
// Successfully connected to Nostr relay
SecureLogger.log("✅ Connected to Nostr relay: \(urlString)",
category: SecureLogger.session, level: .debug)
self?.updateRelayStatus(urlString, isConnected: true)
} else {
SecureLogger.log("Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")",
SecureLogger.log("Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")",
category: SecureLogger.session, level: .error)
self?.updateRelayStatus(urlString, isConnected: false, error: error)
// Trigger disconnection handler for proper backoff
@@ -254,7 +260,11 @@ class NostrRelayManager: ObservableObject {
let event = try NostrEvent(from: eventDict)
// Processing event
// Only log critical events
if event.kind != 1059 { // Don't log every gift wrap
SecureLogger.log("📥 Received Nostr event (kind: \(event.kind)) from relay: \(relayUrl)",
category: SecureLogger.session, level: .debug)
}
DispatchQueue.main.async {
// Update relay stats
@@ -282,7 +292,10 @@ class NostrRelayManager: ObservableObject {
let eventId = array[1] as? String,
let success = array[2] as? Bool {
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
if !success {
if success {
SecureLogger.log("✅ Event \(eventId.prefix(16))... accepted by relay: \(relayUrl)",
category: SecureLogger.session, level: .debug)
} else {
SecureLogger.log("📮 Event \(eventId) rejected by relay: \(reason)",
category: SecureLogger.session, level: .error)
}
@@ -311,8 +324,8 @@ class NostrRelayManager: ObservableObject {
let data = try encoder.encode(req)
let message = String(data: data, encoding: .utf8) ?? ""
// Sending event to relay
// Event JSON prepared
SecureLogger.log("📤 Sending Nostr event (kind: \(event.kind)) to relay: \(relayUrl)",
category: SecureLogger.session, level: .debug)
connection.send(.string(message)) { [weak self] error in
DispatchQueue.main.async {
@@ -320,6 +333,8 @@ class NostrRelayManager: ObservableObject {
SecureLogger.log("❌ Failed to send event to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error)
} else {
// SecureLogger.log(" Event sent to relay: \(relayUrl)",
// category: SecureLogger.session, level: .debug)
// Update relay stats
if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) {
self?.relays[index].messagesSent += 1