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
+3
View File
@@ -5,6 +5,9 @@
## implementation plans ## implementation plans
plans/ plans/
## AI
CLAUDE.md
## User settings ## User settings
xcuserdata/ xcuserdata/
+5 -5
View File
@@ -47,14 +47,14 @@ BitChat is a decentralized, peer-to-peer messaging application that works over B
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ Bluetooth Transport Layer │ │ Bluetooth Transport Layer │
(BluetoothMeshService) │ (SimplifiedBluetoothService) │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
``` ```
## Core Components ## Core Components
### 1. BluetoothMeshService (Transport Layer) ### 1. SimplifiedBluetoothService (Transport Layer)
- **Location**: `bitchat/Services/BluetoothMeshService.swift` - **Location**: `bitchat/Services/SimplifiedBluetoothService.swift`
- **Purpose**: Manages BLE connections and implements mesh networking - **Purpose**: Manages BLE connections and implements mesh networking
- **Key Responsibilities**: - **Key Responsibilities**:
- Peer discovery (scanning and advertising simultaneously) - Peer discovery (scanning and advertising simultaneously)
@@ -159,7 +159,7 @@ BitChat is a decentralized, peer-to-peer messaging application that works over B
### Services (`/bitchat/Services/`) ### Services (`/bitchat/Services/`)
Application-level services that coordinate between layers: Application-level services that coordinate between layers:
- `BluetoothMeshService`: Core networking - `SimplifiedBluetoothService`: Core networking
- `NoiseEncryptionService`: Encryption coordination - `NoiseEncryptionService`: Encryption coordination
- `MessageRetryService`: Reliability layer - `MessageRetryService`: Reliability layer
- `DeliveryTracker`: Acknowledgment handling - `DeliveryTracker`: Acknowledgment handling
@@ -368,7 +368,7 @@ Structured content for different message types:
5. Update help text 5. Update help text
### Debugging Bluetooth Issues ### Debugging Bluetooth Issues
1. Check `BluetoothMeshService` logs 1. Check `SimplifiedBluetoothService` logs
2. Verify peer states and connections 2. Verify peer states and connections
3. Monitor characteristic updates 3. Monitor characteristic updates
4. Use Bluetooth debugging tools 4. Use Bluetooth debugging tools
+204
View File
@@ -0,0 +1,204 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
BitChat is a decentralized peer-to-peer messaging iOS/macOS app that works over Bluetooth mesh networks without requiring internet, servers, or phone numbers. It implements end-to-end encryption using the Noise Protocol Framework and integrates Nostr as a fallback transport for mutual favorites.
**Core Value Proposition**: Side-groupchat for scenarios where traditional communication infrastructure is unavailable or untrusted. Security and reliability are paramount over features.
## Common Development Commands
### Building the Project
```bash
# Option 1: Generate Xcode project with XcodeGen (preferred)
xcodegen generate
open bitchat.xcodeproj
# Option 2: Open with Swift Package Manager
open Package.swift
# Option 3: Quick macOS build and run using Just
just run # Build and run macOS app
just build # Build only
just clean # Clean and restore original files
just dev-run # Quick development build
```
### Testing
```bash
# Run iOS tests
xcodebuild test -project bitchat.xcodeproj -scheme "bitchat (iOS)" -destination "platform=iOS Simulator,name=iPhone 15"
# Run macOS tests
xcodebuild test -project bitchat.xcodeproj -scheme "bitchat (macOS)" -destination "platform=macOS"
# Run specific test file
xcodebuild test -project bitchat.xcodeproj -scheme "bitchat (iOS)" -only-testing:bitchatTests_iOS/NoiseProtocolTests
# Run all tests with verbose output
xcodebuild test -project bitchat.xcodeproj -scheme "bitchat (iOS)" -destination "platform=iOS Simulator,name=iPhone 15" -verbose
```
### Building for Device
```bash
# Build for iOS device
xcodebuild -project bitchat.xcodeproj -scheme "bitchat (iOS)" -configuration Release -destination "generic/platform=iOS" archive
# Build for macOS (unsigned)
xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
```
## High-Level Architecture
BitChat uses a layered architecture with clear separation of concerns:
### Core Architecture Layers
1. **Transport Layer** (`SimplifiedBluetoothService`, `NostrRelayManager`)
- Handles Bluetooth LE mesh networking and Nostr WebSocket connections
- Manages peer discovery, connection lifecycle, and message routing
- Implements automatic transport selection based on peer availability
2. **Protocol Layer** (`BitchatProtocol`, `NostrProtocol`)
- Defines binary message format for efficient BLE transmission
- Implements NIP-17 gift-wrapped messages for Nostr privacy
- Handles message framing, fragmentation, and reassembly
3. **Security Layer** (`NoiseProtocol`, `NoiseEncryptionService`)
- Implements Noise_XX_25519_AESGCM_SHA256 for E2E encryption
- Manages cryptographic handshakes and session keys
- Provides forward secrecy and mutual authentication
4. **Application Services** (`FavoritesPersistenceService`, `NotificationService`, `PeerStateManager`)
- Manages favorites system and mutual trust relationships
- Handles system notifications and peer state
- Transport selection logic integrated in ChatViewModel (lines 653-665)
5. **UI Layer** (`ChatViewModel`, `ContentView`)
- MVVM architecture with SwiftUI views
- Manages chat state, commands, and user interactions
- Handles private chats and channel management
### Key Design Patterns
- **Transport Abstraction**: Messages automatically route through available transports (Bluetooth when connected, Nostr for mutual favorites when offline)
- **Three-Layer Identity**: Ephemeral, Cryptographic, and Social identities
- **Mutual Favorites**: Nostr transport requires bidirectional trust and is fully functional
- **Binary Protocol**: Optimized for BLE's limited bandwidth (~20KB/s)
- **Efficient Message Deduplication**: Time-based LRU cache prevents loops in mesh network
- **TTL-Based Routing**: Maximum 7 hops to prevent infinite propagation
- **Consolidated Maintenance**: Single timer handles all periodic tasks (announces, cleanup, connectivity checks)
### Critical Files and Their Roles
- `Services/SimplifiedBluetoothService.swift` (~1860 lines): Core BLE mesh implementation
- `Services/FavoritesPersistenceService.swift`: Manages favorite relationships
- `Noise/NoiseProtocol.swift` (~900 lines): Cryptographic protocol implementation
- `Nostr/NostrProtocol.swift`: NIP-17 private message implementation
- `ViewModels/ChatViewModel.swift` (~3100 lines): Central business logic and state
- `Protocols/BitchatProtocol.swift` (~1100 lines): Binary message format definitions
- `Protocols/BinaryProtocol.swift` (~580 lines): Low-level encoding/decoding
## Important Considerations
### Security
- Never log sensitive data (keys, message content)
- Use `SecureLogger` for security-aware logging
- All private messages use end-to-end encryption
- Identity keys stored in iOS Keychain
### Performance
- Bluetooth LE has ~512 byte MTU, messages are fragmented
- Use LZ4 compression for large messages
- Minimize protocol overhead for battery efficiency
- Batch UI updates to prevent performance issues
### Testing Requirements
- Physical devices required (Bluetooth doesn't work in simulator)
- Test with multiple devices for mesh functionality
- Enable Bluetooth permissions in device settings
- Test both transport modes (Bluetooth and Nostr)
### Platform Differences
- iOS: Full background Bluetooth support with proper entitlements
- macOS: Limited background support, may require app in foreground
- Share Extension: iOS only, for sharing content to BitChat
### Known Quirks
- XcodeGen may require temporary file moves for macOS builds (handled by Justfile)
- LaunchScreen.storyboard is iOS-only, needs hiding for macOS builds
- Nostr keys derived from Noise keys using BIP-32 path m/44'/1237'/0'/0/0
## Debugging Tips
### Bluetooth Issues
- Check `SimplifiedBluetoothService` state and peer connections
- Verify app has Bluetooth permissions enabled
- Ensure devices are in range (<50 meters typical)
- Monitor characteristic notifications and MTU
### Nostr Transport (Fully Functional)
- Verify mutual favorite status in `FavoritesPersistenceService`
- Check relay connections in `NostrRelayManager` logs
- Test gift wrap encryption/decryption in `NostrProtocol`
- Transport selection happens automatically in `ChatViewModel.sendPrivateMessage()` (lines 653-665)
### Message Delivery
- Check for message processing in `SimplifiedBluetoothService.handleReceivedPacket()`
- Verify handshake state for private messages in `NoiseEncryptionService`
- Efficient deduplication uses `MessageDeduplicator` class with time-based cleanup
- Monitor TTL values in mesh propagation (max 7 hops)
## Common Tasks
### Adding New Message Types
1. Define in `MessageType` enum in `BitchatProtocol.swift`
2. Implement encoding/decoding in `BinaryProtocol.swift`
3. Add handling in `ChatViewModel.processMessage()`
4. Update UI components if needed
5. Add unit tests for encoding/decoding in `bitchatTests/Protocol/`
### Implementing New Commands
1. Add to `ChatViewModel.processCommand()` (~line 2800)
2. Define required message types if needed
3. Implement command logic and validation
4. Add to autocomplete suggestions in `getCommandSuggestions()`
5. Update help text in `/help` command implementation
### Modifying Transports
1. Update transport-specific protocol implementations
2. Test failover between transports (automatic in ChatViewModel)
3. Verify message delivery in both modes
4. Nostr integration is complete and functional for mutual favorites
### Running Code Quality Checks
```bash
# SwiftLint (if configured)
swiftlint lint --path bitchat/
# Swift format check (if using swift-format)
swift-format lint -r bitchat/
# Build with strict warnings
xcodebuild -project bitchat.xcodeproj -scheme "bitchat (iOS)" -configuration Debug SWIFT_TREAT_WARNINGS_AS_ERRORS=YES build
```
## Recent Optimizations
### Performance Improvements
- **Message Deduplication**: Replaced O(n) Set recreation with efficient time-based LRU cache
- **Timer Consolidation**: Single maintenance timer replaces multiple timers, reducing overhead
- **Main Thread Optimization**: Added `notifyUI()` helper to avoid unnecessary dispatches
- **didReceiveMessage Refactoring**: Split 300-line method into focused helper methods
### Architecture Clarifications
- Nostr transport is fully implemented and functional (not partial)
- Transport selection logic is integrated in ChatViewModel, not a separate service
- Peer state management could be further consolidated (3 systems track same data)
Remember: This app is designed for scenarios where traditional communication infrastructure is unavailable or untrusted. Security and reliability are paramount over features.
+277 -307
View File
@@ -7,101 +7,97 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; }; 049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; };
04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; }; 049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; };
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; }; 049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; }; 049BD3942E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; };
04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; }; 049BD3952E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; };
04636BD12E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */; }; 049BD3962E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
04636BD22E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */; }; 049BD3992E506A12001A566B /* UnifiedPeerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3982E506A12001A566B /* UnifiedPeerService.swift */; };
04636BD32E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */; }; 049BD39A2E506A12001A566B /* UnifiedPeerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3982E506A12001A566B /* UnifiedPeerService.swift */; };
04636BD42E30BE5100FBCFA8 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */; }; 0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
04636BD52E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */; }; 0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
04636BD62E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */; };
04636BD72E30BE5100FBCFA8 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */; };
04636BD82E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */; };
04636BD92E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */; };
04636BDA2E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */; };
04636BDB2E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */; };
04636BDC2E30BE5100FBCFA8 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */; };
04636BDD2E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */; };
04636BDE2E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */; };
04636BDF2E30BE5100FBCFA8 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */; };
04636BE02E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */; };
04636BE82E30BEC600FBCFA8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */; };
04636BEB2E30BEC600FBCFA8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */; };
04636BED2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */; };
04636BEE2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */; };
046D705D2E3C105D00C00594 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046D705C2E3C105D00C00594 /* InputValidator.swift */; };
046D705E2E3C105D00C00594 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046D705C2E3C105D00C00594 /* InputValidator.swift */; };
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */; };
04B6BA452E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */; };
04B6BA462E2035530090FE39 /* NoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA432E2035530090FE39 /* NoiseSession.swift */; };
04B6BA472E2035530090FE39 /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA412E2035530090FE39 /* NoiseProtocol.swift */; };
04B6BA492E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */; };
04B6BA4A2E2035530090FE39 /* NoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA432E2035530090FE39 /* NoiseSession.swift */; };
04B6BA4B2E2035530090FE39 /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA412E2035530090FE39 /* NoiseProtocol.swift */; };
04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */; };
04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */; };
04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA532E203D6C0090FE39 /* FingerprintView.swift */; };
04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA532E203D6C0090FE39 /* FingerprintView.swift */; };
04E363362E3800310048E624 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 04F127E72E37EBCD00FFBA8D /* P256K */; };
04E363372E3800310048E624 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 04F127E82E37EBDB00FFBA8D /* P256K */; };
04F127E42E37EBAA00FFBA8D /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127E22E37EBAA00FFBA8D /* NostrIdentity.swift */; };
04F127E52E37EBAA00FFBA8D /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127E22E37EBAA00FFBA8D /* NostrIdentity.swift */; };
04F127F22E37EEB800FFBA8D /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F02E37EEB800FFBA8D /* NostrProtocol.swift */; };
04F127F32E37EEB800FFBA8D /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F12E37EEB800FFBA8D /* NostrRelayManager.swift */; };
04F127F42E37EEB800FFBA8D /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F02E37EEB800FFBA8D /* NostrProtocol.swift */; };
04F127F52E37EEB800FFBA8D /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F12E37EEB800FFBA8D /* NostrRelayManager.swift */; };
04F127F82E37EEEE00FFBA8D /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F72E37EEEE00FFBA8D /* MessageRouter.swift */; };
04F127F92E37EEEE00FFBA8D /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F62E37EEEE00FFBA8D /* FavoritesPersistenceService.swift */; };
04F127FA2E37EEEE00FFBA8D /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F72E37EEEE00FFBA8D /* MessageRouter.swift */; };
04F127FB2E37EEEE00FFBA8D /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127F62E37EEEE00FFBA8D /* FavoritesPersistenceService.swift */; };
04F127FE2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127FC2E37EF3D00FFBA8D /* BitchatPeer.swift */; };
04F127FF2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127FC2E37EF3D00FFBA8D /* BitchatPeer.swift */; };
04F128042E37F00000FFBA8D /* ProcessedMessagesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127FF2E37F00000FFBA8D /* ProcessedMessagesService.swift */; };
04F128052E37F00000FFBA8D /* ProcessedMessagesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F127FF2E37F00000FFBA8D /* ProcessedMessagesService.swift */; };
04F128062E37F10000FFBA8D /* PeerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F128072E37F10000FFBA8D /* PeerSession.swift */; };
04F128082E37F10000FFBA8D /* PeerSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F128072E37F10000FFBA8D /* PeerSession.swift */; };
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
132DF1E24B4E9C7DCDAD4376 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */; };
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */; };
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; }; 2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC75901A0F0073B5BB8356E7 /* TestConstants.swift */; };
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; }; 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; };
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */; };
3849CA6D99B2D536636DF4A6 /* MockSimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */; };
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; 4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
5D95F2BFBE257A1225998389 /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; }; 501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */; };
5C93B4FDD0C448C3EDDBF8AE /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */; };
5EE49E150BBF0488E7473687 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */; };
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; }; 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.swift */; };
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78595178957244CBDF7E79B6 /* NostrRelayManager.swift */; };
6C63FA98D59854C15C57B3D6 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */; };
6C803BF930E7E19BE6E99EAA /* MockSimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */; };
6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.swift */; };
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; 6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
7241FFD6CFFB875B864FA223 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */; };
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; }; 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; }; 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; }; 765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */; };
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; }; 7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; };
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; }; 7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; };
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; }; 7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */; }; 84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11186E29A064E8D210880E1B /* BitchatPeer.swift */; };
84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11186E29A064E8D210880E1B /* BitchatPeer.swift */; };
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */; };
885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; };
8A14ADADF5CD7A79919CB655 /* NoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */; };
8C1AB0F2D48207E0755DA91A /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC75901A0F0073B5BB8356E7 /* TestConstants.swift */; };
8DE687D2EB5EB120868DBFB5 /* SimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */; };
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.swift */; };
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; }; 8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; 923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
92D1CF17DF88EA298F6E5E8E /* NoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */; };
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; 92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */; };
9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318B743C64628A125261163 /* BinaryEncodingUtils.swift */; };
9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B378C16594575FCC7F9C75 /* ShareViewController.swift */; }; 9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B378C16594575FCC7F9C75 /* ShareViewController.swift */; };
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
A2977428C1D9EF9944C4BFAF /* SimplifiedBluetoothServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */; };
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; }; ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
ACE2ED172C37F01561E50B71 /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */; };
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; }; AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318B743C64628A125261163 /* BinaryEncodingUtils.swift */; };
AFF33EF44626EF0579D17EB1 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.swift */; };
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; }; B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; };
B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */; };
B909706CD38FC56C0C8EB7BF /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */; };
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; }; BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
C0A80BA73EC1A372B9338E3C /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; }; BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78595178957244CBDF7E79B6 /* NostrRelayManager.swift */; };
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; }; BE729E149C98F775D9622D9C /* SimplifiedBluetoothServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */; };
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */; }; C165DD35BB8E9C327A3C2DA4 /* SimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */; };
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; }; C3B1226CD30C87501EF6F12F /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8043995007F0D84438EDD9 /* NostrIdentity.swift */; };
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7EFB209C86BBD956B749EC /* SecureLogger.swift */; };
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; }; D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; }; D691938B4029A04CC905FDC8 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */; };
D782AB596DDB5C846554F7C3 /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8043995007F0D84438EDD9 /* NostrIdentity.swift */; };
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; }; E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; };
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7EFB209C86BBD956B749EC /* SecureLogger.swift */; };
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */; };
EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */; };
F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; }; FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -144,169 +140,87 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; }; 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteService.swift; sourceTree = "<group>"; };
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; }; 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatE2ETests.swift; sourceTree = "<group>"; }; 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicChatE2ETests.swift; sourceTree = "<group>"; }; 049BD3982E506A12001A566B /* UnifiedPeerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedPeerService.swift; sourceTree = "<group>"; };
04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBluetoothMeshService.swift; sourceTree = "<group>"; }; 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNoiseSession.swift; sourceTree = "<group>"; }; 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; }; 11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; };
04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseHandshakeCoordinator.swift; sourceTree = "<group>"; };
046D705C2E3C105D00C00594 /* InputValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputValidator.swift; sourceTree = "<group>"; };
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityConsiderations.swift; sourceTree = "<group>"; };
04B6BA432E2035530090FE39 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; };
04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseEncryptionService.swift; sourceTree = "<group>"; };
04B6BA532E203D6C0090FE39 /* FingerprintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintView.swift; sourceTree = "<group>"; };
04F127E22E37EBAA00FFBA8D /* NostrIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrIdentity.swift; sourceTree = "<group>"; };
04F127F02E37EEB800FFBA8D /* NostrProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrProtocol.swift; sourceTree = "<group>"; };
04F127F12E37EEB800FFBA8D /* NostrRelayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRelayManager.swift; sourceTree = "<group>"; };
04F127F62E37EEEE00FFBA8D /* FavoritesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPersistenceService.swift; sourceTree = "<group>"; };
04F127F72E37EEEE00FFBA8D /* MessageRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRouter.swift; sourceTree = "<group>"; };
04F127FC2E37EF3D00FFBA8D /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; };
04F127FF2E37F00000FFBA8D /* ProcessedMessagesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedMessagesService.swift; sourceTree = "<group>"; };
04F128072E37F10000FFBA8D /* PeerSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerSession.swift; sourceTree = "<group>"; };
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = "<group>"; };
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; }; 136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; }; 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; }; 2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrProtocol.swift; sourceTree = "<group>"; };
32F149C43D1915831B60FE09 /* CompressionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionUtil.swift; sourceTree = "<group>"; }; 32F149C43D1915831B60FE09 /* CompressionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionUtil.swift; sourceTree = "<group>"; };
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; }; 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = "<group>"; }; 3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = "<group>"; };
394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseEncryptionService.swift; sourceTree = "<group>"; };
3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPersistenceService.swift; sourceTree = "<group>"; };
43613045E63D21D429396805 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityConsiderations.swift; sourceTree = "<group>"; };
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = "<group>"; }; 527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = "<group>"; };
5318B743C64628A125261163 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrIdentity.swift; sourceTree = "<group>"; };
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6E2446380E7A44E49A35B664 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; }; 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRelayManager.swift; sourceTree = "<group>"; };
8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatE2ETests.swift; sourceTree = "<group>"; };
8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedBluetoothService.swift; sourceTree = "<group>"; };
8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputValidator.swift; sourceTree = "<group>"; };
9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintView.swift; sourceTree = "<group>"; };
95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
96D0D41CA19EE5A772AA8434 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 96D0D41CA19EE5A772AA8434 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedBluetoothServiceTests.swift; sourceTree = "<group>"; };
9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; };
9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; }; 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; };
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = "<group>"; }; B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseHandshakeCoordinator.swift; sourceTree = "<group>"; };
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C1B378C16594575FCC7F9C75 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; }; C1B378C16594575FCC7F9C75 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizedBloomFilter.swift; sourceTree = "<group>"; }; C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrProtocolTests.swift; sourceTree = "<group>"; };
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; }; C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBluetoothMeshService.swift; sourceTree = "<group>"; };
D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicChatE2ETests.swift; sourceTree = "<group>"; };
D69A18D27F9A565FD6041E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; D69A18D27F9A565FD6041E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; }; E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; };
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryOptimizer.swift; sourceTree = "<group>"; }; EE7EFB209C86BBD956B749EC /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; }; EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; };
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSimplifiedBluetoothService.swift; sourceTree = "<group>"; };
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; }; FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
04F127E92E37EBEF00FFBA8D /* Frameworks */ = { 31F6FDADA63050361C14F3A1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
04E363362E3800310048E624 /* P256K in Frameworks */, 3EE336D150427F736F32B56C /* P256K in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
04F127ED2E37EBFF00FFBA8D /* Frameworks */ = { B5A5CC493FFB3D8966548140 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
04E363372E3800310048E624 /* P256K in Frameworks */, 885BBED78092484A5B069461 /* P256K in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
04636BC62E30BE5100FBCFA8 /* EndToEnd */ = { 0575DCBD15C7C719ADDCB67E /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */, 11186E29A064E8D210880E1B /* BitchatPeer.swift */,
04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */,
);
path = EndToEnd;
sourceTree = "<group>";
};
04636BC92E30BE5100FBCFA8 /* Mocks */ = {
isa = PBXGroup;
children = (
04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */,
04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
04636BCB2E30BE5100FBCFA8 /* Noise */ = {
isa = PBXGroup;
children = (
04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */,
);
path = Noise;
sourceTree = "<group>";
};
04636BCD2E30BE5100FBCFA8 /* Protocol */ = {
isa = PBXGroup;
children = (
04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */,
);
path = Protocol;
sourceTree = "<group>";
};
04636BD02E30BE5100FBCFA8 /* TestUtilities */ = {
isa = PBXGroup;
children = (
04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */,
04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */,
);
path = TestUtilities;
sourceTree = "<group>";
};
04636BE22E30BEC600FBCFA8 /* Integration */ = {
isa = PBXGroup;
children = (
04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */,
);
path = Integration;
sourceTree = "<group>";
};
04B6BA442E2035530090FE39 /* Noise */ = {
isa = PBXGroup;
children = (
04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */,
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */,
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */,
04B6BA432E2035530090FE39 /* NoiseSession.swift */,
);
path = Noise;
sourceTree = "<group>";
};
04F127E32E37EBAA00FFBA8D /* Nostr */ = {
isa = PBXGroup;
children = (
04F127F02E37EEB800FFBA8D /* NostrProtocol.swift */,
04F127F12E37EEB800FFBA8D /* NostrRelayManager.swift */,
04F127E22E37EBAA00FFBA8D /* NostrIdentity.swift */,
);
path = Nostr;
sourceTree = "<group>";
};
04F127EA2E37EBF300FFBA8D /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
04F127FD2E37EF3D00FFBA8D /* Models */ = {
isa = PBXGroup;
children = (
04F127FC2E37EF3D00FFBA8D /* BitchatPeer.swift */,
04F128072E37F10000FFBA8D /* PeerSession.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -318,10 +232,18 @@
A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */, A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */,
C3D98EB3E1B455E321F519F4 /* bitchatTests */, C3D98EB3E1B455E321F519F4 /* bitchatTests */,
9F37F9F2C353B58AC809E93B /* Products */, 9F37F9F2C353B58AC809E93B /* Products */,
04F127EA2E37EBF300FFBA8D /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
204CC4C7704C7348D456E374 /* TestUtilities */ = {
isa = PBXGroup;
children = (
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */,
2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */,
);
path = TestUtilities;
sourceTree = "<group>";
};
2F82C5FC8433F4064F079D1F /* bitchat */ = { 2F82C5FC8433F4064F079D1F /* bitchat */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -331,12 +253,12 @@
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */, EF625BB3AD919322C01A46B2 /* BitchatApp.swift */,
EA706D8E5097785414646A8E /* Info.plist */, EA706D8E5097785414646A8E /* Info.plist */,
95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */, 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */,
C845B6F5D25AEEA0B9FE557F /* Identity */,
0575DCBD15C7C719ADDCB67E /* Models */,
637EDFDD042BDB5F2569A501 /* Noise */,
E78C7F4B6769C0A72F5DE544 /* Nostr */,
ADD53BCDA233C02E53458926 /* Protocols */, ADD53BCDA233C02E53458926 /* Protocols */,
04B6BA442E2035530090FE39 /* Noise */,
04F127E32E37EBAA00FFBA8D /* Nostr */,
D98A3186D7E4C72E35BDF7FE /* Services */, D98A3186D7E4C72E35BDF7FE /* Services */,
6078981E5A3646BC84CC6DB4 /* Identity */,
04F127FD2E37EF3D00FFBA8D /* Models */,
9A78348821A7D3374607D4E3 /* Utils */, 9A78348821A7D3374607D4E3 /* Utils */,
45BB7D87CAE42A8C0447D909 /* ViewModels */, 45BB7D87CAE42A8C0447D909 /* ViewModels */,
A55126E93155456CAA8D6656 /* Views */, A55126E93155456CAA8D6656 /* Views */,
@@ -352,24 +274,48 @@
path = ViewModels; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6078981E5A3646BC84CC6DB4 /* Identity */ = { 5B90895AFF0957E08FA3D429 /* Integration */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6E2446380E7A44E49A35B664 /* IdentityModels.swift */, 5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.swift */,
2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */,
); );
path = Identity; path = Integration;
sourceTree = "<group>";
};
637EDFDD042BDB5F2569A501 /* Noise */ = {
isa = PBXGroup;
children = (
B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.swift */,
43613045E63D21D429396805 /* NoiseProtocol.swift */,
43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */,
9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */,
);
path = Noise;
sourceTree = "<group>";
};
84933DAE9D7E5D0155BA7AEA /* Protocol */ = {
isa = PBXGroup;
children = (
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */,
);
path = Protocol;
sourceTree = "<group>";
};
966CD21F221332CF564AC724 /* Mocks */ = {
isa = PBXGroup;
children = (
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */,
FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */,
);
path = Mocks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9A78348821A7D3374607D4E3 /* Utils */ = { 9A78348821A7D3374607D4E3 /* Utils */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
046D705C2E3C105D00C00594 /* InputValidator.swift */,
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */,
04891CA82E22971E0064A111 /* LRUCache.swift */,
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */,
32F149C43D1915831B60FE09 /* CompressionUtil.swift */, 32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */, 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */,
EE7EFB209C86BBD956B749EC /* SecureLogger.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -399,9 +345,9 @@
A55126E93155456CAA8D6656 /* Views */ = { A55126E93155456CAA8D6656 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
04B6BA532E203D6C0090FE39 /* FingerprintView.swift */,
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */, 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
A08E03AA0C63E97C91749AEC /* ContentView.swift */, A08E03AA0C63E97C91749AEC /* ContentView.swift */,
9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */,
9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */, 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */,
); );
path = Views; path = Views;
@@ -410,43 +356,81 @@
ADD53BCDA233C02E53458926 /* Protocols */ = { ADD53BCDA233C02E53458926 /* Protocols */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */, 5318B743C64628A125261163 /* BinaryEncodingUtils.swift */,
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */, A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */,
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */, 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */,
); );
path = Protocols; path = Protocols;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C2F78AB254FDAD5FEDA18B58 /* EndToEnd */ = {
isa = PBXGroup;
children = (
8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */,
D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */,
);
path = EndToEnd;
sourceTree = "<group>";
};
C3D98EB3E1B455E321F519F4 /* bitchatTests */ = { C3D98EB3E1B455E321F519F4 /* bitchatTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
04636BE22E30BEC600FBCFA8 /* Integration */,
04636BC62E30BE5100FBCFA8 /* EndToEnd */,
04636BC92E30BE5100FBCFA8 /* Mocks */,
04636BCB2E30BE5100FBCFA8 /* Noise */,
04636BCD2E30BE5100FBCFA8 /* Protocol */,
04636BD02E30BE5100FBCFA8 /* TestUtilities */,
D69A18D27F9A565FD6041E12 /* Info.plist */, D69A18D27F9A565FD6041E12 /* Info.plist */,
C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */,
980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */,
C2F78AB254FDAD5FEDA18B58 /* EndToEnd */,
5B90895AFF0957E08FA3D429 /* Integration */,
966CD21F221332CF564AC724 /* Mocks */,
D80E19E04513C0046D611574 /* Noise */,
84933DAE9D7E5D0155BA7AEA /* Protocol */,
204CC4C7704C7348D456E374 /* TestUtilities */,
); );
path = bitchatTests; path = bitchatTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C845B6F5D25AEEA0B9FE557F /* Identity */ = {
isa = PBXGroup;
children = (
05BA20BC0F123F1507C5C247 /* IdentityModels.swift */,
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */,
);
path = Identity;
sourceTree = "<group>";
};
D80E19E04513C0046D611574 /* Noise */ = {
isa = PBXGroup;
children = (
E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */,
);
path = Noise;
sourceTree = "<group>";
};
D98A3186D7E4C72E35BDF7FE /* Services */ = { D98A3186D7E4C72E35BDF7FE /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
04F127F62E37EEEE00FFBA8D /* FavoritesPersistenceService.swift */, 049BD3982E506A12001A566B /* UnifiedPeerService.swift */,
04F127F72E37EEEE00FFBA8D /* MessageRouter.swift */, 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */,
04F127FF2E37F00000FFBA8D /* ProcessedMessagesService.swift */, 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */,
04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */, 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */,
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */, 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */,
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */,
136696FC4436A02D98CE6A77 /* KeychainManager.swift */, 136696FC4436A02D98CE6A77 /* KeychainManager.swift */,
AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */, 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */,
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */, 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */,
8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */,
); );
path = Services; path = Services;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E78C7F4B6769C0A72F5DE544 /* Nostr */ = {
isa = PBXGroup;
children = (
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */,
);
path = Nostr;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -456,7 +440,7 @@
buildPhases = ( buildPhases = (
137ABE739BF20ACDDF8CC605 /* Sources */, 137ABE739BF20ACDDF8CC605 /* Sources */,
0214973A876129753D39EB47 /* Resources */, 0214973A876129753D39EB47 /* Resources */,
04F127ED2E37EBFF00FFBA8D /* Frameworks */, 31F6FDADA63050361C14F3A1 /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -464,7 +448,7 @@
); );
name = bitchat_macOS; name = bitchat_macOS;
packageProductDependencies = ( packageProductDependencies = (
04F127E82E37EBDB00FFBA8D /* P256K */, B1D9136AA0083366353BFA2F /* P256K */,
); );
productName = bitchat_macOS; productName = bitchat_macOS;
productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */; productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */;
@@ -529,8 +513,8 @@
buildPhases = ( buildPhases = (
4E49E34F00154C051AE90FED /* Sources */, 4E49E34F00154C051AE90FED /* Sources */,
CD6E8F32BC38357473954F97 /* Resources */, CD6E8F32BC38357473954F97 /* Resources */,
B5A5CC493FFB3D8966548140 /* Frameworks */,
B6C356449BAE4E0F650565D1 /* Embed Foundation Extensions */, B6C356449BAE4E0F650565D1 /* Embed Foundation Extensions */,
04F127E92E37EBEF00FFBA8D /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -539,7 +523,7 @@
); );
name = bitchat_iOS; name = bitchat_iOS;
packageProductDependencies = ( packageProductDependencies = (
04F127E72E37EBCD00FFBA8D /* P256K */, 4EB6BA1B8464F1EA38F4E286 /* P256K */,
); );
productName = bitchat_iOS; productName = bitchat_iOS;
productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */; productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */;
@@ -587,7 +571,7 @@
mainGroup = 18198ED912AAF495D8AF7763; mainGroup = 18198ED912AAF495D8AF7763;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
04F127E62E37EBCD00FFBA8D /* XCRemoteSwiftPackageReference "swift-secp256k1" */, B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */,
); );
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@@ -635,40 +619,36 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */, AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
C0A80BA73EC1A372B9338E3C /* BatteryOptimizer.swift in Sources */, 9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */,
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */, 4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */, 6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */, 84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */,
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */, 923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
04B6BA452E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */,
04B6BA462E2035530090FE39 /* NoiseSession.swift in Sources */,
04B6BA472E2035530090FE39 /* NoiseProtocol.swift in Sources */,
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */,
04F127F42E37EEB800FFBA8D /* NostrProtocol.swift in Sources */,
04F127F52E37EEB800FFBA8D /* NostrRelayManager.swift in Sources */,
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */, D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */, B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */,
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */, 92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */, 5C93B4FDD0C448C3EDDBF8AE /* FavoritesPersistenceService.swift in Sources */,
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */, 6C63FA98D59854C15C57B3D6 /* FingerprintView.swift in Sources */,
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */,
7241FFD6CFFB875B864FA223 /* InputValidator.swift in Sources */,
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */, FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
04F127E52E37EBAA00FFBA8D /* NostrIdentity.swift in Sources */,
04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */,
04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */,
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */, 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
04F127FE2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */, 501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */,
04F128062E37F10000FFBA8D /* PeerSession.swift in Sources */, AFF33EF44626EF0579D17EB1 /* NoiseHandshakeCoordinator.swift in Sources */,
046D705E2E3C105D00C00594 /* InputValidator.swift in Sources */, 8C1AB0F2D48207E0755DA91A /* NoiseProtocol.swift in Sources */,
04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */, D691938B4029A04CC905FDC8 /* NoiseSecurityConsiderations.swift in Sources */,
04636BED2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */, 8A14ADADF5CD7A79919CB655 /* NoiseSession.swift in Sources */,
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */, 049BD3992E506A12001A566B /* UnifiedPeerService.swift in Sources */,
C3B1226CD30C87501EF6F12F /* NostrIdentity.swift in Sources */,
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */,
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */,
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */, 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */, E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */,
0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */, 049BD3942E4EC4F0001A566B /* PrivateChatManager.swift in Sources */,
04F127F82E37EEEE00FFBA8D /* MessageRouter.swift in Sources */, 049BD3952E4EC4F0001A566B /* AutocompleteService.swift in Sources */,
04F127F92E37EEEE00FFBA8D /* FavoritesPersistenceService.swift in Sources */, 049BD3962E4EC4F0001A566B /* CommandProcessor.swift in Sources */,
04F128042E37F00000FFBA8D /* ProcessedMessagesService.swift in Sources */, D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */,
8DE687D2EB5EB120868DBFB5 /* SimplifiedBluetoothService.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -677,40 +657,36 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */, ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
5D95F2BFBE257A1225998389 /* BatteryOptimizer.swift in Sources */, AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */,
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */, F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */, 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */, 84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */,
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */, 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
04B6BA492E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */,
04B6BA4A2E2035530090FE39 /* NoiseSession.swift in Sources */,
04B6BA4B2E2035530090FE39 /* NoiseProtocol.swift in Sources */,
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */,
04F127F22E37EEB800FFBA8D /* NostrProtocol.swift in Sources */,
04F127F32E37EEB800FFBA8D /* NostrRelayManager.swift in Sources */,
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */, 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */, 7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */,
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */, 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */, ACE2ED172C37F01561E50B71 /* FavoritesPersistenceService.swift in Sources */,
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */, 132DF1E24B4E9C7DCDAD4376 /* FingerprintView.swift in Sources */,
B909706CD38FC56C0C8EB7BF /* IdentityModels.swift in Sources */,
EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */,
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */, 8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
04F127E42E37EBAA00FFBA8D /* NostrIdentity.swift in Sources */,
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */,
1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */,
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */, 7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
04F127FF2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */, 5EE49E150BBF0488E7473687 /* NoiseEncryptionService.swift in Sources */,
04F128082E37F10000FFBA8D /* PeerSession.swift in Sources */, 6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */,
046D705D2E3C105D00C00594 /* InputValidator.swift in Sources */, A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */,
04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */, 9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */,
04636BEE2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */, 92D1CF17DF88EA298F6E5E8E /* NoiseSession.swift in Sources */,
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */, 049BD39A2E506A12001A566B /* UnifiedPeerService.swift in Sources */,
D782AB596DDB5C846554F7C3 /* NostrIdentity.swift in Sources */,
F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */,
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */,
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */, 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */, 68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */,
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */, 049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */,
04F127FA2E37EEEE00FFBA8D /* MessageRouter.swift in Sources */, 049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */,
04F127FB2E37EEEE00FFBA8D /* FavoritesPersistenceService.swift in Sources */, 049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */,
04F128052E37F00000FFBA8D /* ProcessedMessagesService.swift in Sources */, EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */,
C165DD35BB8E9C327A3C2DA4 /* SimplifiedBluetoothService.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -718,15 +694,17 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
04636BD92E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */, AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */,
04636BE82E30BEC600FBCFA8 /* IntegrationTests.swift in Sources */, 8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
04636BDA2E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */, D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
04636BDB2E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */, 6C803BF930E7E19BE6E99EAA /* MockSimplifiedBluetoothService.swift in Sources */,
04636BDC2E30BE5100FBCFA8 /* TestConstants.swift in Sources */, 765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */,
04636BDD2E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */, 968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */,
04636BDE2E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */, ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */,
04636BDF2E30BE5100FBCFA8 /* TestHelpers.swift in Sources */, A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */,
04636BE02E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */, A2977428C1D9EF9944C4BFAF /* SimplifiedBluetoothServiceTests.swift in Sources */,
2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */,
B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -734,15 +712,17 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
04636BD12E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */, 0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */,
04636BEB2E30BEC600FBCFA8 /* IntegrationTests.swift in Sources */, 686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
04636BD22E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */, 8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
04636BD32E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */, 3849CA6D99B2D536636DF4A6 /* MockSimplifiedBluetoothService.swift in Sources */,
04636BD42E30BE5100FBCFA8 /* TestConstants.swift in Sources */, BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
04636BD52E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */, EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
04636BD62E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */, 0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */,
04636BD72E30BE5100FBCFA8 /* TestHelpers.swift in Sources */, 8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */,
04636BD82E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */, BE729E149C98F775D9622D9C /* SimplifiedBluetoothServiceTests.swift in Sources */,
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */,
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -842,7 +822,6 @@
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
INFOPLIST_FILE = bitchatShareExtension/Info.plist; INFOPLIST_FILE = bitchatShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -873,7 +852,6 @@
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
@@ -929,7 +907,6 @@
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
@@ -962,12 +939,10 @@
CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements"; CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@@ -986,7 +961,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -1054,12 +1028,10 @@
CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements"; CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@@ -1078,7 +1050,6 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -1151,7 +1122,6 @@
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
INFOPLIST_FILE = bitchatShareExtension/Info.plist; INFOPLIST_FILE = bitchatShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -1232,7 +1202,7 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
04F127E62E37EBCD00FFBA8D /* XCRemoteSwiftPackageReference "swift-secp256k1" */ = { B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/21-DOT-DEV/swift-secp256k1"; repositoryURL = "https://github.com/21-DOT-DEV/swift-secp256k1";
requirement = { requirement = {
@@ -1243,14 +1213,14 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
04F127E72E37EBCD00FFBA8D /* P256K */ = { 4EB6BA1B8464F1EA38F4E286 /* P256K */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 04F127E62E37EBCD00FFBA8D /* XCRemoteSwiftPackageReference "swift-secp256k1" */; package = B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */;
productName = P256K; productName = P256K;
}; };
04F127E82E37EBDB00FFBA8D /* P256K */ = { B1D9136AA0083366353BFA2F /* P256K */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 04F127E62E37EBCD00FFBA8D /* XCRemoteSwiftPackageReference "swift-secp256k1" */; package = B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */;
productName = P256K; productName = P256K;
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
+17 -4
View File
@@ -13,6 +13,7 @@ import UserNotifications
struct BitchatApp: App { struct BitchatApp: App {
@StateObject private var chatViewModel = ChatViewModel() @StateObject private var chatViewModel = ChatViewModel()
#if os(iOS) #if os(iOS)
@Environment(\.scenePhase) var scenePhase
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#elseif os(macOS) #elseif os(macOS)
@NSApplicationDelegateAdaptor(MacAppDelegate.self) var appDelegate @NSApplicationDelegateAdaptor(MacAppDelegate.self) var appDelegate
@@ -40,16 +41,28 @@ struct BitchatApp: App {
handleURL(url) handleURL(url)
} }
#if os(iOS) #if os(iOS)
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .background:
// Send leave message when going to background
chatViewModel.meshService.stopServices()
case .active:
// Restart services when becoming active
chatViewModel.meshService.startServices()
checkForSharedContent()
case .inactive:
break
@unknown default:
break
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
// Check for shared content when app becomes active // Check for shared content when app becomes active
checkForSharedContent() checkForSharedContent()
// Notify MessageRouter to check for Nostr messages
NotificationCenter.default.post(name: .appDidBecomeActive, object: nil)
} }
#elseif os(macOS) #elseif os(macOS)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
// Notify MessageRouter to check for Nostr messages // App became active
NotificationCenter.default.post(name: .appDidBecomeActive, object: nil)
} }
#endif #endif
} }
+2 -21
View File
@@ -39,33 +39,14 @@
<array> <array>
<string>bluetooth-central</string> <string>bluetooth-central</string>
<string>bluetooth-peripheral</string> <string>bluetooth-peripheral</string>
<string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsArbitraryLoadsForMedia</key>
<false/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>bitchat uses local network access to discover and connect with other bitchat users on your network.</string>
</dict> </dict>
</plist> </plist>
+6 -25
View File
@@ -98,10 +98,10 @@ class PeerManager: ObservableObject {
@Published var favorites: [BitchatPeer] = [] @Published var favorites: [BitchatPeer] = []
@Published var mutualFavorites: [BitchatPeer] = [] @Published var mutualFavorites: [BitchatPeer] = []
private let meshService: BluetoothMeshService private let meshService: SimplifiedBluetoothService
private let favoritesService = FavoritesPersistenceService.shared private let favoritesService = FavoritesPersistenceService.shared
init(meshService: BluetoothMeshService) { init(meshService: SimplifiedBluetoothService) {
self.meshService = meshService self.meshService = meshService
updatePeers() updatePeers()
@@ -142,8 +142,6 @@ class PeerManager: ObservableObject {
// Safety check: Never add our own peer ID // Safety check: Never add our own peer ID
if peerID == meshService.myPeerID { if peerID == meshService.myPeerID {
SecureLogger.log("⚠️ Skipping self peer ID \(peerID) in peer list",
category: SecureLogger.session, level: .warning)
continue continue
} }
@@ -195,22 +193,19 @@ class PeerManager: ObservableObject {
// Skip if this peer is already connected (by nickname) // Skip if this peer is already connected (by nickname)
if connectedNicknames.contains(favorite.peerNickname) { if connectedNicknames.contains(favorite.peerNickname) {
SecureLogger.log(" - Skipping '\(favorite.peerNickname)' (key: \(favoriteKey.hexEncodedString())) - already connected", // Skipping favorite - already connected
category: SecureLogger.session, level: .debug)
continue continue
} }
// Skip if we already added a peer with this ID (prevents duplicates) // Skip if we already added a peer with this ID (prevents duplicates)
if addedPeerIDs.contains(favoriteID) { if addedPeerIDs.contains(favoriteID) {
SecureLogger.log(" - Skipping '\(favorite.peerNickname)' - peer ID already added", // Skipping favorite - peer ID already added
category: SecureLogger.session, level: .debug)
continue continue
} }
// Only add peers that WE favorite (not just ones who favorite us) // Only add peers that WE favorite (not just ones who favorite us)
if !favorite.isFavorite { if !favorite.isFavorite {
SecureLogger.log(" - Skipping '\(favorite.peerNickname)' - we don't favorite them (they favorite us: \(favorite.theyFavoritedUs))", // Skipping - we don't favorite them
category: SecureLogger.session, level: .debug)
continue continue
} }
@@ -292,21 +287,7 @@ class PeerManager: ObservableObject {
SecureLogger.log("📊 Peer list update: \(allPeers.count) total (\(connectedCount) connected, \(offlineCount) offline), \(favorites.count) favorites, \(mutualFavorites.count) mutual", SecureLogger.log("📊 Peer list update: \(allPeers.count) total (\(connectedCount) connected, \(offlineCount) offline), \(favorites.count) favorites, \(mutualFavorites.count) mutual",
category: SecureLogger.session, level: .info) category: SecureLogger.session, level: .info)
// Log each peer's status // Detailed peer status logging removed for brevity
for peer in allPeers {
let statusIcon: String
switch peer.connectionState {
case .bluetoothConnected:
statusIcon = "🟢"
case .nostrAvailable:
statusIcon = "🌐"
case .offline:
statusIcon = "🔴"
}
let favoriteIcon = peer.isMutualFavorite ? "💕" : (peer.isFavorite ? "" : (peer.theyFavoritedUs ? "🌙" : ""))
SecureLogger.log(" \(statusIcon) \(peer.displayName) (ID: \(peer.id.prefix(8))...) \(favoriteIcon)",
category: SecureLogger.session, level: .debug)
}
} else if previousCount != allPeers.count { } else if previousCount != allPeers.count {
// Only log non-favorite updates if count changed // Only log non-favorite updates if count changed
SecureLogger.log("✅ Updated peer list: \(allPeers.count) total peers", SecureLogger.log("✅ Updated peer list: \(allPeers.count) total peers",
-91
View File
@@ -1,91 +0,0 @@
import Foundation
import CoreBluetooth
/// Unified model for tracking all peer session data
/// Consolidates multiple redundant data structures into a single source of truth
class PeerSession {
// Core identification
let peerID: String
var nickname: String
// Bluetooth connection
var peripheral: CBPeripheral?
var peripheralID: String?
var characteristic: CBCharacteristic?
// Authentication and encryption
var isAuthenticated: Bool = false
var hasEstablishedNoiseSession: Bool = false
var fingerprint: String?
// Connection state
var isConnected: Bool = false
var lastSeen: Date
// Protocol state
var hasAnnounced: Bool = false
var hasReceivedAnnounce: Bool = false
var isActivePeer: Bool = false
// Message tracking
var lastMessageSent: Date?
var lastMessageReceived: Date?
var pendingMessages: [String] = []
// Connection timing
var lastConnectionTime: Date?
var lastSuccessfulMessageTime: Date?
var lastHeardFromPeer: Date?
// Availability tracking
var isAvailable: Bool = false
// Identity binding
var identityBinding: PeerIdentityBinding?
init(peerID: String, nickname: String = "Unknown") {
self.peerID = peerID
self.nickname = nickname
self.lastSeen = Date()
}
/// Update Bluetooth connection info
func updateBluetoothConnection(peripheral: CBPeripheral?, characteristic: CBCharacteristic?) {
self.peripheral = peripheral
self.peripheralID = peripheral?.identifier.uuidString
self.characteristic = characteristic
self.isConnected = (peripheral?.state == .connected)
if isConnected {
self.lastSeen = Date()
}
}
/// Update authentication state
func updateAuthenticationState(authenticated: Bool, noiseSession: Bool) {
self.isAuthenticated = authenticated
self.hasEstablishedNoiseSession = noiseSession
if authenticated {
self.isActivePeer = true
}
}
/// Check if session is stale
var isStale: Bool {
// Consider stale if not seen for more than 5 minutes and not connected
return !isConnected && Date().timeIntervalSince(lastSeen) > 300
}
/// Get display status for UI
var displayStatus: String {
if isConnected {
if isAuthenticated {
return "🟢" // Connected and authenticated
} else {
return "🟡" // Connected but not authenticated
}
} else {
return "🔴" // Not connected
}
}
}
+10
View File
@@ -140,6 +140,16 @@ struct NostrIdentityBridge {
} }
return pubkey 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) // Bech32 encoding for Nostr (minimal implementation)
+6 -4
View File
@@ -59,10 +59,11 @@ struct NostrProtocol {
} }
/// Decrypt a received NIP-17 message /// 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( static func decryptPrivateMessage(
giftWrap: NostrEvent, giftWrap: NostrEvent,
recipientIdentity: NostrIdentity recipientIdentity: NostrIdentity
) throws -> (content: String, senderPubkey: String) { ) throws -> (content: String, senderPubkey: String, timestamp: Int) {
// Starting decryption // Starting decryption
@@ -94,7 +95,7 @@ struct NostrProtocol {
throw error throw error
} }
return (content: rumor.content, senderPubkey: rumor.pubkey) return (content: rumor.content, senderPubkey: rumor.pubkey, timestamp: rumor.created_at)
} }
// MARK: - Private Methods // MARK: - Private Methods
@@ -438,8 +439,9 @@ struct NostrProtocol {
private static func randomizedTimestamp() -> Date { private static func randomizedTimestamp() -> Date {
// Add random offset to current time for privacy // Add random offset to current time for privacy
// TEMPORARY: Reduced range to debug timestamp issue // This prevents timing correlation attacks while the actual message timestamp
let offset = TimeInterval.random(in: -60...60) // +/- 1 minute (was +/- 15 minutes) // is preserved in the encrypted rumor
let offset = TimeInterval.random(in: -900...900) // +/- 15 minutes
let now = Date() let now = Date()
let randomized = now.addingTimeInterval(offset) let randomized = now.addingTimeInterval(offset)
+24 -9
View File
@@ -57,6 +57,7 @@ class NostrRelayManager: ObservableObject {
/// Connect to all configured relays /// Connect to all configured relays
func connect() { func connect() {
SecureLogger.log("🌐 Connecting to \(relays.count) Nostr relays", category: SecureLogger.session, level: .debug)
for relay in relays { for relay in relays {
connectToRelay(relay.url) connectToRelay(relay.url)
} }
@@ -96,6 +97,9 @@ class NostrRelayManager: ObservableObject {
) { ) {
messageHandlers[id] = handler 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]) let req = NostrRequest.subscribe(id: id, filters: [filter])
do { do {
@@ -107,9 +111,8 @@ class NostrRelayManager: ObservableObject {
return return
} }
// Sending subscription to relays // SecureLogger.log("📋 Subscription filter JSON: \(messageString.prefix(200))...",
// Filter JSON prepared // category: SecureLogger.session, level: .debug)
// Full filter JSON logged
// Send subscription to all connected relays // Send subscription to all connected relays
for (relayUrl, connection) in connections { for (relayUrl, connection) in connections {
@@ -118,6 +121,8 @@ class NostrRelayManager: ObservableObject {
SecureLogger.log("❌ Failed to send subscription to \(relayUrl): \(error)", SecureLogger.log("❌ Failed to send subscription to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error) category: SecureLogger.session, level: .error)
} else { } else {
// SecureLogger.log(" Subscription '\(id)' sent to relay: \(relayUrl)",
// category: SecureLogger.session, level: .debug)
// Subscription sent successfully // Subscription sent successfully
Task { @MainActor in Task { @MainActor in
var subs = self.subscriptions[relayUrl] ?? Set<String>() var subs = self.subscriptions[relayUrl] ?? Set<String>()
@@ -188,10 +193,11 @@ class NostrRelayManager: ObservableObject {
task.sendPing { [weak self] error in task.sendPing { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
if error == nil { 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) self?.updateRelayStatus(urlString, isConnected: true)
} else { } 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) category: SecureLogger.session, level: .error)
self?.updateRelayStatus(urlString, isConnected: false, error: error) self?.updateRelayStatus(urlString, isConnected: false, error: error)
// Trigger disconnection handler for proper backoff // Trigger disconnection handler for proper backoff
@@ -254,7 +260,11 @@ class NostrRelayManager: ObservableObject {
let event = try NostrEvent(from: eventDict) 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 { DispatchQueue.main.async {
// Update relay stats // Update relay stats
@@ -282,7 +292,10 @@ class NostrRelayManager: ObservableObject {
let eventId = array[1] as? String, let eventId = array[1] as? String,
let success = array[2] as? Bool { let success = array[2] as? Bool {
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given" 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)", SecureLogger.log("📮 Event \(eventId) rejected by relay: \(reason)",
category: SecureLogger.session, level: .error) category: SecureLogger.session, level: .error)
} }
@@ -311,8 +324,8 @@ class NostrRelayManager: ObservableObject {
let data = try encoder.encode(req) let data = try encoder.encode(req)
let message = String(data: data, encoding: .utf8) ?? "" let message = String(data: data, encoding: .utf8) ?? ""
// Sending event to relay SecureLogger.log("📤 Sending Nostr event (kind: \(event.kind)) to relay: \(relayUrl)",
// Event JSON prepared category: SecureLogger.session, level: .debug)
connection.send(.string(message)) { [weak self] error in connection.send(.string(message)) { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -320,6 +333,8 @@ class NostrRelayManager: ObservableObject {
SecureLogger.log("❌ Failed to send event to \(relayUrl): \(error)", SecureLogger.log("❌ Failed to send event to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error) category: SecureLogger.session, level: .error)
} else { } else {
// SecureLogger.log(" Event sent to relay: \(relayUrl)",
// category: SecureLogger.session, level: .debug)
// Update relay stats // Update relay stats
if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) { if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) {
self?.relays[index].messagesSent += 1 self?.relays[index].messagesSent += 1
+28 -26
View File
@@ -209,7 +209,7 @@ struct BinaryProtocol {
// Decode binary data to BitchatPacket // Decode binary data to BitchatPacket
static func decode(_ data: Data) -> BitchatPacket? { static func decode(_ data: Data) -> BitchatPacket? {
// Remove padding first // Remove padding first and create a defensive copy
let unpaddedData = MessagePadding.unpad(data) let unpaddedData = MessagePadding.unpad(data)
// Minimum size check: header + senderID // Minimum size check: header + senderID
@@ -217,41 +217,43 @@ struct BinaryProtocol {
return nil return nil
} }
// Convert to array for safer indexed access
let dataArray = Array(unpaddedData)
var offset = 0 var offset = 0
// Header parsing with bounds checks // Header parsing with bounds checks
guard offset + 1 <= unpaddedData.count else { return nil } guard offset < dataArray.count else { return nil }
let version = unpaddedData[offset]; offset += 1 let version = dataArray[offset]; offset += 1
// Check if version is 1 (only supported version) // Check if version is 1 (only supported version)
guard version == 1 else { guard version == 1 else {
return nil return nil
} }
guard offset + 1 <= unpaddedData.count else { return nil } guard offset < dataArray.count else { return nil }
let type = unpaddedData[offset]; offset += 1 let type = dataArray[offset]; offset += 1
guard offset + 1 <= unpaddedData.count else { return nil } guard offset < dataArray.count else { return nil }
let ttl = unpaddedData[offset]; offset += 1 let ttl = dataArray[offset]; offset += 1
// Timestamp - need 8 bytes // Timestamp - need 8 bytes
guard offset + 8 <= unpaddedData.count else { return nil } guard offset + 8 <= dataArray.count else { return nil }
let timestampData = unpaddedData[offset..<offset+8] let timestampData = Data(dataArray[offset..<offset+8])
let timestamp = timestampData.reduce(0) { result, byte in let timestamp = timestampData.reduce(0) { result, byte in
(result << 8) | UInt64(byte) (result << 8) | UInt64(byte)
} }
offset += 8 offset += 8
// Flags // Flags
guard offset + 1 <= unpaddedData.count else { return nil } guard offset < dataArray.count else { return nil }
let flags = unpaddedData[offset]; offset += 1 let flags = dataArray[offset]; offset += 1
let hasRecipient = (flags & Flags.hasRecipient) != 0 let hasRecipient = (flags & Flags.hasRecipient) != 0
let hasSignature = (flags & Flags.hasSignature) != 0 let hasSignature = (flags & Flags.hasSignature) != 0
let isCompressed = (flags & Flags.isCompressed) != 0 let isCompressed = (flags & Flags.isCompressed) != 0
// Payload length - need 2 bytes // Payload length - need 2 bytes
guard offset + 2 <= unpaddedData.count else { return nil } guard offset + 2 <= dataArray.count else { return nil }
let payloadLengthData = unpaddedData[offset..<offset+2] let payloadLengthData = Data(dataArray[offset..<offset+2])
let payloadLength = payloadLengthData.reduce(0) { result, byte in let payloadLength = payloadLengthData.reduce(0) { result, byte in
(result << 8) | UInt16(byte) (result << 8) | UInt16(byte)
} }
@@ -261,15 +263,15 @@ struct BinaryProtocol {
guard payloadLength <= 65535 else { return nil } guard payloadLength <= 65535 else { return nil }
// SenderID - need 8 bytes // SenderID - need 8 bytes
guard offset + senderIDSize <= unpaddedData.count else { return nil } guard offset + senderIDSize <= dataArray.count else { return nil }
let senderID = unpaddedData[offset..<offset+senderIDSize] let senderID = Data(dataArray[offset..<offset+senderIDSize])
offset += senderIDSize offset += senderIDSize
// RecipientID if present // RecipientID if present
var recipientID: Data? var recipientID: Data?
if hasRecipient { if hasRecipient {
guard offset + recipientIDSize <= unpaddedData.count else { return nil } guard offset + recipientIDSize <= dataArray.count else { return nil }
recipientID = unpaddedData[offset..<offset+recipientIDSize] recipientID = Data(dataArray[offset..<offset+recipientIDSize])
offset += recipientIDSize offset += recipientIDSize
} }
@@ -280,8 +282,8 @@ struct BinaryProtocol {
guard Int(payloadLength) >= 2 else { return nil } guard Int(payloadLength) >= 2 else { return nil }
// Check we have enough data for the original size prefix // Check we have enough data for the original size prefix
guard offset + 2 <= unpaddedData.count else { return nil } guard offset + 2 <= dataArray.count else { return nil }
let originalSizeData = unpaddedData[offset..<offset+2] let originalSizeData = Data(dataArray[offset..<offset+2])
let originalSize = Int(originalSizeData.reduce(0) { result, byte in let originalSize = Int(originalSizeData.reduce(0) { result, byte in
(result << 8) | UInt16(byte) (result << 8) | UInt16(byte)
}) })
@@ -292,11 +294,11 @@ struct BinaryProtocol {
// Check we have enough data for the compressed payload // Check we have enough data for the compressed payload
let compressedPayloadSize = Int(payloadLength) - 2 let compressedPayloadSize = Int(payloadLength) - 2
guard compressedPayloadSize >= 0 && offset + compressedPayloadSize <= unpaddedData.count else { guard compressedPayloadSize >= 0 && offset + compressedPayloadSize <= dataArray.count else {
return nil return nil
} }
let compressedPayload = unpaddedData[offset..<offset+compressedPayloadSize] let compressedPayload = Data(dataArray[offset..<offset+compressedPayloadSize])
offset += compressedPayloadSize offset += compressedPayloadSize
// Decompress with error handling // Decompress with error handling
@@ -312,23 +314,23 @@ struct BinaryProtocol {
payload = decompressedPayload payload = decompressedPayload
} else { } else {
// Uncompressed payload // Uncompressed payload
guard Int(payloadLength) >= 0 && offset + Int(payloadLength) <= unpaddedData.count else { guard Int(payloadLength) >= 0 && offset + Int(payloadLength) <= dataArray.count else {
return nil return nil
} }
payload = unpaddedData[offset..<offset+Int(payloadLength)] payload = Data(dataArray[offset..<offset+Int(payloadLength)])
offset += Int(payloadLength) offset += Int(payloadLength)
} }
// Signature if present // Signature if present
var signature: Data? var signature: Data?
if hasSignature { if hasSignature {
guard offset + signatureSize <= unpaddedData.count else { return nil } guard offset + signatureSize <= dataArray.count else { return nil }
signature = unpaddedData[offset..<offset+signatureSize] signature = Data(dataArray[offset..<offset+signatureSize])
offset += signatureSize offset += signatureSize
} }
// Final validation: ensure we haven't gone past the end // Final validation: ensure we haven't gone past the end
guard offset <= unpaddedData.count else { return nil } guard offset <= dataArray.count else { return nil }
return BitchatPacket( return BitchatPacket(
type: type, type: type,
+85 -629
View File
@@ -32,7 +32,7 @@
/// 1. **Creation**: Messages are created with type, content, and metadata /// 1. **Creation**: Messages are created with type, content, and metadata
/// 2. **Encoding**: Converted to binary format with proper field ordering /// 2. **Encoding**: Converted to binary format with proper field ordering
/// 3. **Fragmentation**: Split if larger than BLE MTU (512 bytes) /// 3. **Fragmentation**: Split if larger than BLE MTU (512 bytes)
/// 4. **Transmission**: Sent via BluetoothMeshService /// 4. **Transmission**: Sent via SimplifiedBluetoothService
/// 5. **Routing**: Relayed by intermediate nodes (TTL decrements) /// 5. **Routing**: Relayed by intermediate nodes (TTL decrements)
/// 6. **Reassembly**: Fragments collected and reassembled /// 6. **Reassembly**: Fragments collected and reassembled
/// 7. **Decoding**: Binary data parsed back to message objects /// 7. **Decoding**: Binary data parsed back to message objects
@@ -49,12 +49,12 @@
/// - **Fragment**: Multi-part message handling /// - **Fragment**: Multi-part message handling
/// - **Delivery/Read**: Message acknowledgments /// - **Delivery/Read**: Message acknowledgments
/// - **Noise**: Encrypted channel establishment /// - **Noise**: Encrypted channel establishment
/// - **Version**: Protocol compatibility negotiation /// - **Version**: Protocol version negotiation
/// ///
/// ## Future Extensions /// ## Future Extensions
/// The protocol is designed to be extensible: /// The protocol is designed to be extensible:
/// - Reserved message type ranges for future use /// - Reserved message type ranges for future use
/// - Version negotiation for backward compatibility /// - Version field for protocol evolution
/// - Optional fields for new features /// - Optional fields for new features
/// ///
@@ -95,21 +95,21 @@ struct MessagePadding {
guard !data.isEmpty else { return data } guard !data.isEmpty else { return data }
// Last byte tells us how much padding to remove // Last byte tells us how much padding to remove
let paddingLength = Int(data[data.count - 1]) let lastIndex = data.count - 1
guard lastIndex >= 0 else { return data }
let paddingLength = Int(data[lastIndex])
guard paddingLength > 0 && paddingLength <= data.count else { guard paddingLength > 0 && paddingLength <= data.count else {
// Debug logging for 243-byte packets // No valid padding, return original data
if data.count == 243 {
}
return data return data
} }
let result = data.prefix(data.count - paddingLength) // Create a new Data object (not a subsequence) for thread safety
let unpaddedLength = data.count - paddingLength
guard unpaddedLength >= 0 else { return Data() }
// Debug logging for 243-byte packets // Return a proper copy, not a subsequence
if data.count == 243 { return Data(data.prefix(unpaddedLength))
}
return result
} }
// Find optimal block size for data // Find optimal block size for data
@@ -132,57 +132,50 @@ struct MessagePadding {
// MARK: - Message Types // MARK: - Message Types
/// Defines all message types in the BitChat protocol. /// Simplified BitChat protocol message types.
/// Each type has a unique identifier for efficient binary encoding. /// Reduced from 24 types to just 6 essential ones.
/// Types are grouped by function: user messages, protocol control, encryption, etc. /// All private communication metadata (receipts, status) is embedded in noiseEncrypted payloads.
enum MessageType: UInt8 { enum MessageType: UInt8 {
case announce = 0x01 // Public messages (unencrypted)
case leave = 0x03 case announce = 0x01 // "I'm here" with nickname
case message = 0x04 // All user messages (private and broadcast) case message = 0x02 // Public chat message
case fragmentStart = 0x05 case leave = 0x03 // "I'm leaving"
case fragmentContinue = 0x06
case fragmentEnd = 0x07
case deliveryAck = 0x0A // Acknowledge message received
case deliveryStatusRequest = 0x0B // Request delivery status update
case readReceipt = 0x0C // Message has been read/viewed
// Noise Protocol messages // Noise encryption
case noiseHandshakeInit = 0x10 // Noise handshake initiation case noiseHandshake = 0x10 // Handshake (init or response determined by payload)
case noiseHandshakeResp = 0x11 // Noise handshake response case noiseEncrypted = 0x11 // All encrypted payloads (messages, receipts, etc.)
case noiseEncrypted = 0x12 // Noise encrypted transport message
case noiseIdentityAnnounce = 0x13 // Announce static public key for discovery
// Protocol-level acknowledgments // Fragmentation (simplified)
case protocolAck = 0x22 // Generic protocol acknowledgment case fragment = 0x20 // Single fragment type for large messages
case protocolNack = 0x23 // Negative acknowledgment (failure)
case systemValidation = 0x24 // Session validation ping
case handshakeRequest = 0x25 // Request handshake for pending messages
// Favorite system messages
case favorited = 0x30 // Peer favorited us
case unfavorited = 0x31 // Peer unfavorited us
var description: String { var description: String {
switch self { switch self {
case .announce: return "announce" case .announce: return "announce"
case .leave: return "leave"
case .message: return "message" case .message: return "message"
case .fragmentStart: return "fragmentStart" case .leave: return "leave"
case .fragmentContinue: return "fragmentContinue" case .noiseHandshake: return "noiseHandshake"
case .fragmentEnd: return "fragmentEnd"
case .deliveryAck: return "deliveryAck"
case .deliveryStatusRequest: return "deliveryStatusRequest"
case .readReceipt: return "readReceipt"
case .noiseHandshakeInit: return "noiseHandshakeInit"
case .noiseHandshakeResp: return "noiseHandshakeResp"
case .noiseEncrypted: return "noiseEncrypted" case .noiseEncrypted: return "noiseEncrypted"
case .noiseIdentityAnnounce: return "noiseIdentityAnnounce" case .fragment: return "fragment"
case .protocolAck: return "protocolAck" }
case .protocolNack: return "protocolNack" }
case .systemValidation: return "systemValidation" }
case .handshakeRequest: return "handshakeRequest"
case .favorited: return "favorited" // MARK: - Noise Payload Types
case .unfavorited: return "unfavorited"
/// Types of payloads embedded within noiseEncrypted messages.
/// The first byte of decrypted Noise payload indicates the type.
/// This provides privacy - observers can't distinguish message types.
enum NoisePayloadType: UInt8 {
// Messages and status
case privateMessage = 0x01 // Private chat message
case readReceipt = 0x02 // Message was read
case delivered = 0x03 // Message was delivered
var description: String {
switch self {
case .privateMessage: return "privateMessage"
case .readReceipt: return "readReceipt"
case .delivered: return "delivered"
} }
} }
} }
@@ -198,14 +191,8 @@ enum LazyHandshakeState {
case failed(Error) // Handshake failed case failed(Error) // Handshake failed
} }
// MARK: - Special Recipients // MARK: - Special Recipients (removed)
// Previously defined broadcast identifiers were unused; removed for simplicity.
/// Defines special recipient identifiers used in the protocol.
/// These magic values indicate broadcast or system-level recipients
/// rather than specific peer IDs.
struct SpecialRecipients {
static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast
}
// MARK: - Core Protocol Structures // MARK: - Core Protocol Structures
@@ -269,103 +256,8 @@ struct BitchatPacket: Codable {
} }
} }
// MARK: - Delivery Acknowledgments // MARK: - Delivery Acknowledgments (removed)
// Legacy DeliveryAck structures are no longer used; delivery status flows via Noise payloads.
/// Acknowledgment sent when a message is successfully delivered to a recipient.
/// Provides delivery confirmation for reliable messaging and UI feedback.
/// - Note: Only sent for direct messages, not broadcasts
struct DeliveryAck: Codable {
let originalMessageID: String
let ackID: String
let recipientID: String // Who received it
let recipientNickname: String
let timestamp: Date
let hopCount: UInt8 // How many hops to reach recipient
init(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UInt8) {
self.originalMessageID = originalMessageID
self.ackID = UUID().uuidString
self.recipientID = recipientID
self.recipientNickname = recipientNickname
self.timestamp = Date()
self.hopCount = hopCount
}
// For binary decoding
private init(originalMessageID: String, ackID: String, recipientID: String, recipientNickname: String, timestamp: Date, hopCount: UInt8) {
self.originalMessageID = originalMessageID
self.ackID = ackID
self.recipientID = recipientID
self.recipientNickname = recipientNickname
self.timestamp = timestamp
self.hopCount = hopCount
}
func encode() -> Data? {
try? JSONEncoder().encode(self)
}
static func decode(from data: Data) -> DeliveryAck? {
try? JSONDecoder().decode(DeliveryAck.self, from: data)
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
data.appendUUID(originalMessageID)
data.appendUUID(ackID)
// RecipientID as 8-byte hex string
var recipientData = Data()
var tempID = recipientID
while tempID.count >= 2 && recipientData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
recipientData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while recipientData.count < 8 {
recipientData.append(0)
}
data.append(recipientData)
data.appendUInt8(hopCount)
data.appendDate(timestamp)
data.appendString(recipientNickname)
return data
}
static func fromBinaryData(_ data: Data) -> DeliveryAck? {
// Create defensive copy
let dataCopy = Data(data)
// Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname
guard dataCopy.count >= 50 else { return nil }
var offset = 0
guard let originalMessageID = dataCopy.readUUID(at: &offset),
let ackID = dataCopy.readUUID(at: &offset) else { return nil }
guard let recipientIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let recipientID = recipientIDData.hexEncodedString()
guard InputValidator.validatePeerID(recipientID) else { return nil }
guard let hopCount = dataCopy.readUInt8(at: &offset),
InputValidator.validateHopCount(hopCount),
let timestamp = dataCopy.readDate(at: &offset),
InputValidator.validateTimestamp(timestamp),
let recipientNicknameRaw = dataCopy.readString(at: &offset),
let recipientNickname = InputValidator.validateNickname(recipientNicknameRaw) else { return nil }
return DeliveryAck(originalMessageID: originalMessageID,
ackID: ackID,
recipientID: recipientID,
recipientNickname: recipientNickname,
timestamp: timestamp,
hopCount: hopCount)
}
}
// MARK: - Read Receipts // MARK: - Read Receipts
@@ -456,460 +348,8 @@ struct ReadReceipt: Codable {
} }
} }
// MARK: - Handshake Requests
// Handshake request for pending messages // PeerIdentityBinding removed (unused).
struct HandshakeRequest: Codable {
let requestID: String
let requesterID: String // Who needs the handshake
let requesterNickname: String // Nickname of requester
let targetID: String // Who should initiate handshake
let pendingMessageCount: UInt8 // Number of messages queued
let timestamp: Date
init(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UInt8) {
self.requestID = UUID().uuidString
self.requesterID = requesterID
self.requesterNickname = requesterNickname
self.targetID = targetID
self.pendingMessageCount = pendingMessageCount
self.timestamp = Date()
}
// For binary decoding
private init(requestID: String, requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UInt8, timestamp: Date) {
self.requestID = requestID
self.requesterID = requesterID
self.requesterNickname = requesterNickname
self.targetID = targetID
self.pendingMessageCount = pendingMessageCount
self.timestamp = timestamp
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
data.appendUUID(requestID)
// RequesterID as 8-byte hex string
var requesterData = Data()
var tempID = requesterID
while tempID.count >= 2 && requesterData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
requesterData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while requesterData.count < 8 {
requesterData.append(0)
}
data.append(requesterData)
// TargetID as 8-byte hex string
var targetData = Data()
tempID = targetID
while tempID.count >= 2 && targetData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
targetData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while targetData.count < 8 {
targetData.append(0)
}
data.append(targetData)
data.appendUInt8(pendingMessageCount)
data.appendDate(timestamp)
data.appendString(requesterNickname)
return data
}
static func fromBinaryData(_ data: Data) -> HandshakeRequest? {
// Create defensive copy
let dataCopy = Data(data)
// Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname
guard dataCopy.count >= 42 else { return nil }
var offset = 0
guard let requestID = dataCopy.readUUID(at: &offset) else { return nil }
guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let requesterID = requesterIDData.hexEncodedString()
guard InputValidator.validatePeerID(requesterID) else { return nil }
guard let targetIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let targetID = targetIDData.hexEncodedString()
guard InputValidator.validatePeerID(targetID) else { return nil }
guard let pendingMessageCount = dataCopy.readUInt8(at: &offset),
let timestamp = dataCopy.readDate(at: &offset),
InputValidator.validateTimestamp(timestamp),
let requesterNicknameRaw = dataCopy.readString(at: &offset),
let requesterNickname = InputValidator.validateNickname(requesterNicknameRaw) else { return nil }
return HandshakeRequest(requestID: requestID,
requesterID: requesterID,
requesterNickname: requesterNickname,
targetID: targetID,
pendingMessageCount: pendingMessageCount,
timestamp: timestamp)
}
}
// MARK: - Protocol Acknowledgments
// Protocol-level acknowledgment for reliable delivery
struct ProtocolAck: Codable {
let originalPacketID: String // ID of the packet being acknowledged
let ackID: String // Unique ID for this ACK
let senderID: String // Who sent the original packet
let receiverID: String // Who received and is acknowledging
let packetType: UInt8 // Type of packet being acknowledged
let timestamp: Date // When ACK was generated
let hopCount: UInt8 // Hops taken to reach receiver
init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, hopCount: UInt8) {
self.originalPacketID = originalPacketID
self.ackID = UUID().uuidString
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = Date()
self.hopCount = hopCount
}
// Private init for binary decoding
private init(originalPacketID: String, ackID: String, senderID: String, receiverID: String,
packetType: UInt8, timestamp: Date, hopCount: UInt8) {
self.originalPacketID = originalPacketID
self.ackID = ackID
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = timestamp
self.hopCount = hopCount
}
func toBinaryData() -> Data {
var data = Data()
data.appendUUID(originalPacketID)
data.appendUUID(ackID)
// Sender and receiver IDs as 8-byte hex strings
data.append(Data(hexString: senderID) ?? Data(repeating: 0, count: 8))
data.append(Data(hexString: receiverID) ?? Data(repeating: 0, count: 8))
data.appendUInt8(packetType)
data.appendUInt8(hopCount)
data.appendDate(timestamp)
return data
}
static func fromBinaryData(_ data: Data) -> ProtocolAck? {
let dataCopy = Data(data)
guard dataCopy.count >= 50 else { return nil } // 2 UUIDs + 2 IDs + type + hop + timestamp
var offset = 0
guard let originalPacketID = dataCopy.readUUID(at: &offset),
let ackID = dataCopy.readUUID(at: &offset),
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let packetType = dataCopy.readUInt8(at: &offset),
InputValidator.validateMessageType(packetType),
let hopCount = dataCopy.readUInt8(at: &offset),
InputValidator.validateHopCount(hopCount),
let timestamp = dataCopy.readDate(at: &offset),
InputValidator.validateTimestamp(timestamp) else { return nil }
let senderID = senderIDData.hexEncodedString()
let receiverID = receiverIDData.hexEncodedString()
guard InputValidator.validatePeerID(senderID),
InputValidator.validatePeerID(receiverID) else { return nil }
return ProtocolAck(originalPacketID: originalPacketID,
ackID: ackID,
senderID: senderID,
receiverID: receiverID,
packetType: packetType,
timestamp: timestamp,
hopCount: hopCount)
}
}
// Protocol-level negative acknowledgment
struct ProtocolNack: Codable {
let originalPacketID: String // ID of the packet that failed
let nackID: String // Unique ID for this NACK
let senderID: String // Who sent the original packet
let receiverID: String // Who is reporting the failure
let packetType: UInt8 // Type of packet that failed
let timestamp: Date // When NACK was generated
let reason: String // Reason for failure
let errorCode: UInt8 // Numeric error code
// Error codes
enum ErrorCode: UInt8 {
case unknown = 0
case checksumFailed = 1
case decryptionFailed = 2
case malformedPacket = 3
case unsupportedVersion = 4
case resourceExhausted = 5
case routingFailed = 6
case sessionExpired = 7
}
init(originalPacketID: String, senderID: String, receiverID: String,
packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) {
self.originalPacketID = originalPacketID
self.nackID = UUID().uuidString
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = Date()
self.reason = reason
self.errorCode = errorCode.rawValue
}
// Private init for binary decoding
private init(originalPacketID: String, nackID: String, senderID: String, receiverID: String,
packetType: UInt8, timestamp: Date, reason: String, errorCode: UInt8) {
self.originalPacketID = originalPacketID
self.nackID = nackID
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = timestamp
self.reason = reason
self.errorCode = errorCode
}
func toBinaryData() -> Data {
var data = Data()
data.appendUUID(originalPacketID)
data.appendUUID(nackID)
// Sender and receiver IDs as 8-byte hex strings
data.append(Data(hexString: senderID) ?? Data(repeating: 0, count: 8))
data.append(Data(hexString: receiverID) ?? Data(repeating: 0, count: 8))
data.appendUInt8(packetType)
data.appendUInt8(errorCode)
data.appendDate(timestamp)
data.appendString(reason)
return data
}
static func fromBinaryData(_ data: Data) -> ProtocolNack? {
let dataCopy = Data(data)
guard dataCopy.count >= 52 else { return nil } // Minimum size
var offset = 0
guard let originalPacketID = dataCopy.readUUID(at: &offset),
let nackID = dataCopy.readUUID(at: &offset),
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let packetType = dataCopy.readUInt8(at: &offset),
InputValidator.validateMessageType(packetType),
let errorCode = dataCopy.readUInt8(at: &offset),
let timestamp = dataCopy.readDate(at: &offset),
InputValidator.validateTimestamp(timestamp),
let reasonRaw = dataCopy.readString(at: &offset),
let reason = InputValidator.validateReasonString(reasonRaw) else { return nil }
let senderID = senderIDData.hexEncodedString()
let receiverID = receiverIDData.hexEncodedString()
guard InputValidator.validatePeerID(senderID),
InputValidator.validatePeerID(receiverID) else { return nil }
return ProtocolNack(originalPacketID: originalPacketID,
nackID: nackID,
senderID: senderID,
receiverID: receiverID,
packetType: packetType,
timestamp: timestamp,
reason: reason,
errorCode: errorCode)
}
}
// MARK: - Peer Identity Rotation
/// Announces a peer's cryptographic identity to enable secure communication.
/// Contains the peer's Noise static public key and supports identity rotation
/// by binding ephemeral peer IDs to stable cryptographic fingerprints.
/// - Note: Critical for establishing end-to-end encrypted channels
struct NoiseIdentityAnnouncement: Codable {
let peerID: String // Current ephemeral peer ID
let publicKey: Data // Noise static public key
let signingPublicKey: Data // Ed25519 signing public key
let nickname: String // Current nickname
let timestamp: Date // When this binding was created
let previousPeerID: String? // Previous peer ID (for smooth transition)
let signature: Data // Signature proving ownership
init(peerID: String, publicKey: Data, signingPublicKey: Data, nickname: String, timestamp: Date, previousPeerID: String? = nil, signature: Data) {
self.peerID = peerID
self.publicKey = publicKey
self.signingPublicKey = signingPublicKey
// Trim whitespace from nickname
self.nickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
self.timestamp = timestamp
self.previousPeerID = previousPeerID
self.signature = signature
}
// Custom decoder to ensure nickname is trimmed
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.peerID = try container.decode(String.self, forKey: .peerID)
self.publicKey = try container.decode(Data.self, forKey: .publicKey)
self.signingPublicKey = try container.decode(Data.self, forKey: .signingPublicKey)
// Trim whitespace from decoded nickname
let rawNickname = try container.decode(String.self, forKey: .nickname)
self.nickname = rawNickname.trimmingCharacters(in: .whitespacesAndNewlines)
self.timestamp = try container.decode(Date.self, forKey: .timestamp)
self.previousPeerID = try container.decodeIfPresent(String.self, forKey: .previousPeerID)
self.signature = try container.decode(Data.self, forKey: .signature)
}
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
return try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data)
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
// Flags byte: bit 0 = hasPreviousPeerID
var flags: UInt8 = 0
if previousPeerID != nil { flags |= 0x01 }
data.appendUInt8(flags)
// PeerID as 8-byte hex string
var peerData = Data()
var tempID = peerID
while tempID.count >= 2 && peerData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
peerData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while peerData.count < 8 {
peerData.append(0)
}
data.append(peerData)
data.appendData(publicKey)
data.appendData(signingPublicKey)
data.appendString(nickname)
data.appendDate(timestamp)
if let previousPeerID = previousPeerID {
// Previous PeerID as 8-byte hex string
var prevData = Data()
var tempPrevID = previousPeerID
while tempPrevID.count >= 2 && prevData.count < 8 {
let hexByte = String(tempPrevID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
prevData.append(byte)
}
tempPrevID = String(tempPrevID.dropFirst(2))
}
while prevData.count < 8 {
prevData.append(0)
}
data.append(prevData)
}
data.appendData(signature)
return data
}
static func fromBinaryData(_ data: Data) -> NoiseIdentityAnnouncement? {
// Create defensive copy
let dataCopy = Data(data)
// Minimum size check: flags(1) + peerID(8) + min data lengths
guard dataCopy.count >= 20 else { return nil }
var offset = 0
guard let flags = dataCopy.readUInt8(at: &offset) else { return nil }
let hasPreviousPeerID = (flags & 0x01) != 0
// Read peerID using safe method
guard let peerIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let peerID = peerIDBytes.hexEncodedString()
guard InputValidator.validatePeerID(peerID) else { return nil }
guard let publicKey = dataCopy.readData(at: &offset),
InputValidator.validatePublicKey(publicKey),
let signingPublicKey = dataCopy.readData(at: &offset),
InputValidator.validatePublicKey(signingPublicKey),
let rawNickname = dataCopy.readString(at: &offset),
let nickname = InputValidator.validateNickname(rawNickname),
let timestamp = dataCopy.readDate(at: &offset),
InputValidator.validateTimestamp(timestamp) else { return nil }
var previousPeerID: String? = nil
if hasPreviousPeerID {
// Read previousPeerID using safe method
guard let prevIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let prevID = prevIDBytes.hexEncodedString()
guard InputValidator.validatePeerID(prevID) else { return nil }
previousPeerID = prevID
}
guard let signature = dataCopy.readData(at: &offset),
InputValidator.validateSignature(signature) else { return nil }
return NoiseIdentityAnnouncement(peerID: peerID,
publicKey: publicKey,
signingPublicKey: signingPublicKey,
nickname: nickname,
timestamp: timestamp,
previousPeerID: previousPeerID,
signature: signature)
}
}
// Binding between ephemeral peer ID and cryptographic identity
struct PeerIdentityBinding {
let currentPeerID: String // Current ephemeral ID
let fingerprint: String // Permanent cryptographic identity
let publicKey: Data // Noise static public key
let signingPublicKey: Data // Ed25519 signing public key
let nickname: String // Last known nickname
let bindingTimestamp: Date // When this binding was created
let signature: Data // Cryptographic proof of binding
// Verify the binding signature
func verify() -> Bool {
let bindingData = currentPeerID.data(using: .utf8)! + publicKey +
String(Int64(bindingTimestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
do {
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingPublicKey)
return signingKey.isValidSignature(signature, for: bindingData)
} catch {
return false
}
}
}
// MARK: - Delivery Status // MARK: - Delivery Status
@@ -1020,13 +460,7 @@ protocol BitchatDelegate: AnyObject {
// Optional method to check if a fingerprint belongs to a favorite peer // Optional method to check if a fingerprint belongs to a favorite peer
func isFavorite(fingerprint: String) -> Bool func isFavorite(fingerprint: String) -> Bool
// Delivery confirmation methods
func didReceiveDeliveryAck(_ ack: DeliveryAck)
func didReceiveReadReceipt(_ receipt: ReadReceipt)
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
// Peer availability tracking
func peerAvailabilityChanged(_ peerID: String, available: Bool)
} }
// Provide default implementation to make it effectively optional // Provide default implementation to make it effectively optional
@@ -1035,19 +469,41 @@ extension BitchatDelegate {
return false return false
} }
func didReceiveDeliveryAck(_ ack: DeliveryAck) {
// Default empty implementation
}
func didReceiveReadReceipt(_ receipt: ReadReceipt) {
// Default empty implementation
}
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) { func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
// Default empty implementation // Default empty implementation
} }
}
func peerAvailabilityChanged(_ peerID: String, available: Bool) { // MARK: - Noise Payload Helpers
// Default empty implementation
/// Helper to create typed Noise payloads
struct NoisePayload {
let type: NoisePayloadType
let data: Data
/// Encode payload with type prefix
func encode() -> Data {
var encoded = Data()
encoded.append(type.rawValue)
encoded.append(data)
return encoded
}
/// Decode payload from data
static func decode(_ data: Data) -> NoisePayload? {
// Ensure we have at least 1 byte for the type
guard !data.isEmpty else {
return nil
}
// Safely get the first byte
let firstByte = data[data.startIndex]
guard let type = NoisePayloadType(rawValue: firstByte) else {
return nil
}
// Create a proper Data copy (not a subsequence) for thread safety
let payloadData = data.count > 1 ? Data(data.dropFirst()) : Data()
return NoisePayload(type: type, data: payloadData)
} }
} }
+104
View File
@@ -0,0 +1,104 @@
//
// AutocompleteService.swift
// bitchat
//
// Handles autocomplete suggestions for mentions and commands
// This is free and unencumbered software released into the public domain.
//
import Foundation
/// Manages autocomplete functionality for chat
class AutocompleteService {
private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: [])
private let commandRegex = try? NSRegularExpression(pattern: "^/([a-z]*)$", options: [])
private let commands = [
"/msg", "/who", "/clear", "/help",
"/hug", "/slap", "/fav", "/unfav",
"/block", "/unblock"
]
/// Get autocomplete suggestions for current text
func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) {
let textToPosition = String(text.prefix(cursorPosition))
// Check for mention autocomplete
if let (mentionSuggestions, mentionRange) = getMentionSuggestions(textToPosition, peers: peers) {
return (mentionSuggestions, mentionRange)
}
// Don't handle command autocomplete here - ContentView handles it with better UI
// if let (commandSuggestions, commandRange) = getCommandSuggestions(textToPosition) {
// return (commandSuggestions, commandRange)
// }
return ([], nil)
}
/// Apply selected suggestion to text
func applySuggestion(_ suggestion: String, to text: String, range: NSRange) -> String {
guard let textRange = Range(range, in: text) else { return text }
var replacement = suggestion
// Add space after command if it takes arguments
if suggestion.hasPrefix("/") && needsArgument(command: suggestion) {
replacement += " "
}
return text.replacingCharacters(in: textRange, with: replacement)
}
// MARK: - Private Methods
private func getMentionSuggestions(_ text: String, peers: [String]) -> ([String], NSRange)? {
guard let regex = mentionRegex else { return nil }
let nsText = text as NSString
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
guard let match = matches.last else { return nil }
let fullRange = match.range(at: 0)
let captureRange = match.range(at: 1)
let prefix = nsText.substring(with: captureRange).lowercased()
let suggestions = peers
.filter { $0.lowercased().hasPrefix(prefix) }
.sorted()
.prefix(5)
.map { "@\($0)" }
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
}
private func getCommandSuggestions(_ text: String) -> ([String], NSRange)? {
guard let regex = commandRegex else { return nil }
let nsText = text as NSString
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
guard let match = matches.last else { return nil }
let fullRange = match.range(at: 0)
let captureRange = match.range(at: 1)
let prefix = nsText.substring(with: captureRange).lowercased()
let suggestions = commands
.filter { $0.hasPrefix("/\(prefix)") }
.sorted()
.prefix(5)
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
}
private func needsArgument(command: String) -> Bool {
switch command {
case "/who", "/clear", "/help":
return false
default:
return true
}
}
}
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
//
// CommandProcessor.swift
// bitchat
//
// Handles command parsing and execution for BitChat
// This is free and unencumbered software released into the public domain.
//
import Foundation
/// Result of command processing
enum CommandResult {
case success(message: String?)
case error(message: String)
case handled // Command handled, no message needed
}
/// Processes chat commands in a focused, efficient way
@MainActor
class CommandProcessor {
weak var chatViewModel: ChatViewModel?
weak var meshService: SimplifiedBluetoothService?
init(chatViewModel: ChatViewModel? = nil, meshService: SimplifiedBluetoothService? = nil) {
self.chatViewModel = chatViewModel
self.meshService = meshService
}
/// Process a command string
@MainActor
func process(_ command: String) -> CommandResult {
let parts = command.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false)
guard let cmd = parts.first else { return .error(message: "Invalid command") }
let args = parts.count > 1 ? String(parts[1]) : ""
switch cmd {
case "/m", "/msg":
return handleMessage(args)
case "/w", "/who":
return handleWho()
case "/clear":
return handleClear()
case "/hug":
return handleEmote(args, action: "hugs", emoji: "🫂")
case "/slap":
return handleEmote(args, action: "slaps", emoji: "🐟", suffix: " around a bit with a large trout")
case "/block":
return handleBlock(args)
case "/unblock":
return handleUnblock(args)
case "/fav":
return handleFavorite(args, add: true)
case "/unfav":
return handleFavorite(args, add: false)
case "/help", "/h":
return handleHelp()
default:
return .error(message: "unknown command: \(cmd)")
}
}
// MARK: - Command Handlers
private func handleMessage(_ args: String) -> CommandResult {
let parts = args.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false)
guard !parts.isEmpty else {
return .error(message: "usage: /msg @nickname [message]")
}
let targetName = String(parts[0])
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname) else {
return .error(message: "'\(nickname)' not found")
}
chatViewModel?.startPrivateChat(with: peerID)
if parts.count > 1 {
let message = String(parts[1])
chatViewModel?.sendPrivateMessage(message, to: peerID)
}
return .success(message: "started private chat with \(nickname)")
}
private func handleWho() -> CommandResult {
guard let peers = meshService?.getPeerNicknames(), !peers.isEmpty else {
return .success(message: "no one else is online right now")
}
let onlineList = peers.values.sorted().joined(separator: ", ")
return .success(message: "online: \(onlineList)")
}
private func handleClear() -> CommandResult {
if let peerID = chatViewModel?.selectedPrivateChatPeer {
chatViewModel?.privateChats[peerID]?.removeAll()
} else {
chatViewModel?.messages.removeAll()
}
return .handled
}
private func handleEmote(_ args: String, action: String, emoji: String, suffix: String = "") -> CommandResult {
let targetName = args.trimmingCharacters(in: .whitespaces)
guard !targetName.isEmpty else {
return .error(message: "usage: /\(action) <nickname>")
}
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let targetPeerID = chatViewModel?.getPeerIDForNickname(nickname),
let myNickname = chatViewModel?.nickname else {
return .error(message: "cannot \(action) \(nickname): not found")
}
let emoteContent = "* \(emoji) \(myNickname) \(action) \(nickname)\(suffix) *"
if chatViewModel?.selectedPrivateChatPeer != nil {
// In private chat
if let peerNickname = meshService?.getPeerNicknames()[targetPeerID] {
let personalMessage = "* \(emoji) \(myNickname) \(action) you\(suffix) *"
meshService?.sendPrivateMessage(personalMessage, to: targetPeerID,
recipientNickname: peerNickname,
messageID: UUID().uuidString)
}
} else {
// In public chat
meshService?.sendMessage(emoteContent)
}
return .handled
}
private func handleBlock(_ args: String) -> CommandResult {
let targetName = args.trimmingCharacters(in: .whitespaces)
if targetName.isEmpty {
// List blocked users
guard let blockedUsers = chatViewModel?.blockedUsers, !blockedUsers.isEmpty else {
return .success(message: "no blocked peers")
}
var blockedNicknames: [String] = []
if let peers = meshService?.getPeerNicknames() {
for (peerID, nickname) in peers {
if let fingerprint = meshService?.getPeerFingerprint(peerID),
blockedUsers.contains(fingerprint) {
blockedNicknames.append(nickname)
}
}
}
let list = blockedNicknames.isEmpty ? "blocked peers (not currently online)"
: blockedNicknames.sorted().joined(separator: ", ")
return .success(message: "blocked peers: \(list)")
}
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let fingerprint = meshService?.getPeerFingerprint(peerID) else {
return .error(message: "cannot block \(nickname): not found or unable to verify identity")
}
if SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
return .success(message: "\(nickname) is already blocked")
}
// Block the user
if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
identity.isBlocked = true
identity.isFavorite = false
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
} else {
let blockedIdentity = SocialIdentity(
fingerprint: fingerprint,
localPetname: nil,
claimedNickname: nickname,
trustLevel: .unknown,
isFavorite: false,
isBlocked: true,
notes: nil
)
SecureIdentityStateManager.shared.updateSocialIdentity(blockedIdentity)
}
// The peerStateManager and SecureIdentityStateManager handle the blocking state
return .success(message: "blocked \(nickname). you will no longer receive messages from them")
}
private func handleUnblock(_ args: String) -> CommandResult {
let targetName = args.trimmingCharacters(in: .whitespaces)
guard !targetName.isEmpty else {
return .error(message: "usage: /unblock <nickname>")
}
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let fingerprint = meshService?.getPeerFingerprint(peerID) else {
return .error(message: "cannot unblock \(nickname): not found")
}
if !SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
return .success(message: "\(nickname) is not blocked")
}
SecureIdentityStateManager.shared.setBlocked(fingerprint, isBlocked: false)
// The SecureIdentityStateManager handles the unblocking state
return .success(message: "unblocked \(nickname)")
}
private func handleFavorite(_ args: String, add: Bool) -> CommandResult {
let targetName = args.trimmingCharacters(in: .whitespaces)
guard !targetName.isEmpty else {
return .error(message: "usage: /\(add ? "fav" : "unfav") <nickname>")
}
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let noisePublicKey = Data(hexString: peerID) else {
return .error(message: "can't find peer: \(nickname)")
}
if add {
let existingFavorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey)
FavoritesPersistenceService.shared.addFavorite(
peerNoisePublicKey: noisePublicKey,
peerNostrPublicKey: existingFavorite?.peerNostrPublicKey,
peerNickname: nickname
)
chatViewModel?.toggleFavorite(peerID: peerID)
chatViewModel?.sendFavoriteNotification(to: peerID, isFavorite: true)
return .success(message: "added \(nickname) to favorites")
} else {
FavoritesPersistenceService.shared.removeFavorite(peerNoisePublicKey: noisePublicKey)
chatViewModel?.toggleFavorite(peerID: peerID)
chatViewModel?.sendFavoriteNotification(to: peerID, isFavorite: false)
return .success(message: "removed \(nickname) from favorites")
}
}
private func handleHelp() -> CommandResult {
let helpText = """
commands:
/msg @name - start private chat
/who - list who's online
/clear - clear messages
/hug @name - send a hug
/slap @name - slap with a trout
/fav @name - add to favorites
/unfav @name - remove from favorites
/block @name - block
/unblock @name - unblock
"""
return .success(message: helpText)
}
}
-294
View File
@@ -1,294 +0,0 @@
//
// DeliveryTracker.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Combine
class DeliveryTracker {
static let shared = DeliveryTracker()
// Track pending deliveries
private var pendingDeliveries: [String: PendingDelivery] = [:]
private let pendingLock = NSLock()
// Track received ACKs to prevent duplicates
private var receivedAckIDs = Set<String>()
private var sentAckIDs = Set<String>()
// Timeout configuration
private let privateMessageTimeout: TimeInterval = 120 // 2 minutes
private let roomMessageTimeout: TimeInterval = 180 // 3 minutes
private let favoriteTimeout: TimeInterval = 600 // 10 minutes for favorites
// Retry configuration
private let maxRetries = 3
private let retryDelay: TimeInterval = 5 // Base retry delay
// Publishers for UI updates
let deliveryStatusUpdated = PassthroughSubject<(messageID: String, status: DeliveryStatus), Never>()
// Cleanup timer
private var cleanupTimer: Timer?
struct PendingDelivery {
let messageID: String
let sentAt: Date
let recipientID: String
let recipientNickname: String
let retryCount: Int
let isFavorite: Bool
var timeoutTimer: Timer?
var isTimedOut: Bool {
let timeout: TimeInterval = isFavorite ? 300 : 30
return Date().timeIntervalSince(sentAt) > timeout
}
var shouldRetry: Bool {
return retryCount < 3 && isFavorite
}
}
private init() {
startCleanupTimer()
}
deinit {
cleanupTimer?.invalidate()
}
// MARK: - Public Methods
func trackMessage(_ message: BitchatMessage, recipientID: String, recipientNickname: String, isFavorite: Bool = false, expectedRecipients: Int = 1) {
// Only track private messages
guard message.isPrivate else { return }
SecureLogger.log("Tracking message \(message.id) - private: \(message.isPrivate), recipient: \(recipientNickname)", category: SecureLogger.session, level: .info)
let delivery = PendingDelivery(
messageID: message.id,
sentAt: Date(),
recipientID: recipientID,
recipientNickname: recipientNickname,
retryCount: 0,
isFavorite: isFavorite,
timeoutTimer: nil
)
// Store the delivery with lock
pendingLock.lock()
pendingDeliveries[message.id] = delivery
pendingLock.unlock()
// Update status to sent (only if not already delivered)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
self.pendingLock.lock()
let stillPending = self.pendingDeliveries[message.id] != nil
self.pendingLock.unlock()
// Only update to sent if still pending (not already delivered)
if stillPending {
SecureLogger.log("Updating message \(message.id) to sent status (still pending)", category: SecureLogger.session, level: .debug)
self.updateDeliveryStatus(message.id, status: .sent)
} else {
SecureLogger.log("Skipping sent status update for \(message.id) - already delivered", category: SecureLogger.session, level: .debug)
}
}
// Schedule timeout (outside of lock)
scheduleTimeout(for: message.id)
}
func processDeliveryAck(_ ack: DeliveryAck) {
pendingLock.lock()
defer { pendingLock.unlock() }
SecureLogger.log("Processing delivery ACK for message \(ack.originalMessageID) from \(ack.recipientNickname)", category: SecureLogger.session, level: .info)
// Prevent duplicate ACK processing
guard !receivedAckIDs.contains(ack.ackID) else {
SecureLogger.log("Duplicate ACK \(ack.ackID) - ignoring", category: SecureLogger.session, level: .warning)
return
}
receivedAckIDs.insert(ack.ackID)
// Find the pending delivery
guard let delivery = pendingDeliveries[ack.originalMessageID] else {
// Message might have already been delivered or timed out
SecureLogger.log("No pending delivery found for message \(ack.originalMessageID)", category: SecureLogger.session, level: .warning)
return
}
// Cancel timeout timer
delivery.timeoutTimer?.invalidate()
// Direct message - mark as delivered
SecureLogger.log("Marking private message \(ack.originalMessageID) as delivered to \(ack.recipientNickname)", category: SecureLogger.session, level: .info)
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: Date()))
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
}
func generateAck(for message: BitchatMessage, myPeerID: String, myNickname: String, hopCount: UInt8) -> DeliveryAck? {
// Don't ACK our own messages
guard message.senderPeerID != myPeerID else {
return nil
}
// Only ACK private messages
guard message.isPrivate else {
return nil
}
// Don't ACK if we've already sent an ACK for this message
guard !sentAckIDs.contains(message.id) else {
return nil
}
sentAckIDs.insert(message.id)
return DeliveryAck(
originalMessageID: message.id,
recipientID: myPeerID,
recipientNickname: myNickname,
hopCount: hopCount
)
}
func clearDeliveryStatus(for messageID: String) {
pendingLock.lock()
defer { pendingLock.unlock() }
if let delivery = pendingDeliveries[messageID] {
delivery.timeoutTimer?.invalidate()
}
pendingDeliveries.removeValue(forKey: messageID)
}
// MARK: - Private Methods
private func updateDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
SecureLogger.log("Updating delivery status for message \(messageID): \(status)", category: SecureLogger.session, level: .debug)
DispatchQueue.main.async { [weak self] in
self?.deliveryStatusUpdated.send((messageID: messageID, status: status))
}
}
private func scheduleTimeout(for messageID: String) {
// Get delivery info with lock
pendingLock.lock()
guard let delivery = pendingDeliveries[messageID] else {
pendingLock.unlock()
return
}
let isFavorite = delivery.isFavorite
pendingLock.unlock()
let timeout = isFavorite ? favoriteTimeout : privateMessageTimeout
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
self?.handleTimeout(messageID: messageID)
}
pendingLock.lock()
if var updatedDelivery = pendingDeliveries[messageID] {
updatedDelivery.timeoutTimer = timer
pendingDeliveries[messageID] = updatedDelivery
}
pendingLock.unlock()
}
private func handleTimeout(messageID: String) {
pendingLock.lock()
guard let delivery = pendingDeliveries[messageID] else {
pendingLock.unlock()
return
}
let shouldRetry = delivery.shouldRetry
if shouldRetry {
pendingLock.unlock()
// Retry for favorites (outside of lock)
retryDelivery(messageID: messageID)
} else {
// Mark as failed
let reason = "Message not delivered"
pendingDeliveries.removeValue(forKey: messageID)
pendingLock.unlock()
updateDeliveryStatus(messageID, status: .failed(reason: reason))
}
}
private func retryDelivery(messageID: String) {
pendingLock.lock()
guard let delivery = pendingDeliveries[messageID] else {
pendingLock.unlock()
return
}
// Increment retry count
let newDelivery = PendingDelivery(
messageID: delivery.messageID,
sentAt: delivery.sentAt,
recipientID: delivery.recipientID,
recipientNickname: delivery.recipientNickname,
retryCount: delivery.retryCount + 1,
isFavorite: delivery.isFavorite,
timeoutTimer: nil
)
pendingDeliveries[messageID] = newDelivery
let retryCount = delivery.retryCount
pendingLock.unlock()
// Exponential backoff for retry
let delay = retryDelay * pow(2, Double(retryCount))
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
// Trigger resend through delegate or notification
NotificationCenter.default.post(
name: Notification.Name("bitchat.retryMessage"),
object: nil,
userInfo: ["messageID": messageID]
)
// Schedule new timeout
self?.scheduleTimeout(for: messageID)
}
}
private func startCleanupTimer() {
cleanupTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
self?.cleanupOldDeliveries()
}
}
private func cleanupOldDeliveries() {
pendingLock.lock()
defer { pendingLock.unlock() }
let now = Date()
let maxAge: TimeInterval = 3600 // 1 hour
// Clean up old pending deliveries
pendingDeliveries = pendingDeliveries.filter { (_, delivery) in
now.timeIntervalSince(delivery.sentAt) < maxAge
}
// Clean up old ACK IDs (keep last 1000)
if receivedAckIDs.count > 1000 {
receivedAckIDs.removeAll()
}
if sentAckIDs.count > 1000 {
sentAckIDs.removeAll()
}
}
}
-212
View File
@@ -1,212 +0,0 @@
//
// MessageRetryService.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Combine
import CryptoKit
struct RetryableMessage {
let id: String
let originalMessageID: String?
let originalTimestamp: Date?
let content: String
let mentions: [String]?
let isPrivate: Bool
let recipientPeerID: String?
let recipientNickname: String?
let retryCount: Int
let maxRetries: Int = 3
let nextRetryTime: Date
}
class MessageRetryService {
static let shared = MessageRetryService()
private var retryQueue: [RetryableMessage] = []
private var retryTimer: Timer?
private let retryInterval: TimeInterval = 2.0 // Retry every 2 seconds for faster sync
private let maxQueueSize = 50
weak var meshService: BluetoothMeshService?
private init() {
startRetryTimer()
}
deinit {
retryTimer?.invalidate()
}
private func startRetryTimer() {
retryTimer = Timer.scheduledTimer(withTimeInterval: retryInterval, repeats: true) { [weak self] _ in
self?.processRetryQueue()
}
}
func addMessageForRetry(
content: String,
mentions: [String]? = nil,
isPrivate: Bool = false,
recipientPeerID: String? = nil,
recipientNickname: String? = nil,
originalMessageID: String? = nil,
originalTimestamp: Date? = nil
) {
// Don't queue empty or whitespace-only messages
guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return
}
// Don't queue if we're at capacity
guard retryQueue.count < maxQueueSize else {
return
}
// Check if this message is already in the queue
if let messageID = originalMessageID {
let alreadyQueued = retryQueue.contains { msg in
msg.originalMessageID == messageID
}
if alreadyQueued {
return // Don't add duplicate
}
}
let retryMessage = RetryableMessage(
id: UUID().uuidString,
originalMessageID: originalMessageID,
originalTimestamp: originalTimestamp,
content: content,
mentions: mentions,
isPrivate: isPrivate,
recipientPeerID: recipientPeerID,
recipientNickname: recipientNickname,
retryCount: 0,
nextRetryTime: Date().addingTimeInterval(retryInterval)
)
retryQueue.append(retryMessage)
// Sort the queue by original timestamp to maintain message order
retryQueue.sort { (msg1, msg2) in
let time1 = msg1.originalTimestamp ?? Date.distantPast
let time2 = msg2.originalTimestamp ?? Date.distantPast
return time1 < time2
}
}
private func processRetryQueue() {
guard meshService != nil else { return }
let now = Date()
var messagesToRetry: [RetryableMessage] = []
var updatedQueue: [RetryableMessage] = []
for message in retryQueue {
if message.nextRetryTime <= now {
messagesToRetry.append(message)
} else {
updatedQueue.append(message)
}
}
retryQueue = updatedQueue
// Sort messages by original timestamp to maintain order
messagesToRetry.sort { (msg1, msg2) in
let time1 = msg1.originalTimestamp ?? Date.distantPast
let time2 = msg2.originalTimestamp ?? Date.distantPast
return time1 < time2
}
// Send messages with delay to maintain order
for (index, message) in messagesToRetry.enumerated() {
// Check if we should still retry
if message.retryCount >= message.maxRetries {
continue
}
// Add delay between messages to ensure proper ordering
let delay = Double(index) * 0.05 // 50ms between messages
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self = self,
let meshService = self.meshService else { return }
// Check connectivity before retrying
let viewModel = meshService.delegate as? ChatViewModel
let connectedPeers = viewModel?.connectedPeers ?? []
if message.isPrivate {
// For private messages, check if recipient is connected
if let recipientID = message.recipientPeerID,
connectedPeers.contains(recipientID) {
// Retry private message
meshService.sendPrivateMessage(
message.content,
to: recipientID,
recipientNickname: message.recipientNickname ?? "unknown",
messageID: message.originalMessageID
)
} else {
// Recipient not connected, keep in queue with updated retry time
var updatedMessage = message
updatedMessage = RetryableMessage(
id: message.id,
originalMessageID: message.originalMessageID,
originalTimestamp: message.originalTimestamp,
content: message.content,
mentions: message.mentions,
isPrivate: message.isPrivate,
recipientPeerID: message.recipientPeerID,
recipientNickname: message.recipientNickname,
retryCount: message.retryCount + 1,
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
)
self.retryQueue.append(updatedMessage)
}
} else {
// Regular message
if !connectedPeers.isEmpty {
meshService.sendMessage(
message.content,
mentions: message.mentions ?? [],
to: nil,
messageID: message.originalMessageID,
timestamp: message.originalTimestamp
)
} else {
// No peers connected, keep in queue
var updatedMessage = message
updatedMessage = RetryableMessage(
id: message.id,
originalMessageID: message.originalMessageID,
originalTimestamp: message.originalTimestamp,
content: message.content,
mentions: message.mentions,
isPrivate: message.isPrivate,
recipientPeerID: message.recipientPeerID,
recipientNickname: message.recipientNickname,
retryCount: message.retryCount + 1,
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
)
self.retryQueue.append(updatedMessage)
}
}
}
}
}
func clearRetryQueue() {
retryQueue.removeAll()
}
func getRetryQueueCount() -> Int {
return retryQueue.count
}
}
-655
View File
@@ -1,655 +0,0 @@
import Foundation
import Combine
/// Routes messages through the appropriate transport (Bluetooth mesh or Nostr)
@MainActor
class MessageRouter: ObservableObject {
enum Transport {
case bluetoothMesh
case nostr
}
enum DeliveryStatus {
case pending
case sent
case delivered
case failed(Error)
}
struct RoutedMessage {
let id: String
let content: String
let recipientNoisePublicKey: Data
let transport: Transport
let timestamp: Date
var status: DeliveryStatus
}
@Published private(set) var pendingMessages: [String: RoutedMessage] = [:]
private let meshService: BluetoothMeshService
private let nostrRelay: NostrRelayManager
private let favoritesService: FavoritesPersistenceService
private let processedMessagesService = ProcessedMessagesService.shared
private var cancellables = Set<AnyCancellable>()
private let messageDeduplication = LRUCache<String, Date>(maxSize: 1000)
init(
meshService: BluetoothMeshService,
nostrRelay: NostrRelayManager
) {
self.meshService = meshService
self.nostrRelay = nostrRelay
self.favoritesService = FavoritesPersistenceService.shared
setupBindings()
}
/// Send a message to a peer, automatically selecting the best transport
func sendMessage(
_ content: String,
to recipientNoisePublicKey: Data,
preferredTransport: Transport? = nil,
messageId: String? = nil
) async throws {
let finalMessageId = messageId ?? UUID().uuidString
// Check if peer is available on mesh (actually connected, not just known)
let recipientHexID = recipientNoisePublicKey.hexEncodedString()
let peerAvailableOnMesh = meshService.isPeerConnected(recipientHexID)
// Check if this is a mutual favorite
let isMutualFavorite = favoritesService.isMutualFavorite(recipientNoisePublicKey)
// Determine transport
let transport: Transport
if let preferred = preferredTransport {
transport = preferred
} else if peerAvailableOnMesh {
// Always prefer mesh when available
transport = .bluetoothMesh
} else if isMutualFavorite {
// Use Nostr for mutual favorites when not on mesh
transport = .nostr
} else {
throw MessageRouterError.peerNotReachable
}
// Create routed message
let routedMessage = RoutedMessage(
id: finalMessageId,
content: content,
recipientNoisePublicKey: recipientNoisePublicKey,
transport: transport,
timestamp: Date(),
status: .pending
)
pendingMessages[finalMessageId] = routedMessage
// Route based on transport
switch transport {
case .bluetoothMesh:
try await sendViaMesh(routedMessage)
case .nostr:
try await sendViaNostr(routedMessage)
}
}
/// Send a favorite/unfavorite notification
func sendFavoriteNotification(
to recipientNoisePublicKey: Data,
isFavorite: Bool
) async throws {
// messageType is used for logging below
// let messageType: MessageType = isFavorite ? .favorited : .unfavorited
let recipientHexID = recipientNoisePublicKey.hexEncodedString()
let action = isFavorite ? "favorite" : "unfavorite"
// Try mesh first
if meshService.getPeerNicknames()[recipientHexID] != nil {
// Send via mesh as a system message
meshService.sendFavoriteNotification(to: recipientHexID, isFavorite: isFavorite)
} else if let favoriteStatus = favoritesService.getFavoriteStatus(for: recipientNoisePublicKey),
let recipientNostrPubkey = favoriteStatus.peerNostrPublicKey {
// Send via Nostr as a special message
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
throw MessageRouterError.noNostrIdentity
}
// Include our npub in the content
let content = isFavorite ? "FAVORITED:\(senderIdentity.npub)" : "UNFAVORITED:\(senderIdentity.npub)"
let event = try NostrProtocol.createPrivateMessage(
content: content,
recipientPubkey: recipientNostrPubkey,
senderIdentity: senderIdentity
)
nostrRelay.sendEvent(event)
} else {
SecureLogger.log("⚠️ Cannot send \(action) notification - peer not reachable via mesh or Nostr",
category: SecureLogger.session, level: .warning)
}
}
// MARK: - Private Methods
private func sendViaMesh(_ message: RoutedMessage) async throws {
// Send the message through mesh - using sendPrivateMessage for now
let recipientHexID = message.recipientNoisePublicKey.hexEncodedString()
if let recipientNickname = meshService.getPeerNicknames()[recipientHexID] {
meshService.sendPrivateMessage(message.content, to: recipientHexID, recipientNickname: recipientNickname, messageID: message.id)
}
// Update status
pendingMessages[message.id]?.status = .sent
}
private func sendViaNostr(_ message: RoutedMessage) async throws {
// Get recipient's Nostr public key
let favoriteStatus = favoritesService.getFavoriteStatus(for: message.recipientNoisePublicKey)
// Looking up Nostr key for recipient
if favoriteStatus != nil {
// Found favorite relationship
} else {
SecureLogger.log("❌ No favorite relationship found",
category: SecureLogger.session, level: .error)
}
guard let favoriteStatus = favoriteStatus,
let recipientNostrPubkey = favoriteStatus.peerNostrPublicKey else {
throw MessageRouterError.noNostrPublicKey
}
// Get sender's Nostr identity
guard let senderIdentity = try NostrIdentityBridge.getCurrentNostrIdentity() else {
throw MessageRouterError.noNostrIdentity
}
// Create NIP-17 encrypted message with structured content
let structuredContent = "MSG:\(message.id):\(message.content)"
let event = try NostrProtocol.createPrivateMessage(
content: structuredContent,
recipientPubkey: recipientNostrPubkey,
senderIdentity: senderIdentity
)
// Created gift wrap event
// Send via relay
nostrRelay.sendEvent(event)
// Update status
pendingMessages[message.id]?.status = .sent
}
private func setupBindings() {
// Monitor Nostr messages
setupNostrMessageHandling()
// Clean up old pending messages periodically
Timer.publish(every: 60, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.cleanupOldMessages()
}
.store(in: &cancellables)
// Listen for app becoming active to check for messages
NotificationCenter.default.publisher(for: .appDidBecomeActive)
.sink { [weak self] _ in
self?.checkForNostrMessages()
}
.store(in: &cancellables)
}
private func setupNostrMessageHandling() {
guard (try? NostrIdentityBridge.getCurrentNostrIdentity()) != nil else {
SecureLogger.log("⚠️ No Nostr identity available for initial setup", category: SecureLogger.session, level: .warning)
return
}
// Connect to relays if not already connected
if !nostrRelay.isConnected {
nostrRelay.connect()
// Wait for connections to establish before subscribing
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
self?.subscribeToNostrMessages()
}
} else {
// Already connected, subscribe immediately
subscribeToNostrMessages()
}
}
/// Check for Nostr messages when app becomes active
func checkForNostrMessages() {
// Checking for Nostr messages
guard (try? NostrIdentityBridge.getCurrentNostrIdentity()) != nil else {
SecureLogger.log("⚠️ No Nostr identity available for message check", category: SecureLogger.session, level: .warning)
return
}
// Ensure we're connected to relays first
if !nostrRelay.isConnected {
// Connecting to Nostr relays
nostrRelay.connect()
// Wait a bit for connections to establish before subscribing
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.subscribeToNostrMessages()
}
} else {
// Already connected, subscribe immediately
subscribeToNostrMessages()
}
}
private func subscribeToNostrMessages() {
guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
// Subscribing to Nostr messages
// Full pubkey recorded
// Pubkey length verified
// Unsubscribe existing subscription to refresh
nostrRelay.unsubscribe(id: "router-messages")
// Create a new subscription for recent messages
let sinceDate = processedMessagesService.getSubscriptionSinceDate()
let filter = NostrFilter.giftWrapsFor(
pubkey: currentIdentity.publicKeyHex,
since: sinceDate
)
// Subscribing to messages since date
// Subscribing to gift wraps
nostrRelay.subscribe(filter: filter, id: "router-messages") { [weak self] event in
// Received Nostr event
self?.handleNostrMessage(event)
}
}
private func handleNostrMessage(_ giftWrap: NostrEvent) {
// Check if we've already processed this event
if processedMessagesService.isMessageProcessed(giftWrap.id) {
// Skipping already processed event
return
}
// Attempting to decrypt gift wrap
// Full event ID recorded
// Event timestamp recorded
// Event tags recorded
// Decrypt the message
guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
SecureLogger.log("❌ No current Nostr identity available",
category: SecureLogger.session, level: .error)
return
}
// Check if this event is actually tagged for us
let ourPubkey = currentIdentity.publicKeyHex
let isTaggedForUs = giftWrap.tags.contains { tag in
tag.count >= 2 && tag[0] == "p" && tag[1] == ourPubkey
}
if !isTaggedForUs {
SecureLogger.log("⚠️ Gift wrap not tagged for us! Our pubkey: \(ourPubkey.prefix(8))..., Event tags: \(giftWrap.tags)",
category: SecureLogger.session, level: .warning)
return
}
do {
let (content, senderPubkey) = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap,
recipientIdentity: currentIdentity
)
// Mark this event as processed to avoid duplicates on app restart
let eventTimestamp = Date(timeIntervalSince1970: TimeInterval(giftWrap.created_at))
processedMessagesService.markMessageAsProcessed(giftWrap.id, timestamp: eventTimestamp)
// Check for deduplication within current session
let messageHash = "\(senderPubkey)-\(content)-\(giftWrap.created_at)"
if messageDeduplication.get(messageHash) != nil {
return // Already processed in this session
}
messageDeduplication.set(messageHash, value: Date())
// Handle special messages
if content.hasPrefix("FAVORITED") || content.hasPrefix("UNFAVORITED") {
let parts = content.split(separator: ":")
let isFavorite = parts.first == "FAVORITED"
let nostrNpub = parts.count > 1 ? String(parts[1]) : nil
handleFavoriteNotification(from: senderPubkey, isFavorite: isFavorite, nostrNpub: nostrNpub)
return
}
// Handle delivery acknowledgments
if content.hasPrefix("DELIVERED:") {
let parts = content.split(separator: ":")
if parts.count > 1 {
let messageId = String(parts[1])
handleDeliveryAcknowledgment(messageId: messageId, from: senderPubkey)
}
return
}
// Handle read receipts
if content.hasPrefix("READ:") {
let parts = content.split(separator: ":", maxSplits: 1)
if parts.count > 1 {
let receiptDataString = String(parts[1])
if let receiptData = Data(base64Encoded: receiptDataString),
let receipt = ReadReceipt.fromBinaryData(receiptData) {
handleReadReceipt(receipt, from: senderPubkey)
}
}
return
}
// Find the sender's Noise public key
guard let senderNoiseKey = findNoisePublicKey(for: senderPubkey) else { return }
// Parse structured message content
var messageId = UUID().uuidString
var messageContent = content
if content.hasPrefix("MSG:") {
let parts = content.split(separator: ":", maxSplits: 2)
if parts.count >= 3 {
messageId = String(parts[1])
messageContent = String(parts[2])
}
}
// Create a BitchatMessage and inject into the stream
let chatMessage = BitchatMessage(
id: messageId,
sender: favoritesService.getFavoriteStatus(for: senderNoiseKey)?.peerNickname ?? "Unknown",
content: messageContent,
timestamp: Date(timeIntervalSince1970: TimeInterval(giftWrap.created_at)),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: nil,
senderPeerID: senderNoiseKey.hexEncodedString(),
mentions: nil,
deliveryStatus: .delivered(to: "nostr", at: Date())
)
// Post notification for ChatViewModel to handle
NotificationCenter.default.post(
name: .nostrMessageReceived,
object: nil,
userInfo: ["message": chatMessage]
)
// Send delivery acknowledgment back to sender
sendDeliveryAcknowledgment(for: chatMessage.id, to: senderPubkey)
} catch {
SecureLogger.log("❌ Failed to decrypt gift wrap: \(error)",
category: SecureLogger.session, level: .error)
}
}
private func handleFavoriteNotification(from nostrPubkey: String, isFavorite: Bool, nostrNpub: String? = nil) {
// Find the sender's Noise public key
guard let senderNoiseKey = findNoisePublicKey(for: nostrPubkey) else { return }
// Update favorites service - nostrPubkey is already the hex public key
favoritesService.updatePeerFavoritedUs(
peerNoisePublicKey: senderNoiseKey,
favorited: isFavorite,
peerNostrPublicKey: nostrPubkey
)
// Post notification for UI update
NotificationCenter.default.post(
name: .favoriteStatusChanged,
object: nil,
userInfo: [
"peerPublicKey": senderNoiseKey,
"isFavorite": isFavorite
]
)
}
private func findNoisePublicKey(for nostrPubkey: String) -> Data? {
// Search through favorites for matching Nostr pubkey
for (noiseKey, relationship) in favoritesService.favorites {
if relationship.peerNostrPublicKey == nostrPubkey {
return noiseKey
}
}
return nil
}
private func handleDeliveryAcknowledgment(messageId: String, from senderPubkey: String) {
// Find the sender's Noise public key
guard let senderNoiseKey = findNoisePublicKey(for: senderPubkey) else { return }
// Post notification for ChatViewModel to update delivery status
NotificationCenter.default.post(
name: .messageDeliveryAcknowledged,
object: nil,
userInfo: [
"messageId": messageId,
"senderNoiseKey": senderNoiseKey
]
)
}
private func handleReadReceipt(_ receipt: ReadReceipt, from senderPubkey: String) {
// Find the sender's Noise public key
guard let senderNoiseKey = findNoisePublicKey(for: senderPubkey) else { return }
let senderHexID = senderNoiseKey.hexEncodedString()
// Update the receipt with the correct sender ID
var updatedReceipt = receipt
updatedReceipt.readerID = senderHexID
// Post notification for ChatViewModel to process
NotificationCenter.default.post(
name: .readReceiptReceived,
object: nil,
userInfo: ["receipt": updatedReceipt]
)
}
func sendReadReceipt(
for originalMessageID: String,
to recipientNoisePublicKey: Data,
preferredTransport: Transport? = nil
) async throws {
// Get nickname from delegate or use default
let nickname = (meshService.delegate as? ChatViewModel)?.nickname ?? "Anonymous"
// Create read receipt
let receipt = ReadReceipt(
originalMessageID: originalMessageID,
readerID: meshService.myPeerID,
readerNickname: nickname
)
// Encode receipt
let receiptData = receipt.toBinaryData()
let content = "READ:\(receiptData.base64EncodedString())"
// Check if peer is connected via mesh (mesh takes precedence)
let recipientHexID = recipientNoisePublicKey.hexEncodedString()
// First check if the peer is currently connected with the given ID
var actualRecipientHexID = recipientHexID
var actualRecipientNoiseKey = recipientNoisePublicKey
// Always check if they reconnected with a new ID, even if preferredTransport is specified
if let favoriteStatus = favoritesService.getFavoriteStatus(for: recipientNoisePublicKey) {
let peerNickname = favoriteStatus.peerNickname
// Search through all current peers to find one with the same nickname
for (currentPeerID, currentNickname) in meshService.getPeerNicknames() {
if currentNickname == peerNickname,
currentPeerID != recipientHexID,
let currentNoiseKey = Data(hexString: currentPeerID) {
SecureLogger.log("🔄 Found updated peer ID for \(peerNickname): \(recipientHexID) -> \(currentPeerID)",
category: SecureLogger.session, level: .info)
actualRecipientHexID = currentPeerID
actualRecipientNoiseKey = currentNoiseKey
break
}
}
// If still not found in connected peers, check all favorites for the current key
if meshService.getPeerNicknames()[actualRecipientHexID] == nil {
// Search through all favorites to find the current noise key for this nickname
for (noiseKey, relationship) in favoritesService.favorites {
if relationship.peerNickname == peerNickname && relationship.peerNostrPublicKey != nil {
SecureLogger.log("🔄 Using current favorite key for \(peerNickname): \(recipientHexID) -> \(noiseKey.hexEncodedString())",
category: SecureLogger.session, level: .info)
actualRecipientHexID = noiseKey.hexEncodedString()
actualRecipientNoiseKey = noiseKey
break
}
}
}
}
let isConnectedOnMesh = meshService.isPeerConnected(actualRecipientHexID)
if isConnectedOnMesh && preferredTransport != .nostr {
// Send via mesh
SecureLogger.log("📡 Sending read receipt via mesh to \(actualRecipientHexID)",
category: SecureLogger.session, level: .debug)
meshService.sendReadReceipt(receipt, to: actualRecipientHexID)
} else {
// Send via Nostr
SecureLogger.log("🌐 Sending read receipt via Nostr to \(actualRecipientHexID)",
category: SecureLogger.session, level: .debug)
// Get recipient's Nostr public key using the actual current noise key
let favoriteStatus = favoritesService.getFavoriteStatus(for: actualRecipientNoiseKey)
guard let recipientNostrPubkey = favoriteStatus?.peerNostrPublicKey else {
SecureLogger.log("❌ Cannot send read receipt - no Nostr key for recipient",
category: SecureLogger.session, level: .error)
throw MessageRouterError.noNostrKey
}
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
SecureLogger.log("⚠️ No Nostr identity available for read receipt",
category: SecureLogger.session, level: .warning)
throw MessageRouterError.noIdentity
}
// Create read receipt message
guard let event = try? NostrProtocol.createPrivateMessage(
content: content,
recipientPubkey: recipientNostrPubkey,
senderIdentity: senderIdentity
) else {
SecureLogger.log("❌ Failed to create read receipt",
category: SecureLogger.session, level: .error)
throw MessageRouterError.encryptionFailed
}
// Send via relay
nostrRelay.sendEvent(event)
}
}
private func sendDeliveryAcknowledgment(for messageId: String, to recipientNostrPubkey: String) {
SecureLogger.log("📤 Sending delivery acknowledgment for message \(messageId)",
category: SecureLogger.session, level: .debug)
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
SecureLogger.log("⚠️ No Nostr identity available for acknowledgment",
category: SecureLogger.session, level: .warning)
return
}
// Create acknowledgment message
let content = "DELIVERED:\(messageId)"
guard let event = try? NostrProtocol.createPrivateMessage(
content: content,
recipientPubkey: recipientNostrPubkey,
senderIdentity: senderIdentity
) else {
SecureLogger.log("❌ Failed to create delivery acknowledgment",
category: SecureLogger.session, level: .error)
return
}
// Send via relay
nostrRelay.sendEvent(event)
}
private func cleanupOldMessages() {
let cutoff = Date().addingTimeInterval(-300) // 5 minutes
pendingMessages = pendingMessages.filter { $0.value.timestamp > cutoff }
}
}
// MARK: - Errors
enum MessageRouterError: LocalizedError {
case peerNotReachable
case noNostrPublicKey
case noNostrIdentity
case transportFailed
case noNostrKey
case noIdentity
case encryptionFailed
var errorDescription: String? {
switch self {
case .peerNotReachable:
return "Peer is not reachable via mesh or Nostr"
case .noNostrPublicKey:
return "Peer's Nostr public key is unknown"
case .noNostrIdentity:
return "No Nostr identity available"
case .transportFailed:
return "Failed to send message"
case .noNostrKey:
return "No Nostr key available for recipient"
case .noIdentity:
return "No identity available"
case .encryptionFailed:
return "Failed to encrypt message"
}
}
}
// MARK: - Notification Names
extension Notification.Name {
static let nostrMessageReceived = Notification.Name("NostrMessageReceived")
static let messageDeliveryAcknowledged = Notification.Name("MessageDeliveryAcknowledged")
static let readReceiptReceived = Notification.Name("ReadReceiptReceived")
static let appDidBecomeActive = Notification.Name("AppDidBecomeActive")
}
+26 -5
View File
@@ -11,7 +11,7 @@
/// ///
/// High-level encryption service that manages Noise Protocol sessions for secure /// High-level encryption service that manages Noise Protocol sessions for secure
/// peer-to-peer communication in BitChat. Acts as the bridge between the transport /// peer-to-peer communication in BitChat. Acts as the bridge between the transport
/// layer (BluetoothMeshService) and the cryptographic layer (NoiseProtocol). /// layer (SimplifiedBluetoothService) and the cryptographic layer (NoiseProtocol).
/// ///
/// ## Overview /// ## Overview
/// This service provides a simplified API for establishing and managing encrypted /// This service provides a simplified API for establishing and managing encrypted
@@ -60,7 +60,7 @@
/// ``` /// ```
/// ///
/// ## Integration Points /// ## Integration Points
/// - **BluetoothMeshService**: Calls this service for all private messages /// - **SimplifiedBluetoothService**: Calls this service for all private messages
/// - **ChatViewModel**: Monitors encryption status for UI indicators /// - **ChatViewModel**: Monitors encryption status for UI indicators
/// - **NoiseHandshakeCoordinator**: Prevents handshake race conditions /// - **NoiseHandshakeCoordinator**: Prevents handshake race conditions
/// - **KeychainManager**: Secure storage for identity keys /// - **KeychainManager**: Secure storage for identity keys
@@ -162,9 +162,26 @@ class NoiseEncryptionService {
private let rekeyCheckInterval: TimeInterval = 60.0 // Check every minute private let rekeyCheckInterval: TimeInterval = 60.0 // Check every minute
// Callbacks // Callbacks
var onPeerAuthenticated: ((String, String) -> Void)? // peerID, fingerprint private var onPeerAuthenticatedHandlers: [((String, String) -> Void)] = [] // Array of handlers for peer authentication
var onHandshakeRequired: ((String) -> Void)? // peerID needs handshake var onHandshakeRequired: ((String) -> Void)? // peerID needs handshake
// Add a handler for peer authentication
func addOnPeerAuthenticatedHandler(_ handler: @escaping (String, String) -> Void) {
serviceQueue.async(flags: .barrier) { [weak self] in
self?.onPeerAuthenticatedHandlers.append(handler)
}
}
// Legacy support - setting this will add to the handlers array
var onPeerAuthenticated: ((String, String) -> Void)? {
get { nil } // Always return nil for backward compatibility
set {
if let handler = newValue {
addOnPeerAuthenticatedHandler(handler)
}
}
}
init() { init() {
// Load or create static identity key (ONLY from keychain) // Load or create static identity key (ONLY from keychain)
let loadedKey: Curve25519.KeyAgreement.PrivateKey let loadedKey: Curve25519.KeyAgreement.PrivateKey
@@ -434,8 +451,12 @@ class NoiseEncryptionService {
// Log security event // Log security event
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
// Notify about authentication // Notify all handlers about authentication
onPeerAuthenticated?(peerID, fingerprint) serviceQueue.async { [weak self] in
self?.onPeerAuthenticatedHandlers.forEach { handler in
handler(peerID, fingerprint)
}
}
} }
private func calculateFingerprint(for publicKey: Curve25519.KeyAgreement.PublicKey) -> String { private func calculateFingerprint(for publicKey: Curve25519.KeyAgreement.PublicKey) -> String {
+189
View File
@@ -0,0 +1,189 @@
//
// PrivateChatManager.swift
// bitchat
//
// Manages private chat sessions and messages
// This is free and unencumbered software released into the public domain.
//
import Foundation
import SwiftUI
/// Manages all private chat functionality
class PrivateChatManager: ObservableObject {
@Published var privateChats: [String: [BitchatMessage]] = [:]
@Published var selectedPeer: String? = nil
@Published var unreadMessages: Set<String> = []
private var selectedPeerFingerprint: String? = nil
var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel
weak var meshService: SimplifiedBluetoothService?
init(meshService: SimplifiedBluetoothService? = nil) {
self.meshService = meshService
}
/// Start a private chat with a peer
func startChat(with peerID: String) {
selectedPeer = peerID
// Store fingerprint for persistence across reconnections
if let fingerprint = meshService?.getPeerFingerprint(peerID) {
selectedPeerFingerprint = fingerprint
}
// Mark messages as read
markAsRead(from: peerID)
// Initialize chat if needed
if privateChats[peerID] == nil {
privateChats[peerID] = []
}
}
/// End the current private chat
func endChat() {
selectedPeer = nil
selectedPeerFingerprint = nil
}
/// Send a private message
func sendMessage(_ content: String, to peerID: String) {
guard let meshService = meshService,
let peerNickname = meshService.getPeerNicknames()[peerID] else {
return
}
let messageID = UUID().uuidString
// Create local message
let message = BitchatMessage(
id: messageID,
sender: meshService.myNickname,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: peerNickname,
senderPeerID: meshService.myPeerID,
mentions: nil,
deliveryStatus: .sending
)
// Add to chat
if privateChats[peerID] == nil {
privateChats[peerID] = []
}
privateChats[peerID]?.append(message)
// Send via mesh service
meshService.sendPrivateMessage(content, to: peerID, recipientNickname: peerNickname, messageID: messageID)
}
/// Handle incoming private message
func handleIncomingMessage(_ message: BitchatMessage) {
guard let senderPeerID = message.senderPeerID else { return }
// Initialize chat if needed
if privateChats[senderPeerID] == nil {
privateChats[senderPeerID] = []
}
// Add message
privateChats[senderPeerID]?.append(message)
// Mark as unread if not in this chat
if selectedPeer != senderPeerID {
unreadMessages.insert(senderPeerID)
// Send notification
NotificationService.shared.sendPrivateMessageNotification(
from: message.sender,
message: message.content,
peerID: senderPeerID
)
} else {
// Send read receipt if viewing this chat
sendReadReceipt(for: message)
}
}
/// Mark messages from a peer as read
func markAsRead(from peerID: String) {
unreadMessages.remove(peerID)
// Send read receipts for unread messages that haven't been sent yet
if let messages = privateChats[peerID] {
for message in messages {
if message.senderPeerID == peerID && !message.isRelay && !sentReadReceipts.contains(message.id) {
sendReadReceipt(for: message)
}
}
}
}
/// Update the selected peer if fingerprint matches (for reconnections)
func updateSelectedPeer(peers: [String: String]) {
guard let fingerprint = selectedPeerFingerprint else { return }
// Find peer with matching fingerprint
for (peerID, _) in peers {
if meshService?.getPeerFingerprint(peerID) == fingerprint {
selectedPeer = peerID
break
}
}
}
/// Get chat messages for current context
func getCurrentMessages() -> [BitchatMessage] {
guard let peer = selectedPeer else { return [] }
return privateChats[peer] ?? []
}
/// Clear a private chat
func clearChat(with peerID: String) {
privateChats[peerID]?.removeAll()
}
/// Handle delivery acknowledgment
func handleDeliveryAck(messageID: String, from peerID: String) {
guard privateChats[peerID] != nil else { return }
if let index = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
privateChats[peerID]?[index].deliveryStatus = .delivered(to: "recipient", at: Date())
}
}
/// Handle read receipt
func handleReadReceipt(messageID: String, from peerID: String) {
guard privateChats[peerID] != nil else { return }
if let index = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
privateChats[peerID]?[index].deliveryStatus = .read(by: "recipient", at: Date())
}
}
// MARK: - Private Methods
private func sendReadReceipt(for message: BitchatMessage) {
guard !sentReadReceipts.contains(message.id),
let senderPeerID = message.senderPeerID else {
return
}
sentReadReceipts.insert(message.id)
// Create read receipt using the simplified method
let receipt = ReadReceipt(
originalMessageID: message.id,
readerID: meshService?.myPeerID ?? "",
readerNickname: meshService?.myNickname ?? ""
)
// Send through mesh service's read receipt method
meshService?.sendReadReceipt(receipt, to: senderPeerID)
}
}
@@ -1,106 +0,0 @@
import Foundation
/// Service to track processed messages across app restarts to prevent duplicates
@MainActor
final class ProcessedMessagesService {
static let shared = ProcessedMessagesService()
private let userDefaults = UserDefaults.standard
private let processedMessagesKey = "ProcessedNostrMessages"
private let lastProcessedTimestampKey = "LastProcessedNostrTimestamp"
private let maxStoredMessages = 1000 // Keep last 1000 message IDs
private var processedMessageIDs: Set<String> = []
private var lastProcessedTimestamp: Date?
private init() {
loadProcessedMessages()
}
/// Check if a message has already been processed
func isMessageProcessed(_ messageID: String) -> Bool {
return processedMessageIDs.contains(messageID)
}
/// Mark a message as processed
func markMessageAsProcessed(_ messageID: String, timestamp: Date) {
processedMessageIDs.insert(messageID)
// Update last processed timestamp if this message is newer
if let lastTimestamp = lastProcessedTimestamp {
if timestamp > lastTimestamp {
lastProcessedTimestamp = timestamp
}
} else {
lastProcessedTimestamp = timestamp
}
// Trim if we have too many stored IDs
if processedMessageIDs.count > maxStoredMessages {
trimOldestMessages()
}
saveProcessedMessages()
}
/// Get the timestamp to use for Nostr subscription filters
func getSubscriptionSinceDate() -> Date {
// If we have a last processed timestamp, use it minus a small buffer
if let lastTimestamp = lastProcessedTimestamp {
// Go back 1 hour before last processed message for safety
return lastTimestamp.addingTimeInterval(-3600)
}
// Default: look back 24 hours on first run
return Date().addingTimeInterval(-86400)
}
/// Clear all processed messages (useful for debugging)
func clearProcessedMessages() {
processedMessageIDs.removeAll()
lastProcessedTimestamp = nil
saveProcessedMessages()
}
// MARK: - Private Methods
private func loadProcessedMessages() {
if let data = userDefaults.data(forKey: processedMessagesKey),
let decoded = try? JSONDecoder().decode([String].self, from: data) {
processedMessageIDs = Set(decoded)
}
if let timestampInterval = userDefaults.object(forKey: lastProcessedTimestampKey) as? TimeInterval {
lastProcessedTimestamp = Date(timeIntervalSince1970: timestampInterval)
}
}
private func saveProcessedMessages() {
// Convert Set to Array for encoding
let messageArray = Array(processedMessageIDs)
if let encoded = try? JSONEncoder().encode(messageArray) {
userDefaults.set(encoded, forKey: processedMessagesKey)
}
if let timestamp = lastProcessedTimestamp {
userDefaults.set(timestamp.timeIntervalSince1970, forKey: lastProcessedTimestampKey)
}
userDefaults.synchronize()
}
private func trimOldestMessages() {
// Since we don't track insertion order, we'll just keep the most recent N messages
// In a production app, you might want to track timestamps for each message
let excess = processedMessageIDs.count - maxStoredMessages
if excess > 0 {
// Remove random excess messages (not ideal, but simple)
for _ in 0..<excess {
if let first = processedMessageIDs.first {
processedMessageIDs.remove(first)
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+405
View File
@@ -0,0 +1,405 @@
//
// UnifiedPeerService.swift
// bitchat
//
// Unified peer state management combining mesh connectivity and favorites
// This is free and unencumbered software released into the public domain.
//
import Foundation
import Combine
import SwiftUI
import CryptoKit
/// Single source of truth for peer state, combining mesh connectivity and favorites
@MainActor
class UnifiedPeerService: ObservableObject {
// MARK: - Published Properties
@Published private(set) var peers: [BitchatPeer] = []
@Published private(set) var connectedPeerIDs: Set<String> = []
@Published private(set) var favorites: [BitchatPeer] = []
@Published private(set) var mutualFavorites: [BitchatPeer] = []
// MARK: - Private Properties
private var peerIndex: [String: BitchatPeer] = [:]
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
private let meshService: SimplifiedBluetoothService
private let favoritesService = FavoritesPersistenceService.shared
private var cancellables = Set<AnyCancellable>()
// MARK: - Initialization
init(meshService: SimplifiedBluetoothService) {
self.meshService = meshService
// Subscribe to changes from both services
setupSubscriptions()
// Perform initial update
Task { @MainActor in
updatePeers()
}
}
// MARK: - Setup
private func setupSubscriptions() {
// Subscribe to mesh peer updates
meshService.fullPeersPublisher
.combineLatest(favoritesService.$favorites)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updatePeers()
}
.store(in: &cancellables)
// Also listen for favorite change notifications
NotificationCenter.default.publisher(for: .favoriteStatusChanged)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updatePeers()
}
.store(in: &cancellables)
}
// MARK: - Core Update Logic
private func updatePeers() {
let meshPeers = meshService.fullPeersPublisher.value
let favorites = favoritesService.favorites
var enrichedPeers: [BitchatPeer] = []
var connected: Set<String> = []
var addedPeerIDs: Set<String> = []
// Phase 1: Add all connected mesh peers
for (peerID, peerInfo) in meshPeers where peerInfo.isConnected {
guard peerID != meshService.myPeerID else { continue } // Never add self
let peer = buildPeerFromMesh(
peerID: peerID,
peerInfo: peerInfo,
favorites: favorites
)
enrichedPeers.append(peer)
connected.insert(peerID)
addedPeerIDs.insert(peerID)
// Update fingerprint cache
if let publicKey = peerInfo.noisePublicKey {
fingerprintCache[peerID] = publicKey.sha256Fingerprint()
}
}
// Phase 2: Add offline favorites that we actively favorite
for (favoriteKey, favorite) in favorites where favorite.isFavorite {
let peerID = favoriteKey.hexEncodedString()
// Skip if already added (connected peer)
if addedPeerIDs.contains(peerID) { continue }
// Skip if connected under different ID but same nickname
let isConnectedByNickname = enrichedPeers.contains {
$0.nickname == favorite.peerNickname && $0.isConnected
}
if isConnectedByNickname { continue }
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID)
enrichedPeers.append(peer)
addedPeerIDs.insert(peerID)
// Update fingerprint cache
fingerprintCache[peerID] = favoriteKey.sha256Fingerprint()
}
// Phase 3: Sort peers
enrichedPeers.sort { lhs, rhs in
// Connected first
if lhs.isConnected != rhs.isConnected {
return lhs.isConnected
}
// Then favorites
if lhs.isFavorite != rhs.isFavorite {
return lhs.isFavorite
}
// Finally alphabetical
return lhs.displayName < rhs.displayName
}
// Phase 4: Build subsets and indices
var favoritesList: [BitchatPeer] = []
var mutualsList: [BitchatPeer] = []
var newIndex: [String: BitchatPeer] = [:]
for peer in enrichedPeers {
newIndex[peer.id] = peer
if peer.isFavorite {
favoritesList.append(peer)
}
if peer.isMutualFavorite {
mutualsList.append(peer)
}
}
// Phase 5: Update published properties
self.peers = enrichedPeers
self.connectedPeerIDs = connected
self.favorites = favoritesList
self.mutualFavorites = mutualsList
self.peerIndex = newIndex
// Log summary (commented out to reduce noise)
// let connectedCount = connected.count
// let offlineCount = enrichedPeers.count - connectedCount
// Peer update: \(enrichedPeers.count) total (\(connectedCount) connected, \(offlineCount) offline)
}
// MARK: - Peer Building Helpers
private func buildPeerFromMesh(
peerID: String,
peerInfo: SimplifiedBluetoothService.PeerInfoSnapshot,
favorites: [Data: FavoritesPersistenceService.FavoriteRelationship]
) -> BitchatPeer {
var peer = BitchatPeer(
id: peerID,
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
nickname: peerInfo.nickname,
lastSeen: peerInfo.lastSeen,
isConnected: true
)
// Check for favorite status
if let noiseKey = peerInfo.noisePublicKey,
let favoriteStatus = favorites[noiseKey] {
peer.favoriteStatus = favoriteStatus
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
} else {
// Check by nickname for reconnected peers
let favoriteByNickname = favorites.values.first {
$0.peerNickname == peerInfo.nickname
}
if let favorite = favoriteByNickname,
let noiseKey = peerInfo.noisePublicKey {
SecureLogger.log(
"🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
category: SecureLogger.session,
level: .info
)
// Update the favorite's key in persistence
favoritesService.updateNoisePublicKey(
from: favorite.peerNoisePublicKey,
to: noiseKey,
peerNickname: peerInfo.nickname
)
// Get updated favorite
peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey)
peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey
}
}
return peer
}
private func buildPeerFromFavorite(
favorite: FavoritesPersistenceService.FavoriteRelationship,
peerID: String
) -> BitchatPeer {
var peer = BitchatPeer(
id: peerID,
noisePublicKey: favorite.peerNoisePublicKey,
nickname: favorite.peerNickname,
lastSeen: favorite.lastUpdated,
isConnected: false
)
peer.favoriteStatus = favorite
peer.nostrPublicKey = favorite.peerNostrPublicKey
return peer
}
// MARK: - Public Methods
/// Get peer by ID
func getPeer(by id: String) -> BitchatPeer? {
return peerIndex[id]
}
/// Get peer ID for nickname
func getPeerID(for nickname: String) -> String? {
for peer in peers {
if peer.displayName == nickname || peer.nickname == nickname {
return peer.id
}
}
return nil
}
/// Check if peer is online
func isOnline(_ peerID: String) -> Bool {
return connectedPeerIDs.contains(peerID)
}
/// Check if peer is blocked
func isBlocked(_ peerID: String) -> Bool {
// Get fingerprint
guard let fingerprint = getFingerprint(for: peerID) else { return false }
// Check SecureIdentityStateManager for block status
if let identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
return identity.isBlocked
}
return false
}
/// Toggle favorite status
func toggleFavorite(_ peerID: String) {
guard let peer = getPeer(by: peerID) else {
SecureLogger.log("⚠️ Cannot toggle favorite - peer not found: \(peerID)",
category: SecureLogger.session, level: .warning)
return
}
let wasFavorite = peer.isFavorite
// Get the actual nickname for logging and saving
var actualNickname = peer.nickname
// Debug logging to understand the issue
SecureLogger.log("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)",
category: SecureLogger.session, level: .info)
if actualNickname.isEmpty {
// Try to get from mesh service's current peer list
if let meshPeerNickname = meshService.getPeerNicknames()[peerID] {
actualNickname = meshPeerNickname
SecureLogger.log("🔍 Got nickname from mesh service: '\(actualNickname)'",
category: SecureLogger.session, level: .debug)
}
}
// Use displayName as fallback (which shows ID prefix if nickname is empty)
let finalNickname = actualNickname.isEmpty ? peer.displayName : actualNickname
if wasFavorite {
// Remove favorite
favoritesService.removeFavorite(peerNoisePublicKey: peer.noisePublicKey)
} else {
// Get or derive peer's Nostr public key if not already known
var peerNostrKey = peer.nostrPublicKey
if peerNostrKey == nil {
// Try to get from NostrIdentityBridge association
peerNostrKey = NostrIdentityBridge.getNostrPublicKey(for: peer.noisePublicKey)
}
// Add favorite
favoritesService.addFavorite(
peerNoisePublicKey: peer.noisePublicKey,
peerNostrPublicKey: peerNostrKey,
peerNickname: finalNickname
)
}
// Log the final nickname being saved
SecureLogger.log("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))",
category: SecureLogger.session, level: .info)
// Send favorite notification to the peer
meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
// Force update of peers to reflect the change
updatePeers()
// Force UI update by notifying SwiftUI directly
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
}
}
/// Toggle blocked status
func toggleBlocked(_ peerID: String) {
guard let fingerprint = getFingerprint(for: peerID) else { return }
// Get or create social identity
var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint)
?? SocialIdentity(
fingerprint: fingerprint,
localPetname: nil,
claimedNickname: getPeer(by: peerID)?.displayName ?? "Unknown",
trustLevel: .unknown,
isFavorite: false,
isBlocked: false,
notes: nil
)
// Toggle blocked status
identity.isBlocked = !identity.isBlocked
// Can't be both favorite and blocked
if identity.isBlocked {
identity.isFavorite = false
// Also remove from favorites service
if let peer = getPeer(by: peerID) {
favoritesService.removeFavorite(peerNoisePublicKey: peer.noisePublicKey)
}
}
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
}
/// Get fingerprint for peer ID
func getFingerprint(for peerID: String) -> String? {
// Check cache first
if let cached = fingerprintCache[peerID] {
return cached
}
// Try to get from mesh service
if let fingerprint = meshService.getPeerFingerprint(peerID) {
fingerprintCache[peerID] = fingerprint
return fingerprint
}
// Try to get from peer's public key
if let peer = getPeer(by: peerID) {
let fingerprint = peer.noisePublicKey.sha256Fingerprint()
fingerprintCache[peerID] = fingerprint
return fingerprint
}
return nil
}
// MARK: - Compatibility Methods (for easy migration)
var allPeers: [BitchatPeer] { peers }
var connectedPeers: [String] { Array(connectedPeerIDs) }
var favoritePeers: Set<String> {
Set(favorites.compactMap { getFingerprint(for: $0.id) })
}
var blockedUsers: Set<String> {
Set(peers.compactMap { peer in
isBlocked(peer.id) ? getFingerprint(for: peer.id) : nil
})
}
}
// MARK: - Helper Extensions
extension Data {
func sha256Fingerprint() -> String {
// Implementation matches existing fingerprint generation in NoiseEncryptionService
let hash = SHA256.hash(data: self)
return hash.map { String(format: "%02x", $0) }.joined()
}
}
-222
View File
@@ -1,222 +0,0 @@
//
// BatteryOptimizer.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import Combine
#if os(iOS)
import UIKit
#elseif os(macOS)
import IOKit.ps
#endif
enum PowerMode {
case performance // Max performance, battery drain OK
case balanced // Default balanced mode
case powerSaver // Aggressive power saving
case ultraLowPower // Emergency mode
var scanDuration: TimeInterval {
switch self {
case .performance: return 3.0
case .balanced: return 2.0
case .powerSaver: return 1.0
case .ultraLowPower: return 0.5
}
}
var scanPauseDuration: TimeInterval {
switch self {
case .performance: return 2.0
case .balanced: return 3.0
case .powerSaver: return 8.0
case .ultraLowPower: return 20.0
}
}
var maxConnections: Int {
switch self {
case .performance: return 20
case .balanced: return 10
case .powerSaver: return 5
case .ultraLowPower: return 2
}
}
var advertisingInterval: TimeInterval {
// Note: iOS doesn't let us control this directly, but we can stop/start advertising
switch self {
case .performance: return 0.0 // Continuous
case .balanced: return 5.0 // Advertise every 5 seconds
case .powerSaver: return 15.0 // Advertise every 15 seconds
case .ultraLowPower: return 30.0 // Advertise every 30 seconds
}
}
}
class BatteryOptimizer {
static let shared = BatteryOptimizer()
@Published var currentPowerMode: PowerMode = .balanced
@Published var isInBackground: Bool = false
@Published var batteryLevel: Float = 1.0
@Published var isCharging: Bool = false
private var observers: [NSObjectProtocol] = []
private init() {
setupObservers()
updateBatteryStatus()
}
deinit {
observers.forEach { NotificationCenter.default.removeObserver($0) }
}
private func setupObservers() {
#if os(iOS)
// Monitor app state
observers.append(
NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.isInBackground = true
self?.updatePowerMode()
}
)
observers.append(
NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.isInBackground = false
self?.updatePowerMode()
}
)
// Monitor battery
UIDevice.current.isBatteryMonitoringEnabled = true
observers.append(
NotificationCenter.default.addObserver(
forName: UIDevice.batteryLevelDidChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.updateBatteryStatus()
}
)
observers.append(
NotificationCenter.default.addObserver(
forName: UIDevice.batteryStateDidChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.updateBatteryStatus()
}
)
#endif
}
private func updateBatteryStatus() {
#if os(iOS)
batteryLevel = UIDevice.current.batteryLevel
if batteryLevel < 0 {
batteryLevel = 1.0 // Unknown battery level
}
isCharging = UIDevice.current.batteryState == .charging ||
UIDevice.current.batteryState == .full
#elseif os(macOS)
if let info = getMacOSBatteryInfo() {
batteryLevel = info.level
isCharging = info.isCharging
}
#endif
updatePowerMode()
}
#if os(macOS)
private func getMacOSBatteryInfo() -> (level: Float, isCharging: Bool)? {
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
for source in sources {
if let description = IOPSGetPowerSourceDescription(snapshot, source).takeUnretainedValue() as? [String: Any] {
if let currentCapacity = description[kIOPSCurrentCapacityKey] as? Int,
let maxCapacity = description[kIOPSMaxCapacityKey] as? Int {
let level = Float(currentCapacity) / Float(maxCapacity)
let isCharging = description[kIOPSPowerSourceStateKey] as? String == kIOPSACPowerValue
return (level, isCharging)
}
}
}
return nil
}
#endif
private func updatePowerMode() {
// Determine optimal power mode based on:
// 1. Battery level
// 2. Charging status
// 3. Background/foreground state
if isCharging {
// When charging, use performance mode unless battery is critical
currentPowerMode = batteryLevel < 0.1 ? .balanced : .performance
} else if isInBackground {
// In background, be less aggressive with power management
// Don't force ultra-low power mode until battery is below 10%
// Use balanced mode in background above 30% battery
if batteryLevel < 0.1 {
currentPowerMode = .ultraLowPower
} else if batteryLevel < 0.3 {
currentPowerMode = .powerSaver
} else {
currentPowerMode = .balanced // Use balanced mode above 30% in background
}
} else {
// Foreground, not charging
if batteryLevel < 0.1 {
currentPowerMode = .ultraLowPower
} else if batteryLevel < 0.3 {
currentPowerMode = .powerSaver
} else if batteryLevel < 0.6 {
currentPowerMode = .balanced
} else {
currentPowerMode = .performance
}
}
}
// Manual power mode override
func setPowerMode(_ mode: PowerMode) {
currentPowerMode = mode
}
// Get current scan parameters
var scanParameters: (duration: TimeInterval, pause: TimeInterval) {
return (currentPowerMode.scanDuration, currentPowerMode.scanPauseDuration)
}
// Should we skip non-essential operations?
var shouldSkipNonEssential: Bool {
return currentPowerMode == .ultraLowPower ||
(currentPowerMode == .powerSaver && isInBackground)
}
// Should we reduce message frequency?
var shouldThrottleMessages: Bool {
return currentPowerMode == .powerSaver || currentPowerMode == .ultraLowPower
}
}
-171
View File
@@ -1,171 +0,0 @@
//
// LRUCache.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
/// Thread-safe LRU (Least Recently Used) cache implementation
final class LRUCache<Key: Hashable, Value> {
private class Node {
var key: Key
var value: Value
var prev: Node?
var next: Node?
init(key: Key, value: Value) {
self.key = key
self.value = value
}
}
private let maxSize: Int
private var cache: [Key: Node] = [:]
private var head: Node?
private var tail: Node?
private let queue = DispatchQueue(label: "bitchat.lrucache", attributes: .concurrent)
init(maxSize: Int) {
self.maxSize = maxSize
}
func set(_ key: Key, value: Value) {
queue.sync(flags: .barrier) {
if let node = cache[key] {
// Update existing value and move to front
node.value = value
moveToFront(node)
} else {
// Add new node
let newNode = Node(key: key, value: value)
cache[key] = newNode
addToFront(newNode)
// Remove oldest if over capacity
if cache.count > maxSize {
if let tailNode = tail {
removeNode(tailNode)
cache.removeValue(forKey: tailNode.key)
}
}
}
}
}
func get(_ key: Key) -> Value? {
return queue.sync(flags: .barrier) {
guard let node = cache[key] else { return nil }
moveToFront(node)
return node.value
}
}
func contains(_ key: Key) -> Bool {
return queue.sync {
return cache[key] != nil
}
}
func remove(_ key: Key) {
queue.sync(flags: .barrier) {
if let node = cache[key] {
removeNode(node)
cache.removeValue(forKey: key)
}
}
}
func removeAll() {
queue.sync(flags: .barrier) {
cache.removeAll()
head = nil
tail = nil
}
}
var count: Int {
return queue.sync {
return cache.count
}
}
var keys: [Key] {
return queue.sync {
return Array(cache.keys)
}
}
// MARK: - Private Helpers
private func addToFront(_ node: Node) {
node.next = head
node.prev = nil
if let head = head {
head.prev = node
}
head = node
if tail == nil {
tail = node
}
}
private func removeNode(_ node: Node) {
if let prev = node.prev {
prev.next = node.next
} else {
head = node.next
}
if let next = node.next {
next.prev = node.prev
} else {
tail = node.prev
}
node.prev = nil
node.next = nil
}
private func moveToFront(_ node: Node) {
guard node !== head else { return }
removeNode(node)
addToFront(node)
}
}
// MARK: - Bounded Set
/// Thread-safe set with maximum size using LRU eviction
final class BoundedSet<Element: Hashable> {
private let cache: LRUCache<Element, Bool>
init(maxSize: Int) {
self.cache = LRUCache(maxSize: maxSize)
}
func insert(_ element: Element) {
cache.set(element, value: true)
}
func contains(_ element: Element) -> Bool {
return cache.get(element) != nil
}
func remove(_ element: Element) {
cache.remove(element)
}
func removeAll() {
cache.removeAll()
}
var count: Int {
return cache.count
}
}
-141
View File
@@ -1,141 +0,0 @@
//
// OptimizedBloomFilter.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import CryptoKit
/// Optimized Bloom filter using bit-packed storage and better hash functions
struct OptimizedBloomFilter {
private var bitArray: [UInt64]
private let bitCount: Int
private let hashCount: Int
// Statistics
private(set) var insertCount: Int = 0
init(expectedItems: Int = 1000, falsePositiveRate: Double = 0.01) {
// Calculate optimal bit count and hash count
let m = Double(expectedItems) * abs(log(falsePositiveRate)) / (log(2) * log(2))
self.bitCount = Int(max(64, m.rounded()))
let k = Double(bitCount) / Double(expectedItems) * log(2)
self.hashCount = Int(max(1, min(10, k.rounded())))
// Initialize bit array (64 bits per UInt64)
let arraySize = (bitCount + 63) / 64
self.bitArray = Array(repeating: 0, count: arraySize)
}
mutating func insert(_ item: String) {
let hashes = generateHashes(item)
for i in 0..<hashCount {
let bitIndex = hashes[i] % bitCount
let arrayIndex = bitIndex / 64
let bitOffset = bitIndex % 64
bitArray[arrayIndex] |= (1 << bitOffset)
}
insertCount += 1
}
func contains(_ item: String) -> Bool {
let hashes = generateHashes(item)
for i in 0..<hashCount {
let bitIndex = hashes[i] % bitCount
let arrayIndex = bitIndex / 64
let bitOffset = bitIndex % 64
if (bitArray[arrayIndex] & (1 << bitOffset)) == 0 {
return false
}
}
return true
}
mutating func reset() {
for i in 0..<bitArray.count {
bitArray[i] = 0
}
insertCount = 0
}
// Generate multiple hash values using double hashing technique
private func generateHashes(_ item: String) -> [Int] {
guard let data = item.data(using: .utf8) else {
return Array(repeating: 0, count: hashCount)
}
// Use SHA256 for high-quality hash values
let hash = SHA256.hash(data: data)
let hashBytes = Array(hash)
var hashes = [Int]()
// Extract multiple hash values from the SHA256 output
for i in 0..<hashCount {
let offset = (i * 4) % (hashBytes.count - 3)
let value = Int(hashBytes[offset]) |
(Int(hashBytes[offset + 1]) << 8) |
(Int(hashBytes[offset + 2]) << 16) |
(Int(hashBytes[offset + 3]) << 24)
hashes.append(abs(value))
}
return hashes
}
// Calculate current false positive probability
var estimatedFalsePositiveRate: Double {
guard insertCount > 0 else { return 0 }
// Count set bits
var setBits = 0
for value in bitArray {
setBits += value.nonzeroBitCount
}
// Calculate probability: (1 - e^(-kn/m))^k
let ratio = Double(hashCount * insertCount) / Double(bitCount)
return pow(1 - exp(-ratio), Double(hashCount))
}
// Get memory usage in bytes
var memorySizeBytes: Int {
return bitArray.count * 8
}
}
// Extension for adaptive Bloom filter that adjusts based on network size
extension OptimizedBloomFilter {
static func adaptive(for networkSize: Int) -> OptimizedBloomFilter {
// Adjust parameters based on network size
let expectedItems: Int
let falsePositiveRate: Double
switch networkSize {
case 0..<50:
expectedItems = 500
falsePositiveRate = 0.01
case 50..<200:
expectedItems = 2000
falsePositiveRate = 0.02
case 200..<500:
expectedItems = 5000
falsePositiveRate = 0.03
default:
expectedItems = 10000
falsePositiveRate = 0.05
}
return OptimizedBloomFilter(expectedItems: expectedItems, falsePositiveRate: falsePositiveRate)
}
}
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -397,7 +397,7 @@ struct ContentView: View {
_ = viewModel.completeNickname(suggestion, in: &messageText) _ = viewModel.completeNickname(suggestion, in: &messageText)
}) { }) {
HStack { HStack {
Text("@\(suggestion)") Text(suggestion)
.font(.system(size: 11, design: .monospaced)) .font(.system(size: 11, design: .monospaced))
.foregroundColor(textColor) .foregroundColor(textColor)
.fontWeight(.medium) .fontWeight(.medium)
@@ -426,10 +426,13 @@ struct ContentView: View {
let commandInfo: [(commands: [String], syntax: String?, description: String)] = [ let commandInfo: [(commands: [String], syntax: String?, description: String)] = [
(["/block"], "[nickname]", "block or list blocked peers"), (["/block"], "[nickname]", "block or list blocked peers"),
(["/clear"], nil, "clear chat messages"), (["/clear"], nil, "clear chat messages"),
(["/fav"], "<nickname>", "add to favorites"),
(["/help"], nil, "show this help"),
(["/hug"], "<nickname>", "send someone a warm hug"), (["/hug"], "<nickname>", "send someone a warm hug"),
(["/m", "/msg"], "<nickname> [message]", "send private message"), (["/m", "/msg"], "<nickname> [message]", "send private message"),
(["/slap"], "<nickname>", "slap someone with a trout"), (["/slap"], "<nickname>", "slap someone with a trout"),
(["/unblock"], "<nickname>", "unblock a peer"), (["/unblock"], "<nickname>", "unblock a peer"),
(["/unfav"], "<nickname>", "remove from favorites"),
(["/w"], nil, "see who's online") (["/w"], nil, "see who's online")
] ]
@@ -512,10 +515,13 @@ struct ContentView: View {
let commandDescriptions = [ let commandDescriptions = [
("/block", "block or list blocked peers"), ("/block", "block or list blocked peers"),
("/clear", "clear chat messages"), ("/clear", "clear chat messages"),
("/fav", "add to favorites"),
("/help", "show this help"),
("/hug", "send someone a warm hug"), ("/hug", "send someone a warm hug"),
("/m", "send private message"), ("/m", "send private message"),
("/slap", "slap someone with a trout"), ("/slap", "slap someone with a trout"),
("/unblock", "unblock a peer"), ("/unblock", "unblock a peer"),
("/unfav", "remove from favorites"),
("/w", "see who's online") ("/w", "see who's online")
] ]
@@ -645,7 +651,7 @@ struct ContentView: View {
displayName: peer.id == currentMyPeerID ? viewModel.nickname : peer.nickname, displayName: peer.id == currentMyPeerID ? viewModel.nickname : peer.nickname,
isFavorite: peer.favoriteStatus?.isFavorite ?? false, isFavorite: peer.favoriteStatus?.isFavorite ?? false,
isMe: peer.id == currentMyPeerID, isMe: peer.id == currentMyPeerID,
hasUnreadMessages: viewModel.unreadPrivateMessages.contains(peer.id), hasUnreadMessages: viewModel.hasUnreadMessages(for: peer.id),
encryptionStatus: viewModel.getEncryptionStatus(for: peer.id), encryptionStatus: viewModel.getEncryptionStatus(for: peer.id),
connectionState: peer.connectionState, connectionState: peer.connectionState,
isMutualFavorite: peer.favoriteStatus?.isMutual ?? false isMutualFavorite: peer.favoriteStatus?.isMutual ?? false
@@ -884,7 +890,7 @@ struct ContentView: View {
// People counter with unread indicator // People counter with unread indicator
HStack(spacing: 4) { HStack(spacing: 4) {
if !viewModel.unreadPrivateMessages.isEmpty { if viewModel.hasAnyUnreadMessages {
Image(systemName: "envelope.fill") Image(systemName: "envelope.fill")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
+2
View File
@@ -26,6 +26,8 @@
<dict> <dict>
<key>NSExtensionActivationRule</key> <key>NSExtensionActivationRule</key>
<dict> <dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key> <key>NSExtensionActivationSupportsText</key>
<true/> <true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key> <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
+9 -246
View File
@@ -16,9 +16,6 @@ final class PrivateChatE2ETests: XCTestCase {
var bob: MockBluetoothMeshService! var bob: MockBluetoothMeshService!
var charlie: MockBluetoothMeshService! var charlie: MockBluetoothMeshService!
var deliveryTracker: DeliveryTracker!
var retryService: MessageRetryService!
override func setUp() { override func setUp() {
super.setUp() super.setUp()
@@ -27,22 +24,13 @@ final class PrivateChatE2ETests: XCTestCase {
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2) bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3) charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
// Setup delivery tracking // Delivery tracking is now handled internally by SimplifiedBluetoothService
deliveryTracker = DeliveryTracker.shared
retryService = MessageRetryService.shared
retryService.meshService = alice
// Clear any existing state
deliveryTracker.clearDeliveryStatus(for: "")
retryService.clearRetryQueue()
} }
override func tearDown() { override func tearDown() {
alice = nil alice = nil
bob = nil bob = nil
charlie = nil charlie = nil
deliveryTracker = nil
retryService = nil
super.tearDown() super.tearDown()
} }
@@ -104,227 +92,16 @@ final class PrivateChatE2ETests: XCTestCase {
// MARK: - Delivery Acknowledgment Tests // MARK: - Delivery Acknowledgment Tests
func testDeliveryAckGeneration() { // NOTE: DeliveryTracker has been removed in SimplifiedBluetoothService.
simulateConnection(alice, bob) // Delivery tracking is now handled internally.
let messageID = UUID().uuidString
let expectation = XCTestExpectation(description: "Delivery status updated")
// Monitor delivery status
let cancellable = deliveryTracker.deliveryStatusUpdated.sink { update in
if update.messageID == messageID {
switch update.status {
case .delivered(let recipient, _):
XCTAssertEqual(recipient, TestConstants.testNickname2)
expectation.fulfill()
default:
break
}
}
}
// Setup Bob to generate ACK
bob.packetDeliveryHandler = { packet in
if let message = BitchatMessage.fromBinaryPayload(packet.payload),
message.isPrivate {
// Generate ACK
if let ack = self.deliveryTracker.generateAck(
for: message,
myPeerID: TestConstants.testPeerID2,
myNickname: TestConstants.testNickname2,
hopCount: 1
) {
// Send ACK back
let ackData = ack.encode()!
let ackPacket = TestHelpers.createTestPacket(
type: 0x03,
senderID: TestConstants.testPeerID2,
recipientID: TestConstants.testPeerID1,
payload: ackData
)
self.alice.simulateIncomingPacket(ackPacket)
}
}
}
// Setup Alice to process ACK
alice.packetDeliveryHandler = { packet in
if packet.type == 0x03 {
if let ack = DeliveryAck.decode(from: packet.payload) {
self.deliveryTracker.processDeliveryAck(ack)
}
}
}
// Track the message
let trackedMessage = BitchatMessage(
id: messageID,
sender: TestConstants.testNickname1,
content: TestConstants.testMessage1,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: TestConstants.testNickname2,
senderPeerID: TestConstants.testPeerID1,
mentions: nil
)
deliveryTracker.trackMessage(
trackedMessage,
recipientID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// Send the message
alice.sendPrivateMessage(
TestConstants.testMessage1,
to: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2,
messageID: messageID
)
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
cancellable.cancel()
}
func testDeliveryTimeout() {
// Don't connect peers - message should timeout
let messageID = UUID().uuidString
let expectation = XCTestExpectation(description: "Delivery failed due to timeout")
// Use shared instance (can't create new one due to private init)
let shortTimeoutTracker = DeliveryTracker.shared
let cancellable = shortTimeoutTracker.deliveryStatusUpdated.sink { update in
if update.messageID == messageID {
switch update.status {
case .failed(let reason):
XCTAssertTrue(reason.contains("not delivered"))
expectation.fulfill()
default:
break
}
}
}
let trackedMessage = BitchatMessage(
id: messageID,
sender: TestConstants.testNickname1,
content: TestConstants.testMessage1,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: TestConstants.testNickname2,
senderPeerID: TestConstants.testPeerID1,
mentions: nil
)
// Track with short timeout (will use default 30s for private messages)
shortTimeoutTracker.trackMessage(
trackedMessage,
recipientID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// Don't actually send - let it timeout
// For testing, we'll manually trigger timeout
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
shortTimeoutTracker.clearDeliveryStatus(for: messageID)
shortTimeoutTracker.deliveryStatusUpdated.send((messageID: messageID, status: .failed(reason: "Message not delivered")))
}
wait(for: [expectation], timeout: TestConstants.shortTimeout)
cancellable.cancel()
}
// MARK: - Message Retry Tests // MARK: - Message Retry Tests
func testMessageRetryOnFailure() { // NOTE: MessageRetryService has been removed in SimplifiedBluetoothService.
let messageContent = "Retry test message" // Retry logic is now handled internally.
let expectation = XCTestExpectation(description: "Message retried")
var sendCount = 0
// Override send to count attempts
alice.messageDeliveryHandler = { message in
if message.content == messageContent {
sendCount += 1
if sendCount == 2 { // Original + 1 retry
expectation.fulfill()
}
}
}
// Add to retry queue
retryService.addMessageForRetry(
content: messageContent,
isPrivate: true,
recipientPeerID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// Simulate connection after delay to trigger retry
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
self.simulateConnection(self.alice, self.bob)
}
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
XCTAssertGreaterThanOrEqual(sendCount, 1)
}
func testRetryQueueOrdering() {
// Add multiple messages to retry queue
let messages = [
(content: "First", timestamp: Date().addingTimeInterval(-10)),
(content: "Second", timestamp: Date().addingTimeInterval(-5)),
(content: "Third", timestamp: Date())
]
for msg in messages {
retryService.addMessageForRetry(
content: msg.content,
isPrivate: true,
recipientPeerID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2,
originalTimestamp: msg.timestamp
)
}
XCTAssertEqual(retryService.getRetryQueueCount(), 3)
var receivedOrder: [String] = []
let expectation = XCTestExpectation(description: "Messages received in order")
bob.messageDeliveryHandler = { message in
receivedOrder.append(message.content)
if receivedOrder.count == 3 {
expectation.fulfill()
}
}
// Connect to trigger retry
simulateConnection(alice, bob)
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
// Verify order maintained
XCTAssertEqual(receivedOrder, ["First", "Second", "Third"])
}
func testRetryQueueMaxSize() {
// Try to add more than max queue size
for i in 0..<60 {
retryService.addMessageForRetry(
content: "Message \(i)",
recipientPeerID: TestConstants.testPeerID2
)
}
// Should not exceed max size (50)
XCTAssertLessThanOrEqual(retryService.getRetryQueueCount(), 50)
}
// MARK: - End-to-End Encryption Tests // MARK: - End-to-End Encryption Tests
@@ -491,24 +268,7 @@ final class PrivateChatE2ETests: XCTestCase {
// MARK: - Error Handling Tests // MARK: - Error Handling Tests
func testPrivateMessageToUnknownPeer() { // NOTE: This test relied on MessageRetryService which has been removed
// Alice not connected to anyone
let expectation = XCTestExpectation(description: "Message added to retry queue")
retryService.addMessageForRetry(
content: TestConstants.testMessage1,
isPrivate: true,
recipientPeerID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertEqual(self.retryService.getRetryQueueCount(), 1)
expectation.fulfill()
}
wait(for: [expectation], timeout: TestConstants.shortTimeout)
}
func testDuplicateAckPrevention() { func testDuplicateAckPrevention() {
simulateConnection(alice, bob) simulateConnection(alice, bob)
@@ -537,6 +297,8 @@ final class PrivateChatE2ETests: XCTestCase {
) )
// Generate multiple ACKs for same message // Generate multiple ACKs for same message
// NOTE: DeliveryTracker has been removed - this test is no longer applicable
/*
for _ in 0..<3 { for _ in 0..<3 {
if let ack = deliveryTracker.generateAck( if let ack = deliveryTracker.generateAck(
for: message, for: message,
@@ -554,6 +316,7 @@ final class PrivateChatE2ETests: XCTestCase {
alice.simulateIncomingPacket(ackPacket) alice.simulateIncomingPacket(ackPacket)
} }
} }
*/
// Should only generate one ACK // Should only generate one ACK
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+12 -12
View File
@@ -287,7 +287,7 @@ final class IntegrationTests: XCTestCase {
// Track identity announcements // Track identity announcements
nodes["Bob"]!.packetDeliveryHandler = { packet in nodes["Bob"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.noiseIdentityAnnounce.rawValue { if packet.type == 0x04 { // noiseIdentityAnnounce was removed
bobReceivedIdentityAnnounce = true bobReceivedIdentityAnnounce = true
if aliceReceivedIdentityAnnounce { if aliceReceivedIdentityAnnounce {
expectation.fulfill() expectation.fulfill()
@@ -296,7 +296,7 @@ final class IntegrationTests: XCTestCase {
} }
nodes["Alice"]!.packetDeliveryHandler = { packet in nodes["Alice"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.noiseIdentityAnnounce.rawValue { if packet.type == 0x04 { // noiseIdentityAnnounce was removed
aliceReceivedIdentityAnnounce = true aliceReceivedIdentityAnnounce = true
if bobReceivedIdentityAnnounce { if bobReceivedIdentityAnnounce {
expectation.fulfill() expectation.fulfill()
@@ -347,7 +347,7 @@ final class IntegrationTests: XCTestCase {
let handshakeExpectation = XCTestExpectation(description: "New handshake completed") let handshakeExpectation = XCTestExpectation(description: "New handshake completed")
nodes["Bob"]!.packetDeliveryHandler = { packet in nodes["Bob"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.noiseHandshakeInit.rawValue { if packet.type == 0x05 { // noiseHandshakeInit was removed
// Bob initiates new handshake after restart // Bob initiates new handshake after restart
do { do {
let response = try self.noiseManagers["Alice"]!.handleIncomingHandshake( let response = try self.noiseManagers["Alice"]!.handleIncomingHandshake(
@@ -357,7 +357,7 @@ final class IntegrationTests: XCTestCase {
if let resp = response { if let resp = response {
// Send response back to Bob // Send response back to Bob
let responsePacket = TestHelpers.createTestPacket( let responsePacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeResp.rawValue, type: 0x06, // noiseHandshakeResp was removed
payload: resp payload: resp
) )
self.nodes["Bob"]!.simulateIncomingPacket(responsePacket) self.nodes["Bob"]!.simulateIncomingPacket(responsePacket)
@@ -365,7 +365,7 @@ final class IntegrationTests: XCTestCase {
} catch { } catch {
XCTFail("Handshake handling failed: \(error)") XCTFail("Handshake handling failed: \(error)")
} }
} else if packet.type == MessageType.noiseHandshakeResp.rawValue && packet.senderID.hexEncodedString() == TestConstants.testPeerID1 { } else if packet.type == 0x06 // noiseHandshakeResp was removed && packet.senderID.hexEncodedString() == TestConstants.testPeerID1 {
// Final handshake message (message 3 in XX pattern) // Final handshake message (message 3 in XX pattern)
handshakeExpectation.fulfill() handshakeExpectation.fulfill()
} }
@@ -552,7 +552,7 @@ final class IntegrationTests: XCTestCase {
let nackData = nack.toBinaryData() let nackData = nack.toBinaryData()
let nackPacket = TestHelpers.createTestPacket( let nackPacket = TestHelpers.createTestPacket(
type: MessageType.protocolNack.rawValue, type: 0x08, // protocolNack was removed
payload: nackData payload: nackData
) )
self.nodes["Alice"]!.simulateIncomingPacket(nackPacket) self.nodes["Alice"]!.simulateIncomingPacket(nackPacket)
@@ -560,7 +560,7 @@ final class IntegrationTests: XCTestCase {
// Bob clears session // Bob clears session
self.noiseManagers["Bob"]!.removeSession(for: TestConstants.testPeerID1) self.noiseManagers["Bob"]!.removeSession(for: TestConstants.testPeerID1)
} }
} else if packet.type == MessageType.noiseHandshakeInit.rawValue { } else if packet.type == 0x05 { // noiseHandshakeInit was removed
// Bob receives handshake init from Alice after NACK // Bob receives handshake init from Alice after NACK
do { do {
let response = try self.noiseManagers["Bob"]!.handleIncomingHandshake( let response = try self.noiseManagers["Bob"]!.handleIncomingHandshake(
@@ -569,7 +569,7 @@ final class IntegrationTests: XCTestCase {
) )
if let resp = response { if let resp = response {
let responsePacket = TestHelpers.createTestPacket( let responsePacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeResp.rawValue, type: 0x06, // noiseHandshakeResp was removed
payload: resp payload: resp
) )
self.nodes["Alice"]!.simulateIncomingPacket(responsePacket) self.nodes["Alice"]!.simulateIncomingPacket(responsePacket)
@@ -582,7 +582,7 @@ final class IntegrationTests: XCTestCase {
// Setup Alice's handler to clear session on NACK and initiate handshake // Setup Alice's handler to clear session on NACK and initiate handshake
nodes["Alice"]!.packetDeliveryHandler = { packet in nodes["Alice"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.protocolNack.rawValue { if packet.type == 0x08 { // protocolNack was removed
// Alice receives NACK - clear session // Alice receives NACK - clear session
self.noiseManagers["Alice"]!.removeSession(for: TestConstants.testPeerID2) self.noiseManagers["Alice"]!.removeSession(for: TestConstants.testPeerID2)
@@ -590,14 +590,14 @@ final class IntegrationTests: XCTestCase {
do { do {
let handshakeInit = try self.noiseManagers["Alice"]!.initiateHandshake(with: TestConstants.testPeerID2) let handshakeInit = try self.noiseManagers["Alice"]!.initiateHandshake(with: TestConstants.testPeerID2)
let handshakePacket = TestHelpers.createTestPacket( let handshakePacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeInit.rawValue, type: 0x05, // noiseHandshakeInit was removed
payload: handshakeInit payload: handshakeInit
) )
self.nodes["Bob"]!.simulateIncomingPacket(handshakePacket) self.nodes["Bob"]!.simulateIncomingPacket(handshakePacket)
} catch { } catch {
XCTFail("Alice failed to initiate handshake: \(error)") XCTFail("Alice failed to initiate handshake: \(error)")
} }
} else if packet.type == MessageType.noiseHandshakeResp.rawValue { } else if packet.type == 0x06 // noiseHandshakeResp was removed {
// Complete handshake // Complete handshake
do { do {
let final = try self.noiseManagers["Alice"]!.handleIncomingHandshake( let final = try self.noiseManagers["Alice"]!.handleIncomingHandshake(
@@ -606,7 +606,7 @@ final class IntegrationTests: XCTestCase {
) )
if let finalMsg = final { if let finalMsg = final {
let finalPacket = TestHelpers.createTestPacket( let finalPacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeResp.rawValue, type: 0x06, // noiseHandshakeResp was removed
payload: finalMsg payload: finalMsg
) )
self.nodes["Bob"]!.simulateIncomingPacket(finalPacket) self.nodes["Bob"]!.simulateIncomingPacket(finalPacket)
@@ -7,139 +7,8 @@
// //
import Foundation import Foundation
import MultipeerConnectivity import CoreBluetooth
@testable import bitchat @testable import bitchat
class MockBluetoothMeshService: BluetoothMeshService { // Compatibility wrapper for old tests - please use MockSimplifiedBluetoothService directly
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = [] typealias MockBluetoothMeshService = MockSimplifiedBluetoothService
var sentPackets: [BitchatPacket] = []
var connectedPeers: Set<String> = []
var messageDeliveryHandler: ((BitchatMessage) -> Void)?
var packetDeliveryHandler: ((BitchatPacket) -> Void)?
// Override these properties
var mockNickname: String = "MockUser"
override var myPeerID: String {
didSet {
// Update when changed
}
}
var nickname: String {
return mockNickname
}
var peerID: String {
return myPeerID
}
override init() {
super.init()
self.myPeerID = "MOCK1234"
}
func simulateConnectedPeer(_ peerID: String) {
connectedPeers.insert(peerID)
delegate?.didConnectToPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
func simulateDisconnectedPeer(_ peerID: String) {
connectedPeers.remove(peerID)
delegate?.didDisconnectFromPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
override func sendMessage(_ content: String, mentions: [String], to room: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
let message = BitchatMessage(
id: messageID ?? UUID().uuidString,
sender: mockNickname,
content: content,
timestamp: timestamp ?? Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: myPeerID,
mentions: mentions.isEmpty ? nil : mentions
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
override func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
let message = BitchatMessage(
id: messageID ?? UUID().uuidString,
sender: mockNickname,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: recipientNickname,
senderPeerID: myPeerID,
mentions: nil
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: recipientPeerID.data(using: .utf8)!,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
func simulateIncomingMessage(_ message: BitchatMessage) {
delegate?.didReceiveMessage(message)
}
func simulateIncomingPacket(_ packet: BitchatPacket) {
// Process through the actual handling logic
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
delegate?.didReceiveMessage(message)
}
packetDeliveryHandler?(packet)
}
func getConnectedPeers() -> [String] {
return Array(connectedPeers)
}
}
@@ -0,0 +1,227 @@
//
// MockSimplifiedBluetoothService.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import CoreBluetooth
@testable import bitchat
// Mock implementation that mimics SimplifiedBluetoothService behavior
class MockSimplifiedBluetoothService: NSObject {
// MARK: - Properties matching SimplifiedBluetoothService
weak var delegate: BitchatDelegate?
var myPeerID: String = "MOCK1234"
var myNickname: String = "MockUser"
// Test-specific properties
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
var sentPackets: [BitchatPacket] = []
var connectedPeers: Set<String> = []
var messageDeliveryHandler: ((BitchatMessage) -> Void)?
var packetDeliveryHandler: ((BitchatPacket) -> Void)?
// Compatibility properties for old tests
var mockNickname: String {
get { return myNickname }
set { myNickname = newValue }
}
var nickname: String {
return myNickname
}
var peerID: String {
return myPeerID
}
// MARK: - Initialization
override init() {
super.init()
}
// MARK: - Methods matching SimplifiedBluetoothService
func setNickname(_ nickname: String) {
self.myNickname = nickname
}
func startServices() {
// Mock implementation - do nothing
}
func stopServices() {
// Mock implementation - do nothing
}
func isPeerConnected(_ peerID: String) -> Bool {
return connectedPeers.contains(peerID)
}
func getPeerNicknames() -> [String: String] {
var nicknames: [String: String] = [:]
for peer in connectedPeers {
nicknames[peer] = "MockPeer_\(peer)"
}
return nicknames
}
func getPeers() -> [String: String] {
return getPeerNicknames()
}
func sendMessage(_ content: String, mentions: [String] = [], to recipientID: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
let message = BitchatMessage(
id: messageID ?? UUID().uuidString,
sender: myNickname,
content: content,
timestamp: timestamp ?? Date(),
isRelay: false,
originalSender: nil,
isPrivate: recipientID != nil,
recipientNickname: nil,
senderPeerID: myPeerID,
mentions: mentions.isEmpty ? nil : mentions
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: recipientID?.data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String) {
let message = BitchatMessage(
id: messageID,
sender: myNickname,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: recipientNickname,
senderPeerID: myPeerID,
mentions: nil
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: recipientPeerID.data(using: .utf8)!,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
// Mock implementation
}
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
// Mock implementation
}
func sendBroadcastAnnounce() {
// Mock implementation
}
func getPeerFingerprint(_ peerID: String) -> String? {
return nil
}
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
return .none
}
func triggerHandshake(with peerID: String) {
// Mock implementation
}
func emergencyDisconnectAll() {
connectedPeers.removeAll()
delegate?.didUpdatePeerList([])
}
func getNoiseService() -> NoiseEncryptionService {
return NoiseEncryptionService()
}
func getFingerprint(for peerID: String) -> String? {
return nil
}
// MARK: - Test Helper Methods
func simulateConnectedPeer(_ peerID: String) {
connectedPeers.insert(peerID)
delegate?.didConnectToPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
func simulateDisconnectedPeer(_ peerID: String) {
connectedPeers.remove(peerID)
delegate?.didDisconnectFromPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
func simulateIncomingMessage(_ message: BitchatMessage) {
delegate?.didReceiveMessage(message)
}
func simulateIncomingPacket(_ packet: BitchatPacket) {
// Process through the actual handling logic
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
delegate?.didReceiveMessage(message)
}
packetDeliveryHandler?(packet)
}
func getConnectedPeers() -> [String] {
return Array(connectedPeers)
}
// MARK: - Compatibility methods for old tests
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
sendPrivateMessage(content, to: recipientPeerID, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString)
}
}
+10 -6
View File
@@ -13,8 +13,7 @@ final class NostrProtocolTests: XCTestCase {
override func setUp() { override func setUp() {
super.setUp() super.setUp()
// Enable secure logging for tests // SecureLogger is always enabled
SecureLogger.enableLogging()
} }
func testNIP17MessageRoundTrip() throws { func testNIP17MessageRoundTrip() throws {
@@ -39,7 +38,7 @@ final class NostrProtocolTests: XCTestCase {
print("Gift wrap pubkey: \(giftWrap.pubkey)") print("Gift wrap pubkey: \(giftWrap.pubkey)")
// Decrypt the gift wrap // Decrypt the gift wrap
let (decryptedContent, senderPubkey) = try NostrProtocol.decryptPrivateMessage( let (decryptedContent, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap, giftWrap: giftWrap,
recipientIdentity: recipient recipientIdentity: recipient
) )
@@ -48,7 +47,12 @@ final class NostrProtocolTests: XCTestCase {
XCTAssertEqual(decryptedContent, originalContent) XCTAssertEqual(decryptedContent, originalContent)
XCTAssertEqual(senderPubkey, sender.publicKeyHex) XCTAssertEqual(senderPubkey, sender.publicKeyHex)
print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey)") // Verify timestamp is reasonable (within last minute)
let messageDate = Date(timeIntervalSince1970: TimeInterval(timestamp))
let timeDiff = abs(messageDate.timeIntervalSinceNow)
XCTAssertLessThan(timeDiff, 60, "Message timestamp should be recent")
print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey) at \(messageDate)")
} }
func testGiftWrapUsesUniqueEphemeralKeys() throws { func testGiftWrapUsesUniqueEphemeralKeys() throws {
@@ -75,11 +79,11 @@ final class NostrProtocolTests: XCTestCase {
print("Message 2 gift wrap pubkey: \(message2.pubkey)") print("Message 2 gift wrap pubkey: \(message2.pubkey)")
// Both should decrypt successfully // Both should decrypt successfully
let (content1, _) = try NostrProtocol.decryptPrivateMessage( let (content1, _, _) = try NostrProtocol.decryptPrivateMessage(
giftWrap: message1, giftWrap: message1,
recipientIdentity: recipient recipientIdentity: recipient
) )
let (content2, _) = try NostrProtocol.decryptPrivateMessage( let (content2, _, _) = try NostrProtocol.decryptPrivateMessage(
giftWrap: message2, giftWrap: message2,
recipientIdentity: recipient recipientIdentity: recipient
) )
@@ -0,0 +1,282 @@
//
// SimplifiedBluetoothServiceTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CoreBluetooth
@testable import bitchat
final class SimplifiedBluetoothServiceTests: XCTestCase {
var service: MockSimplifiedBluetoothService!
override func setUp() {
super.setUp()
service = MockSimplifiedBluetoothService()
service.myPeerID = "TEST1234"
service.mockNickname = "TestUser"
}
override func tearDown() {
service = nil
super.tearDown()
}
// MARK: - Basic Functionality Tests
func testServiceInitialization() {
XCTAssertNotNil(service)
XCTAssertEqual(service.myPeerID, "TEST1234")
XCTAssertEqual(service.myNickname, "TestUser")
}
func testPeerConnection() {
// Test connecting a peer
service.simulateConnectedPeer("PEER5678")
XCTAssertTrue(service.isPeerConnected("PEER5678"))
XCTAssertEqual(service.getConnectedPeers().count, 1)
// Test disconnecting a peer
service.simulateDisconnectedPeer("PEER5678")
XCTAssertFalse(service.isPeerConnected("PEER5678"))
XCTAssertEqual(service.getConnectedPeers().count, 0)
}
func testMultiplePeerConnections() {
service.simulateConnectedPeer("PEER1")
service.simulateConnectedPeer("PEER2")
service.simulateConnectedPeer("PEER3")
XCTAssertEqual(service.getConnectedPeers().count, 3)
XCTAssertTrue(service.isPeerConnected("PEER1"))
XCTAssertTrue(service.isPeerConnected("PEER2"))
XCTAssertTrue(service.isPeerConnected("PEER3"))
service.simulateDisconnectedPeer("PEER2")
XCTAssertEqual(service.getConnectedPeers().count, 2)
XCTAssertFalse(service.isPeerConnected("PEER2"))
}
// MARK: - Message Sending Tests
func testSendPublicMessage() {
let expectation = XCTestExpectation(description: "Message sent")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Hello, world!")
XCTAssertEqual(message.sender, "TestUser")
XCTAssertFalse(message.isPrivate)
expectation.fulfill()
}
service.sendMessage("Hello, world!")
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(service.sentMessages.count, 1)
}
func testSendPrivateMessage() {
let expectation = XCTestExpectation(description: "Private message sent")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Secret message")
XCTAssertEqual(message.sender, "TestUser")
XCTAssertTrue(message.isPrivate)
XCTAssertEqual(message.recipientNickname, "Bob")
expectation.fulfill()
}
service.sendPrivateMessage("Secret message", to: "PEER5678", recipientNickname: "Bob", messageID: "MSG123")
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(service.sentMessages.count, 1)
}
func testSendMessageWithMentions() {
let expectation = XCTestExpectation(description: "Message with mentions sent")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "@alice @bob check this out")
XCTAssertEqual(message.mentions, ["alice", "bob"])
expectation.fulfill()
}
service.sendMessage("@alice @bob check this out", mentions: ["alice", "bob"])
wait(for: [expectation], timeout: 1.0)
}
// MARK: - Message Reception Tests
func testSimulateIncomingMessage() {
let expectation = XCTestExpectation(description: "Message received")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Incoming message")
XCTAssertEqual(message.sender, "RemoteUser")
expectation.fulfill()
}
let incomingMessage = BitchatMessage(
id: "MSG456",
sender: "RemoteUser",
content: "Incoming message",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "REMOTE123",
mentions: nil
)
service.simulateIncomingMessage(incomingMessage)
wait(for: [expectation], timeout: 1.0)
}
func testSimulateIncomingPacket() {
let expectation = XCTestExpectation(description: "Packet processed")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Packet message")
expectation.fulfill()
}
let message = BitchatMessage(
id: "MSG789",
sender: "PacketSender",
content: "Packet message",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "PACKET123",
mentions: nil
)
guard let payload = message.toBinaryPayload() else {
XCTFail("Failed to create binary payload")
return
}
let packet = BitchatPacket(
type: 0x01,
senderID: "PACKET123".data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
service.simulateIncomingPacket(packet)
wait(for: [expectation], timeout: 1.0)
}
// MARK: - Peer Nickname Tests
func testGetPeerNicknames() {
service.simulateConnectedPeer("PEER1")
service.simulateConnectedPeer("PEER2")
let nicknames = service.getPeerNicknames()
XCTAssertEqual(nicknames.count, 2)
XCTAssertEqual(nicknames["PEER1"], "MockPeer_PEER1")
XCTAssertEqual(nicknames["PEER2"], "MockPeer_PEER2")
}
// MARK: - Service State Tests
func testStartStopServices() {
// These are mock implementations, just ensure they don't crash
service.startServices()
service.stopServices()
// Service should still be functional after start/stop
service.simulateConnectedPeer("PEER999")
XCTAssertTrue(service.isPeerConnected("PEER999"))
}
// MARK: - Message Delivery Handler Tests
func testMessageDeliveryHandler() {
let expectation = XCTestExpectation(description: "Delivery handler called")
service.messageDeliveryHandler = { message in
XCTAssertEqual(message.content, "Test delivery")
expectation.fulfill()
}
service.sendMessage("Test delivery")
wait(for: [expectation], timeout: 1.0)
}
func testPacketDeliveryHandler() {
let expectation = XCTestExpectation(description: "Packet handler called")
service.packetDeliveryHandler = { packet in
XCTAssertEqual(packet.type, 0x01)
expectation.fulfill()
}
let message = BitchatMessage(
id: "PKT123",
sender: "TestSender",
content: "Test packet",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "TEST123",
mentions: nil
)
guard let payload = message.toBinaryPayload() else {
XCTFail("Failed to create payload")
return
}
let packet = BitchatPacket(
type: 0x01,
senderID: "TEST123".data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
service.simulateIncomingPacket(packet)
wait(for: [expectation], timeout: 1.0)
}
}
// MARK: - Mock Delegate Helper
private class MockBitchatDelegate: BitchatDelegate {
private let messageHandler: (BitchatMessage) -> Void
init(_ handler: @escaping (BitchatMessage) -> Void) {
self.messageHandler = handler
}
func didReceiveMessage(_ message: BitchatMessage) {
messageHandler(message)
}
func didConnectToPeer(_ peerID: String) {}
func didDisconnectFromPeer(_ peerID: String) {}
func didUpdatePeerList(_ peers: [String]) {}
func isFavorite(fingerprint: String) -> Bool { return false }
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
}
@@ -0,0 +1,45 @@
//
// LegacyTestProtocolTypes.swift
// bitchatTests
//
// Minimal legacy protocol types used only by tests to simulate old flows.
// These are not part of production code anymore.
import Foundation
struct ProtocolNack {
let originalPacketID: String
let nackID: String
let senderID: String
let receiverID: String
let packetType: UInt8
let reason: String
let errorCode: UInt8
enum ErrorCode: UInt8 {
case unknown = 0
case decryptionFailed = 2
}
init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) {
self.originalPacketID = originalPacketID
self.nackID = UUID().uuidString
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.reason = reason
self.errorCode = errorCode.rawValue
}
func toBinaryData() -> Data {
// Tests don't parse the payload; return a compact encoding for completeness
var data = Data()
data.appendUUID(originalPacketID)
data.appendUUID(nackID)
data.append(UInt8(packetType))
data.append(UInt8(errorCode))
data.appendString(reason)
return data
}
}