mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Refactor/ble nostr boundaries (#449)
* Refactor: move Nostr embedding and TLVs out of BLE; add NostrEmbeddedBitChat, Packets, PeerIDUtils; centralize MessageDeduplicator; update ChatViewModel to use new helpers; remove AI_CONTEXT.md and CLAUDE.md * Rename class to BLEService with compatibility alias; move mention parsing out of BLE; emit low-level BLE events to delegate; unify hex helpers; accept 64-hex in isPeerConnected; add PeerIDResolver * Project: rename file to BLEService.swift and update Xcode project; keep typealias SimplifiedBluetoothService = BLEService for compatibility * Remove SimplifiedBluetoothService alias; update app code to use BLEService explicitly * Tests: rename MockSimplifiedBluetoothService to MockBLEService; update typealiases and Xcode project * Docs: update comments to refer to BLEService (tests, protocol, noise service) * Tests: rename SimplifiedBluetoothServiceTests to BLEServiceTests; update project references and class names * Introduce Transport protocol; BLEService conforms; document delegate-only event pattern in BLEService; keep publishers internal for UnifiedPeerService * Adopt Transport end-to-end: add TransportPeerSnapshot + publishers; BLEService maps to Transport snapshots; UnifiedPeerService consumes Transport; ChatViewModel holds Transport * Fix Transport integration: replace getPeerFingerprint with getFingerprint(for:); update PrivateChatManager and CommandProcessor to use Transport; add BLEService.getFingerprint(for:); update PeerManager to use Transport * Refactor transport and BLE/Nostr layers; unify UI events; fix MainActor isolation - Rename SimplifiedBluetoothService to BLEService and slim responsibilities - Introduce Transport protocol and peerEventsDelegate for UI updates - Add NostrTransport and MessageRouter to route PM/read/favorite via BLE or Nostr - Centralize TLVs, PeerID utils, and MessageDeduplicator outside BLE - Update UnifiedPeerService and ChatViewModel to use Transport and delegate events - Fix MainActor isolation: route delegate calls via Task on MainActor; update notifyUI helper - Adjust related files and tests accordingly * BLEService: remove internal publishers; switch to delegate-only events - Drop legacy messages/peers/fullPeers publishers - Provide lightweight peerSnapshotSubject only to satisfy Transport - Rework publishFullPeerData to build snapshots from internal state and notify delegate + subject - Remove all peersPublisher.send call sites - Keep UnifiedPeerService on delegate updates exclusively * Remove inlined Nostr send helpers from ChatViewModel; route via MessageRouter - Replace direct Nostr sends (PM, ACKs, favorites) with MessageRouter - Add router method for delivery ACKs and implement NostrTransport.sendDeliveryAck - Simplify ChatViewModel favorite notification path to use router - Keep Nostr receive handling intact; reduce duplication * Fix ReadReceipt initializer usage in ChatViewModel (readerID + readerNickname) * Fix unused variable warning: replace shadowed 'nostrPubkey' bind with boolean check in ChatViewModel * Fix queued PM format: use TLV for pending messages after Noise handshake - Pending messages (including first-time favorite notifications) now use the same TLV encoding as normal sends - Ensures ChatViewModel can decode on first send, even if handshake completes after queuing --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ plans/
|
|||||||
|
|
||||||
## AI
|
## AI
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
|
||||||
## User settings
|
## User settings
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
|||||||
-469
@@ -1,469 +0,0 @@
|
|||||||
# AI Context for BitChat
|
|
||||||
|
|
||||||
This document provides essential context for AI assistants working on the BitChat codebase. Read this first to understand the project's architecture, design decisions, and key concepts.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
BitChat is a decentralized, peer-to-peer messaging application that works over Bluetooth mesh networks without requiring internet connectivity, servers, or phone numbers. It's designed for scenarios where traditional communication infrastructure is unavailable or untrusted.
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
- **Bluetooth Mesh Networking**: Multi-hop message relay over BLE
|
|
||||||
- **Privacy-First Design**: No accounts, no persistent identifiers
|
|
||||||
- **End-to-End Encryption**: Uses Noise Protocol Framework for private messages
|
|
||||||
- **IRC-Style Commands**: Familiar `/msg`, `/who` interface
|
|
||||||
- **Cross-Platform**: Native iOS and macOS support
|
|
||||||
- **Nostr Integration**: Seamless fallback for mutual favorites when out of Bluetooth range
|
|
||||||
- **Hybrid Transport**: Automatic switching between Bluetooth and Nostr
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ User Interface │
|
|
||||||
│ (ContentView, ChatViewModel) │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Application Services │
|
|
||||||
│ (MessageRetryService, DeliveryTracker, NotificationService) │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Message Router │
|
|
||||||
│ (Transport selection, Favorites integration) │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│ │
|
|
||||||
┌───────────────────────────────┐ ┌────────────────────────────────┐
|
|
||||||
│ Security Layer │ │ Nostr Protocol Layer │
|
|
||||||
│ (NoiseEncryptionService, │ │ (NostrProtocol, NIP-17, │
|
|
||||||
│ SecureIdentityStateManager) │ │ NostrRelayManager) │
|
|
||||||
└───────────────────────────────┘ └────────────────────────────────┘
|
|
||||||
│ │
|
|
||||||
┌───────────────────────────────┐ ┌────────────────────────────────┐
|
|
||||||
│ Protocol Layer │ │ Transport │
|
|
||||||
│ (BitchatProtocol, Binary- │ │ (WebSocket to Nostr │
|
|
||||||
│ Protocol, NoiseProtocol) │ │ relay servers) │
|
|
||||||
└───────────────────────────────┘ └────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Bluetooth Transport Layer │
|
|
||||||
│ (SimplifiedBluetoothService) │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### 1. SimplifiedBluetoothService (Transport Layer)
|
|
||||||
- **Location**: `bitchat/Services/SimplifiedBluetoothService.swift`
|
|
||||||
- **Purpose**: Manages BLE connections and implements mesh networking
|
|
||||||
- **Key Responsibilities**:
|
|
||||||
- Peer discovery (scanning and advertising simultaneously)
|
|
||||||
- Connection management (acts as both central and peripheral)
|
|
||||||
- Message routing and relay
|
|
||||||
- Version negotiation with peers
|
|
||||||
- Automatic reconnection and topology management
|
|
||||||
|
|
||||||
### 2. BitchatProtocol (Protocol Layer)
|
|
||||||
- **Location**: `bitchat/Protocols/BitchatProtocol.swift`
|
|
||||||
- **Purpose**: Defines the application-level messaging protocol
|
|
||||||
- **Key Features**:
|
|
||||||
- Binary packet format for efficiency
|
|
||||||
- Message types: Chat, Announcement, PrivateMessage, etc.
|
|
||||||
- TTL-based routing (max 7 hops)
|
|
||||||
- Message deduplication via unique IDs
|
|
||||||
- Privacy features: padding, timing obfuscation
|
|
||||||
|
|
||||||
### 3. NoiseProtocol Implementation
|
|
||||||
- **Locations**:
|
|
||||||
- `bitchat/Noise/NoiseProtocol.swift` - Core protocol implementation
|
|
||||||
- `bitchat/Noise/NoiseSession.swift` - Session management
|
|
||||||
- `bitchat/Services/NoiseEncryptionService.swift` - High-level encryption API
|
|
||||||
- **Purpose**: Provides end-to-end encryption for private messages
|
|
||||||
- **Implementation Details**:
|
|
||||||
- Uses Noise_XX_25519_AESGCM_SHA256 pattern
|
|
||||||
- Mutual authentication via static keys
|
|
||||||
- Forward secrecy via ephemeral keys
|
|
||||||
- Integrated with identity management
|
|
||||||
|
|
||||||
### 4. Identity System
|
|
||||||
- **Location**: `bitchat/Identity/`
|
|
||||||
- **Three-Layer Model**:
|
|
||||||
1. **Ephemeral Identity**: Short-lived, rotates frequently
|
|
||||||
2. **Cryptographic Identity**: Long-term Noise static keypair
|
|
||||||
3. **Social Identity**: User-chosen nickname and metadata
|
|
||||||
- **Trust Levels**: Untrusted → Verified → Trusted → Blocked
|
|
||||||
|
|
||||||
### 5. ChatViewModel
|
|
||||||
- **Location**: `bitchat/ViewModels/ChatViewModel.swift`
|
|
||||||
- **Purpose**: Central state management and business logic
|
|
||||||
- **Responsibilities**:
|
|
||||||
- Message handling and batching
|
|
||||||
- Command processing (/msg, /who, etc.)
|
|
||||||
- UI state management
|
|
||||||
- Private chat coordination
|
|
||||||
|
|
||||||
### 6. Nostr Integration
|
|
||||||
- **Locations**:
|
|
||||||
- `bitchat/Nostr/NostrProtocol.swift` - NIP-17 private message implementation
|
|
||||||
- `bitchat/Nostr/NostrRelayManager.swift` - WebSocket relay connections
|
|
||||||
- `bitchat/Nostr/NostrIdentity.swift` - Nostr key management
|
|
||||||
- `bitchat/Services/MessageRouter.swift` - Transport selection logic
|
|
||||||
- **Purpose**: Enables communication with mutual favorites when out of Bluetooth range
|
|
||||||
- **Key Features**:
|
|
||||||
- NIP-17 gift-wrapped private messages for metadata privacy
|
|
||||||
- Automatic relay connection management
|
|
||||||
- Seamless transport switching between Bluetooth and Nostr
|
|
||||||
- Integrated with favorites system for mutual authentication
|
|
||||||
|
|
||||||
### 7. MessageRouter
|
|
||||||
- **Location**: `bitchat/Services/MessageRouter.swift`
|
|
||||||
- **Purpose**: Intelligent routing between Bluetooth mesh and Nostr transports
|
|
||||||
- **Transport Selection Logic**:
|
|
||||||
1. Always prefer Bluetooth mesh when peer is connected
|
|
||||||
2. Use Nostr for mutual favorites when peer is offline
|
|
||||||
3. Fail gracefully when no transport is available
|
|
||||||
- **Message Types Routed**:
|
|
||||||
- Regular chat messages
|
|
||||||
- Favorite/unfavorite notifications
|
|
||||||
- Delivery acknowledgments
|
|
||||||
- Read receipts
|
|
||||||
|
|
||||||
## Key Design Decisions
|
|
||||||
|
|
||||||
### 1. Protocol Design
|
|
||||||
- **Binary Protocol**: Chosen for efficiency over BLE's limited bandwidth
|
|
||||||
- **No JSON**: Reduces parsing overhead and message size
|
|
||||||
- **Custom Framing**: Handles BLE's 512-byte MTU limitations
|
|
||||||
|
|
||||||
### 2. Security Architecture
|
|
||||||
- **Noise Protocol**: Industry-standard, well-analyzed framework
|
|
||||||
- **XX Pattern**: Provides mutual authentication and forward secrecy
|
|
||||||
- **No Long-Term Identifiers**: Enhances privacy and deniability
|
|
||||||
|
|
||||||
### 3. Mesh Networking
|
|
||||||
- **TTL-Based Routing**: Prevents infinite loops in mesh
|
|
||||||
- **Bloom Filters**: Efficient duplicate detection
|
|
||||||
|
|
||||||
### 4. Privacy Features
|
|
||||||
- **Message Padding**: Obscures message length
|
|
||||||
- **Timing Obfuscation**: Randomized delays
|
|
||||||
- **Emergency Wipe**: Triple-tap to clear all data
|
|
||||||
|
|
||||||
### 5. Nostr Integration
|
|
||||||
- **NIP-17 Gift Wraps**: Maximum metadata privacy
|
|
||||||
- **Ephemeral Keys**: Each message uses unique ephemeral keys
|
|
||||||
- **Mutual Favorites Only**: Requires bidirectional trust
|
|
||||||
- **Transport Abstraction**: Users don't need to know about Nostr
|
|
||||||
|
|
||||||
## Code Organization
|
|
||||||
|
|
||||||
### Services (`/bitchat/Services/`)
|
|
||||||
Application-level services that coordinate between layers:
|
|
||||||
- `SimplifiedBluetoothService`: Core networking
|
|
||||||
- `NoiseEncryptionService`: Encryption coordination
|
|
||||||
- `MessageRetryService`: Reliability layer
|
|
||||||
- `DeliveryTracker`: Acknowledgment handling
|
|
||||||
- `NotificationService`: System notifications
|
|
||||||
|
|
||||||
### Protocols (`/bitchat/Protocols/`)
|
|
||||||
Protocol definitions and implementations:
|
|
||||||
- `BitchatProtocol`: Application protocol
|
|
||||||
- `BinaryProtocol`: Low-level encoding
|
|
||||||
- `BinaryEncodingUtils`: Helper functions
|
|
||||||
|
|
||||||
### Noise (`/bitchat/Noise/`)
|
|
||||||
Noise Protocol Framework implementation:
|
|
||||||
- `NoiseProtocol`: Core cryptographic operations
|
|
||||||
- `NoiseSession`: Session state management
|
|
||||||
- `NoiseHandshakeCoordinator`: Handshake orchestration
|
|
||||||
- `NoiseSecurityConsiderations`: Security validations
|
|
||||||
|
|
||||||
### Views & ViewModels
|
|
||||||
MVVM architecture for UI:
|
|
||||||
- `ContentView`: Main chat interface
|
|
||||||
- `ChatViewModel`: Business logic and state
|
|
||||||
- Supporting views for settings, identity, etc.
|
|
||||||
|
|
||||||
## Nostr Protocol Implementation
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
BitChat integrates Nostr as a secondary transport for communicating with mutual favorites when Bluetooth connectivity is unavailable. This integration is transparent to users - messages automatically route through Nostr when needed.
|
|
||||||
|
|
||||||
### NIP-17 Private Direct Messages
|
|
||||||
BitChat implements NIP-17 (Private Direct Messages) for metadata-private communication:
|
|
||||||
|
|
||||||
1. **Gift Wrap Structure**:
|
|
||||||
```
|
|
||||||
Gift Wrap (kind 1059) → Seal (kind 13) → Rumor (kind 1)
|
|
||||||
```
|
|
||||||
- **Rumor**: The actual message content (unsigned)
|
|
||||||
- **Seal**: Encrypted rumor, hides sender identity
|
|
||||||
- **Gift Wrap**: Double-encrypted, tagged for recipient
|
|
||||||
|
|
||||||
2. **Ephemeral Keys**:
|
|
||||||
- Each message uses TWO ephemeral key pairs
|
|
||||||
- Seal uses one ephemeral key
|
|
||||||
- Gift wrap uses a different ephemeral key
|
|
||||||
- Provides sender anonymity and forward secrecy
|
|
||||||
|
|
||||||
3. **Timestamp Randomization**:
|
|
||||||
- ±1 minute randomization (reduced from NIP-17's ±15 minutes)
|
|
||||||
- Prevents timing correlation attacks
|
|
||||||
- Configurable in `NostrProtocol.randomizedTimestamp()`
|
|
||||||
|
|
||||||
### Favorites Integration
|
|
||||||
|
|
||||||
The Nostr transport is only available for mutual favorites:
|
|
||||||
|
|
||||||
1. **Favorite Establishment**:
|
|
||||||
- User favorites a peer via `/fav` command
|
|
||||||
- Favorite notification sent via Bluetooth (if connected)
|
|
||||||
- Peer's Nostr public key exchanged during favorite process
|
|
||||||
- Stored in `FavoritesPersistenceService`
|
|
||||||
|
|
||||||
2. **Mutual Requirement**:
|
|
||||||
- Both peers must favorite each other
|
|
||||||
- Prevents spam and unwanted Nostr messages
|
|
||||||
- Enforced by `MessageRouter` transport selection
|
|
||||||
|
|
||||||
3. **Nostr Key Management**:
|
|
||||||
- Derived from Noise static key using BIP-32
|
|
||||||
- Path: `m/44'/1237'/0'/0/0` (1237 = "NOSTR" in decimal)
|
|
||||||
- Consistent npub across app reinstalls
|
|
||||||
- Keys never leave the device
|
|
||||||
|
|
||||||
### Message Routing Logic
|
|
||||||
|
|
||||||
`MessageRouter` automatically selects transport:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
if peerAvailableOnMesh {
|
|
||||||
transport = .bluetoothMesh // Always prefer mesh
|
|
||||||
} else if isMutualFavorite {
|
|
||||||
transport = .nostr // Use Nostr for offline favorites
|
|
||||||
} else {
|
|
||||||
throw MessageRouterError.peerNotReachable
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Relay Configuration
|
|
||||||
|
|
||||||
Default relays (hardcoded for reliability):
|
|
||||||
- `wss://relay.damus.io`
|
|
||||||
- `wss://relay.primal.net`
|
|
||||||
- `wss://offchain.pub`
|
|
||||||
- `wss://nostr21.com`
|
|
||||||
|
|
||||||
Relay selection criteria:
|
|
||||||
- Geographic distribution
|
|
||||||
- High uptime
|
|
||||||
- No authentication required
|
|
||||||
- Support for ephemeral events
|
|
||||||
|
|
||||||
### Message Format
|
|
||||||
|
|
||||||
Structured content for different message types:
|
|
||||||
- Chat: `MSG:<messageID>:<content>`
|
|
||||||
- Favorite: `FAVORITED:<senderNpub>` or `UNFAVORITED:<senderNpub>`
|
|
||||||
- Delivery ACK: `DELIVERED:<messageID>`
|
|
||||||
- Read Receipt: `READ:<base64EncodedReceipt>`
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
1. **NostrRelayManager**:
|
|
||||||
- Manages WebSocket connections to relays
|
|
||||||
- Handles reconnection logic
|
|
||||||
- Processes EVENT, EOSE, OK, NOTICE messages
|
|
||||||
- Implements NIP-01 relay protocol
|
|
||||||
|
|
||||||
2. **NostrProtocol**:
|
|
||||||
- Implements NIP-17 encryption/decryption
|
|
||||||
- Handles gift wrap creation/unwrapping
|
|
||||||
- Manages ephemeral key generation
|
|
||||||
- Provides Schnorr signatures
|
|
||||||
|
|
||||||
3. **ProcessedMessagesService**:
|
|
||||||
- Prevents duplicate message processing
|
|
||||||
- Tracks last subscription timestamp
|
|
||||||
- Persists across app launches
|
|
||||||
- 30-day retention window
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
|
|
||||||
1. **Metadata Protection**:
|
|
||||||
- Sender identity hidden via ephemeral keys
|
|
||||||
- Recipient only visible in gift wrap p-tag
|
|
||||||
- Timing correlation prevented via randomization
|
|
||||||
- Message content double-encrypted
|
|
||||||
|
|
||||||
2. **Relay Trust**:
|
|
||||||
- Relays cannot read message content
|
|
||||||
- Relays can see recipient pubkey (gift wrap)
|
|
||||||
- Relays cannot determine sender
|
|
||||||
- Multiple relays used for redundancy
|
|
||||||
|
|
||||||
3. **Key Hygiene**:
|
|
||||||
- Ephemeral keys used once and discarded
|
|
||||||
- Static Nostr key derived from Noise key
|
|
||||||
- No key reuse between messages
|
|
||||||
- Keys cleared from memory after use
|
|
||||||
|
|
||||||
### Debugging Nostr Issues
|
|
||||||
|
|
||||||
1. **Check relay connections**:
|
|
||||||
- Look for "Connected to Nostr relay" in logs
|
|
||||||
- Verify WebSocket state in NostrRelayManager
|
|
||||||
- Check for relay errors/notices
|
|
||||||
|
|
||||||
2. **Verify gift wrap creation**:
|
|
||||||
- Enable debug logging in NostrProtocol
|
|
||||||
- Check ephemeral key generation
|
|
||||||
- Verify encryption steps
|
|
||||||
|
|
||||||
3. **Message delivery**:
|
|
||||||
- Check ProcessedMessagesService for duplicates
|
|
||||||
- Verify subscription filters
|
|
||||||
- Look for EVENT messages in relay responses
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### 1. Security First
|
|
||||||
- Never log sensitive data (keys, message content)
|
|
||||||
- Use `SecureLogger` for security-aware logging
|
|
||||||
- Validate all inputs from network
|
|
||||||
- Follow principle of least privilege
|
|
||||||
|
|
||||||
### 2. Performance Considerations
|
|
||||||
- BLE has limited bandwidth (~20KB/s practical)
|
|
||||||
- Minimize protocol overhead
|
|
||||||
- Batch operations where possible
|
|
||||||
- Use compression for large messages
|
|
||||||
|
|
||||||
### 3. Testing
|
|
||||||
- Unit tests for protocol logic
|
|
||||||
- Integration tests for service interactions
|
|
||||||
- End-to-end tests for user flows
|
|
||||||
- Mock objects for BLE testing
|
|
||||||
|
|
||||||
### 4. Error Handling
|
|
||||||
- Graceful degradation for network issues
|
|
||||||
- Clear error messages for users
|
|
||||||
- Automatic retry with backoff
|
|
||||||
- Never expose internal errors
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
### Adding a New Message Type
|
|
||||||
1. Define in `MessageType` enum in `BitchatProtocol.swift`
|
|
||||||
2. Implement encoding/decoding logic
|
|
||||||
3. Add handling in `ChatViewModel`
|
|
||||||
4. Update UI if needed
|
|
||||||
5. Add tests
|
|
||||||
|
|
||||||
### Implementing a New Command
|
|
||||||
1. Add to `ChatViewModel.processCommand()`
|
|
||||||
2. Define any new message types needed
|
|
||||||
3. Implement command logic
|
|
||||||
4. Add autocomplete support
|
|
||||||
5. Update help text
|
|
||||||
|
|
||||||
### Debugging Bluetooth Issues
|
|
||||||
1. Check `SimplifiedBluetoothService` logs
|
|
||||||
2. Verify peer states and connections
|
|
||||||
3. Monitor characteristic updates
|
|
||||||
4. Use Bluetooth debugging tools
|
|
||||||
|
|
||||||
### Working with Nostr Transport
|
|
||||||
1. Verify mutual favorite status in `FavoritesPersistenceService`
|
|
||||||
2. Check Nostr key derivation in `NostrIdentity`
|
|
||||||
3. Monitor relay connections in `NostrRelayManager`
|
|
||||||
4. Test gift wrap encryption/decryption
|
|
||||||
5. Verify transport selection in `MessageRouter`
|
|
||||||
|
|
||||||
### Adding Nostr Features
|
|
||||||
1. Understand NIP-17 gift wrap structure
|
|
||||||
2. Maintain ephemeral key hygiene
|
|
||||||
3. Test with multiple relays
|
|
||||||
4. Preserve metadata privacy
|
|
||||||
5. Handle relay disconnections gracefully
|
|
||||||
|
|
||||||
## Security Threat Model
|
|
||||||
|
|
||||||
### Assumptions
|
|
||||||
- Adversaries can intercept all Bluetooth traffic
|
|
||||||
- Devices may be compromised
|
|
||||||
- No trusted infrastructure available
|
|
||||||
|
|
||||||
### Protections
|
|
||||||
- End-to-end encryption for private messages
|
|
||||||
- Message authentication via HMAC
|
|
||||||
- Forward secrecy via ephemeral keys
|
|
||||||
- Deniability through lack of signatures
|
|
||||||
|
|
||||||
### Limitations
|
|
||||||
- Public messages are unencrypted by design
|
|
||||||
- Metadata (who talks to whom) partially visible
|
|
||||||
- Timing attacks possible on mesh network
|
|
||||||
- No protection against flooding/spam (yet)
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
### Implemented
|
|
||||||
- LZ4 compression for messages
|
|
||||||
- Adaptive duty cycling for battery
|
|
||||||
- Connection caching and reuse
|
|
||||||
- Bloom filters for deduplication
|
|
||||||
|
|
||||||
### Future Improvements
|
|
||||||
- Protocol buffer encoding
|
|
||||||
- Better mesh routing algorithms
|
|
||||||
- Predictive pre-connection
|
|
||||||
- Smarter retransmission
|
|
||||||
|
|
||||||
## Troubleshooting Guide
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
1. **Peers not discovering**: Check Bluetooth permissions, ensure app is in foreground
|
|
||||||
2. **Messages not delivering**: Verify mesh connectivity, check TTL values
|
|
||||||
3. **Handshake failures**: Ensure identity state is consistent, check key storage
|
|
||||||
4. **Performance issues**: Monitor connection count, check for message loops
|
|
||||||
|
|
||||||
## External Dependencies
|
|
||||||
|
|
||||||
### Swift Packages
|
|
||||||
- CryptoKit: Apple's crypto framework
|
|
||||||
- Network.framework: For future internet support
|
|
||||||
- No third-party dependencies (by design)
|
|
||||||
|
|
||||||
### System Requirements
|
|
||||||
- iOS 14.0+ / macOS 11.0+
|
|
||||||
- Bluetooth LE hardware
|
|
||||||
- ~50MB storage for app + data
|
|
||||||
|
|
||||||
## Future Roadmap
|
|
||||||
|
|
||||||
### Planned Features
|
|
||||||
- Internet bridging for hybrid networks
|
|
||||||
- Group chat with forward secrecy
|
|
||||||
- Voice messages with Opus codec
|
|
||||||
- File transfer support
|
|
||||||
|
|
||||||
### Architecture Evolution
|
|
||||||
- Plugin system for transports
|
|
||||||
- Modular protocol stack
|
|
||||||
- Cross-platform core library
|
|
||||||
- Federation between networks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start for AI Assistants
|
|
||||||
|
|
||||||
1. **Understand the layers**: Transport → Protocol → Security → Services → UI
|
|
||||||
2. **Follow the data flow**: BLE/Nostr → Binary/JSON → Protocol → ViewModel → View
|
|
||||||
3. **Respect security boundaries**: Never mix trusted and untrusted data
|
|
||||||
4. **Test thoroughly**: This is critical infrastructure for users
|
|
||||||
5. **Ask about design decisions**: Many choices have non-obvious reasons
|
|
||||||
6. **Dual Transport**: Remember that messages can flow over Bluetooth OR Nostr
|
|
||||||
7. **Favorites System**: Nostr only works between mutual favorites
|
|
||||||
|
|
||||||
When in doubt, prioritize security and privacy over features. BitChat users depend on this app in situations where traditional communication has failed them.
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -15,6 +15,22 @@
|
|||||||
049BD3962E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
|
049BD3962E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
|
||||||
049BD3992E506A12001A566B /* UnifiedPeerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3982E506A12001A566B /* UnifiedPeerService.swift */; };
|
049BD3992E506A12001A566B /* UnifiedPeerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3982E506A12001A566B /* UnifiedPeerService.swift */; };
|
||||||
049BD39A2E506A12001A566B /* UnifiedPeerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3982E506A12001A566B /* UnifiedPeerService.swift */; };
|
049BD39A2E506A12001A566B /* UnifiedPeerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3982E506A12001A566B /* UnifiedPeerService.swift */; };
|
||||||
|
049BD39C2E51DBD9001A566B /* NostrEmbeddedBitChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD39B2E51DBD9001A566B /* NostrEmbeddedBitChat.swift */; };
|
||||||
|
049BD39D2E51DBD9001A566B /* NostrEmbeddedBitChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD39B2E51DBD9001A566B /* NostrEmbeddedBitChat.swift */; };
|
||||||
|
049BD3A02E51DBF4001A566B /* Packets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD39E2E51DBF4001A566B /* Packets.swift */; };
|
||||||
|
049BD3A12E51DBF4001A566B /* PeerID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD39F2E51DBF4001A566B /* PeerID.swift */; };
|
||||||
|
049BD3A22E51DBF4001A566B /* Packets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD39E2E51DBF4001A566B /* Packets.swift */; };
|
||||||
|
049BD3A32E51DBF4001A566B /* PeerID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD39F2E51DBF4001A566B /* PeerID.swift */; };
|
||||||
|
049BD3A52E51DC0E001A566B /* MessageDeduplicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */; };
|
||||||
|
049BD3A62E51DC0E001A566B /* MessageDeduplicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */; };
|
||||||
|
049BD3AB2E51E38E001A566B /* PeerIDResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */; };
|
||||||
|
049BD3AC2E51E38E001A566B /* PeerIDResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */; };
|
||||||
|
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3AD2E51ED60001A566B /* Transport.swift */; };
|
||||||
|
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3AD2E51ED60001A566B /* Transport.swift */; };
|
||||||
|
049BD3B22E51F319001A566B /* NostrTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B12E51F319001A566B /* NostrTransport.swift */; };
|
||||||
|
049BD3B32E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; };
|
||||||
|
049BD3B42E51F319001A566B /* NostrTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B12E51F319001A566B /* NostrTransport.swift */; };
|
||||||
|
049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; };
|
||||||
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
||||||
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
||||||
@@ -24,7 +40,7 @@
|
|||||||
2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC75901A0F0073B5BB8356E7 /* TestConstants.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 */; };
|
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */; };
|
||||||
3849CA6D99B2D536636DF4A6 /* MockSimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */; };
|
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */; };
|
||||||
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
|
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
|
||||||
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
|
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 */; };
|
||||||
@@ -36,7 +52,7 @@
|
|||||||
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
|
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
|
||||||
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78595178957244CBDF7E79B6 /* NostrRelayManager.swift */; };
|
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78595178957244CBDF7E79B6 /* NostrRelayManager.swift */; };
|
||||||
6C63FA98D59854C15C57B3D6 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */; };
|
6C63FA98D59854C15C57B3D6 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */; };
|
||||||
6C803BF930E7E19BE6E99EAA /* MockSimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */; };
|
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */; };
|
||||||
6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.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 */; };
|
||||||
@@ -55,7 +71,7 @@
|
|||||||
8C1AB0F2D48207E0755DA91A /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
|
8C1AB0F2D48207E0755DA91A /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
|
||||||
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
|
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
|
||||||
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC75901A0F0073B5BB8356E7 /* TestConstants.swift */; };
|
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC75901A0F0073B5BB8356E7 /* TestConstants.swift */; };
|
||||||
8DE687D2EB5EB120868DBFB5 /* SimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */; };
|
8DE687D2EB5EB120868DBFB5 /* BLEService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6FDA03416FDB2157A0A8C7 /* BLEService.swift */; };
|
||||||
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.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 */; };
|
||||||
@@ -66,7 +82,7 @@
|
|||||||
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 */; };
|
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
|
||||||
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
|
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
|
||||||
A2977428C1D9EF9944C4BFAF /* SimplifiedBluetoothServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */; };
|
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* BLEServiceTests.swift */; };
|
||||||
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
|
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
|
||||||
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.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 */; };
|
||||||
@@ -80,8 +96,8 @@
|
|||||||
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95DBE6A48626C5AE287245E /* NoiseProtocolTests.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 */; };
|
||||||
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78595178957244CBDF7E79B6 /* NostrRelayManager.swift */; };
|
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78595178957244CBDF7E79B6 /* NostrRelayManager.swift */; };
|
||||||
BE729E149C98F775D9622D9C /* SimplifiedBluetoothServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */; };
|
BE729E149C98F775D9622D9C /* BLEServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* BLEServiceTests.swift */; };
|
||||||
C165DD35BB8E9C327A3C2DA4 /* SimplifiedBluetoothService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */; };
|
C165DD35BB8E9C327A3C2DA4 /* BLEService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6FDA03416FDB2157A0A8C7 /* BLEService.swift */; };
|
||||||
C3B1226CD30C87501EF6F12F /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8043995007F0D84438EDD9 /* NostrIdentity.swift */; };
|
C3B1226CD30C87501EF6F12F /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8043995007F0D84438EDD9 /* NostrIdentity.swift */; };
|
||||||
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7EFB209C86BBD956B749EC /* SecureLogger.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 */; };
|
||||||
@@ -144,6 +160,14 @@
|
|||||||
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
||||||
049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
|
049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
|
||||||
049BD3982E506A12001A566B /* UnifiedPeerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedPeerService.swift; sourceTree = "<group>"; };
|
049BD3982E506A12001A566B /* UnifiedPeerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedPeerService.swift; sourceTree = "<group>"; };
|
||||||
|
049BD39B2E51DBD9001A566B /* NostrEmbeddedBitChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEmbeddedBitChat.swift; sourceTree = "<group>"; };
|
||||||
|
049BD39E2E51DBF4001A566B /* Packets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Packets.swift; sourceTree = "<group>"; };
|
||||||
|
049BD39F2E51DBF4001A566B /* PeerID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerID.swift; sourceTree = "<group>"; };
|
||||||
|
049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeduplicator.swift; sourceTree = "<group>"; };
|
||||||
|
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerIDResolver.swift; sourceTree = "<group>"; };
|
||||||
|
049BD3AD2E51ED60001A566B /* Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transport.swift; sourceTree = "<group>"; };
|
||||||
|
049BD3B02E51F319001A566B /* MessageRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRouter.swift; sourceTree = "<group>"; };
|
||||||
|
049BD3B12E51F319001A566B /* NostrTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrTransport.swift; sourceTree = "<group>"; };
|
||||||
05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
|
05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
|
||||||
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
||||||
11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; };
|
11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; };
|
||||||
@@ -168,13 +192,13 @@
|
|||||||
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>"; };
|
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>"; };
|
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>"; };
|
8C6FDA03416FDB2157A0A8C7 /* BLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEService.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>"; };
|
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>"; };
|
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>"; };
|
980B109CBA72BC996455C62B /* BLEServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEServiceTests.swift; sourceTree = "<group>"; };
|
||||||
9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.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>"; };
|
||||||
@@ -193,7 +217,7 @@
|
|||||||
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>"; };
|
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>"; };
|
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>"; };
|
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.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 */
|
||||||
|
|
||||||
@@ -305,7 +329,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */,
|
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */,
|
||||||
FE7CCF2BD78A3F3DAE6DA145 /* MockSimplifiedBluetoothService.swift */,
|
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */,
|
||||||
);
|
);
|
||||||
path = Mocks;
|
path = Mocks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -313,6 +337,8 @@
|
|||||||
9A78348821A7D3374607D4E3 /* Utils */ = {
|
9A78348821A7D3374607D4E3 /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */,
|
||||||
|
049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */,
|
||||||
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
|
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
|
||||||
90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */,
|
90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */,
|
||||||
EE7EFB209C86BBD956B749EC /* SecureLogger.swift */,
|
EE7EFB209C86BBD956B749EC /* SecureLogger.swift */,
|
||||||
@@ -356,6 +382,8 @@
|
|||||||
ADD53BCDA233C02E53458926 /* Protocols */ = {
|
ADD53BCDA233C02E53458926 /* Protocols */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
049BD39E2E51DBF4001A566B /* Packets.swift */,
|
||||||
|
049BD39F2E51DBF4001A566B /* PeerID.swift */,
|
||||||
5318B743C64628A125261163 /* BinaryEncodingUtils.swift */,
|
5318B743C64628A125261163 /* BinaryEncodingUtils.swift */,
|
||||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */,
|
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */,
|
||||||
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */,
|
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */,
|
||||||
@@ -377,7 +405,7 @@
|
|||||||
children = (
|
children = (
|
||||||
D69A18D27F9A565FD6041E12 /* Info.plist */,
|
D69A18D27F9A565FD6041E12 /* Info.plist */,
|
||||||
C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */,
|
C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */,
|
||||||
980B109CBA72BC996455C62B /* SimplifiedBluetoothServiceTests.swift */,
|
980B109CBA72BC996455C62B /* BLEServiceTests.swift */,
|
||||||
C2F78AB254FDAD5FEDA18B58 /* EndToEnd */,
|
C2F78AB254FDAD5FEDA18B58 /* EndToEnd */,
|
||||||
5B90895AFF0957E08FA3D429 /* Integration */,
|
5B90895AFF0957E08FA3D429 /* Integration */,
|
||||||
966CD21F221332CF564AC724 /* Mocks */,
|
966CD21F221332CF564AC724 /* Mocks */,
|
||||||
@@ -408,6 +436,9 @@
|
|||||||
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
049BD3B02E51F319001A566B /* MessageRouter.swift */,
|
||||||
|
049BD3B12E51F319001A566B /* NostrTransport.swift */,
|
||||||
|
049BD3AD2E51ED60001A566B /* Transport.swift */,
|
||||||
049BD3982E506A12001A566B /* UnifiedPeerService.swift */,
|
049BD3982E506A12001A566B /* UnifiedPeerService.swift */,
|
||||||
049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */,
|
049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */,
|
||||||
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */,
|
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */,
|
||||||
@@ -416,7 +447,7 @@
|
|||||||
136696FC4436A02D98CE6A77 /* KeychainManager.swift */,
|
136696FC4436A02D98CE6A77 /* KeychainManager.swift */,
|
||||||
394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */,
|
394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */,
|
||||||
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */,
|
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */,
|
||||||
8C6FDA03416FDB2157A0A8C7 /* SimplifiedBluetoothService.swift */,
|
8C6FDA03416FDB2157A0A8C7 /* BLEService.swift */,
|
||||||
);
|
);
|
||||||
path = Services;
|
path = Services;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -424,6 +455,7 @@
|
|||||||
E78C7F4B6769C0A72F5DE544 /* Nostr */ = {
|
E78C7F4B6769C0A72F5DE544 /* Nostr */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
049BD39B2E51DBD9001A566B /* NostrEmbeddedBitChat.swift */,
|
||||||
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
||||||
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
||||||
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */,
|
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */,
|
||||||
@@ -620,6 +652,8 @@
|
|||||||
files = (
|
files = (
|
||||||
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
|
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
|
||||||
9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */,
|
9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */,
|
||||||
|
049BD3B42E51F319001A566B /* NostrTransport.swift in Sources */,
|
||||||
|
049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */,
|
||||||
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
|
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
|
||||||
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
|
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
|
||||||
84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */,
|
84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */,
|
||||||
@@ -636,19 +670,25 @@
|
|||||||
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */,
|
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */,
|
||||||
AFF33EF44626EF0579D17EB1 /* NoiseHandshakeCoordinator.swift in Sources */,
|
AFF33EF44626EF0579D17EB1 /* NoiseHandshakeCoordinator.swift in Sources */,
|
||||||
8C1AB0F2D48207E0755DA91A /* NoiseProtocol.swift in Sources */,
|
8C1AB0F2D48207E0755DA91A /* NoiseProtocol.swift in Sources */,
|
||||||
|
049BD3AC2E51E38E001A566B /* PeerIDResolver.swift in Sources */,
|
||||||
D691938B4029A04CC905FDC8 /* NoiseSecurityConsiderations.swift in Sources */,
|
D691938B4029A04CC905FDC8 /* NoiseSecurityConsiderations.swift in Sources */,
|
||||||
8A14ADADF5CD7A79919CB655 /* NoiseSession.swift in Sources */,
|
8A14ADADF5CD7A79919CB655 /* NoiseSession.swift in Sources */,
|
||||||
|
049BD39D2E51DBD9001A566B /* NostrEmbeddedBitChat.swift in Sources */,
|
||||||
049BD3992E506A12001A566B /* UnifiedPeerService.swift in Sources */,
|
049BD3992E506A12001A566B /* UnifiedPeerService.swift in Sources */,
|
||||||
C3B1226CD30C87501EF6F12F /* NostrIdentity.swift in Sources */,
|
C3B1226CD30C87501EF6F12F /* NostrIdentity.swift in Sources */,
|
||||||
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */,
|
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */,
|
||||||
|
049BD3A62E51DC0E001A566B /* MessageDeduplicator.swift in Sources */,
|
||||||
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */,
|
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */,
|
||||||
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
||||||
|
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */,
|
||||||
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */,
|
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */,
|
||||||
|
049BD3A02E51DBF4001A566B /* Packets.swift in Sources */,
|
||||||
|
049BD3A12E51DBF4001A566B /* PeerID.swift in Sources */,
|
||||||
049BD3942E4EC4F0001A566B /* PrivateChatManager.swift in Sources */,
|
049BD3942E4EC4F0001A566B /* PrivateChatManager.swift in Sources */,
|
||||||
049BD3952E4EC4F0001A566B /* AutocompleteService.swift in Sources */,
|
049BD3952E4EC4F0001A566B /* AutocompleteService.swift in Sources */,
|
||||||
049BD3962E4EC4F0001A566B /* CommandProcessor.swift in Sources */,
|
049BD3962E4EC4F0001A566B /* CommandProcessor.swift in Sources */,
|
||||||
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */,
|
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */,
|
||||||
8DE687D2EB5EB120868DBFB5 /* SimplifiedBluetoothService.swift in Sources */,
|
8DE687D2EB5EB120868DBFB5 /* BLEService.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -658,6 +698,8 @@
|
|||||||
files = (
|
files = (
|
||||||
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
|
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
|
||||||
AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */,
|
AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */,
|
||||||
|
049BD3B22E51F319001A566B /* NostrTransport.swift in Sources */,
|
||||||
|
049BD3B32E51F319001A566B /* MessageRouter.swift in Sources */,
|
||||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
|
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
|
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
|
||||||
84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */,
|
84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */,
|
||||||
@@ -674,19 +716,25 @@
|
|||||||
5EE49E150BBF0488E7473687 /* NoiseEncryptionService.swift in Sources */,
|
5EE49E150BBF0488E7473687 /* NoiseEncryptionService.swift in Sources */,
|
||||||
6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */,
|
6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */,
|
||||||
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */,
|
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */,
|
||||||
|
049BD3AB2E51E38E001A566B /* PeerIDResolver.swift in Sources */,
|
||||||
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */,
|
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */,
|
||||||
92D1CF17DF88EA298F6E5E8E /* NoiseSession.swift in Sources */,
|
92D1CF17DF88EA298F6E5E8E /* NoiseSession.swift in Sources */,
|
||||||
|
049BD39C2E51DBD9001A566B /* NostrEmbeddedBitChat.swift in Sources */,
|
||||||
049BD39A2E506A12001A566B /* UnifiedPeerService.swift in Sources */,
|
049BD39A2E506A12001A566B /* UnifiedPeerService.swift in Sources */,
|
||||||
D782AB596DDB5C846554F7C3 /* NostrIdentity.swift in Sources */,
|
D782AB596DDB5C846554F7C3 /* NostrIdentity.swift in Sources */,
|
||||||
F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */,
|
F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */,
|
||||||
|
049BD3A52E51DC0E001A566B /* MessageDeduplicator.swift in Sources */,
|
||||||
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */,
|
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */,
|
||||||
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
||||||
|
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */,
|
||||||
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */,
|
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */,
|
||||||
|
049BD3A22E51DBF4001A566B /* Packets.swift in Sources */,
|
||||||
|
049BD3A32E51DBF4001A566B /* PeerID.swift in Sources */,
|
||||||
049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */,
|
049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */,
|
||||||
049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */,
|
049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */,
|
||||||
049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */,
|
049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */,
|
||||||
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */,
|
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */,
|
||||||
C165DD35BB8E9C327A3C2DA4 /* SimplifiedBluetoothService.swift in Sources */,
|
C165DD35BB8E9C327A3C2DA4 /* BLEService.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -697,12 +745,12 @@
|
|||||||
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */,
|
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */,
|
||||||
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
|
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
|
||||||
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
|
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
|
||||||
6C803BF930E7E19BE6E99EAA /* MockSimplifiedBluetoothService.swift in Sources */,
|
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */,
|
||||||
765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */,
|
765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */,
|
||||||
968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */,
|
968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */,
|
||||||
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */,
|
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */,
|
||||||
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */,
|
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */,
|
||||||
A2977428C1D9EF9944C4BFAF /* SimplifiedBluetoothServiceTests.swift in Sources */,
|
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */,
|
||||||
2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */,
|
2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */,
|
||||||
B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */,
|
B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -715,12 +763,12 @@
|
|||||||
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */,
|
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */,
|
||||||
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
|
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
|
||||||
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
|
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
|
||||||
3849CA6D99B2D536636DF4A6 /* MockSimplifiedBluetoothService.swift in Sources */,
|
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
||||||
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
|
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
|
||||||
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
|
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
|
||||||
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */,
|
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */,
|
||||||
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */,
|
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */,
|
||||||
BE729E149C98F775D9622D9C /* SimplifiedBluetoothServiceTests.swift in Sources */,
|
BE729E149C98F775D9622D9C /* BLEServiceTests.swift in Sources */,
|
||||||
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */,
|
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */,
|
||||||
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */,
|
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: SimplifiedBluetoothService
|
private let meshService: Transport
|
||||||
private let favoritesService = FavoritesPersistenceService.shared
|
private let favoritesService = FavoritesPersistenceService.shared
|
||||||
|
|
||||||
init(meshService: SimplifiedBluetoothService) {
|
init(meshService: Transport) {
|
||||||
self.meshService = meshService
|
self.meshService = meshService
|
||||||
updatePeers()
|
updatePeers()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - BitChat-over-Nostr Adapter
|
||||||
|
|
||||||
|
struct NostrEmbeddedBitChat {
|
||||||
|
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a private message for Nostr DMs.
|
||||||
|
static func encodePMForNostr(content: String, messageID: String, recipientPeerID: String, senderPeerID: String) -> String? {
|
||||||
|
// TLV-encode the private message
|
||||||
|
let pm = PrivateMessagePacket(messageID: messageID, content: content)
|
||||||
|
guard let tlv = pm.encode() else { return nil }
|
||||||
|
|
||||||
|
// Prefix with NoisePayloadType
|
||||||
|
var payload = Data([NoisePayloadType.privateMessage.rawValue])
|
||||||
|
payload.append(tlv)
|
||||||
|
|
||||||
|
// Determine 8-byte recipient ID to embed
|
||||||
|
let recipientIDHex: String = normalizeRecipientPeerID(recipientPeerID)
|
||||||
|
|
||||||
|
let packet = BitchatPacket(
|
||||||
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
|
senderID: Data(hexString: senderPeerID) ?? Data(),
|
||||||
|
recipientID: Data(hexString: recipientIDHex),
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: payload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: 7
|
||||||
|
)
|
||||||
|
|
||||||
|
guard let data = packet.toBinaryData() else { return nil }
|
||||||
|
return "bitchat1:" + base64URLEncode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a delivery/read ack for Nostr DMs.
|
||||||
|
static func encodeAckForNostr(type: NoisePayloadType, messageID: String, recipientPeerID: String, senderPeerID: String) -> String? {
|
||||||
|
guard type == .delivered || type == .readReceipt else { return nil }
|
||||||
|
|
||||||
|
var payload = Data([type.rawValue])
|
||||||
|
payload.append(Data(messageID.utf8))
|
||||||
|
|
||||||
|
let recipientIDHex: String = normalizeRecipientPeerID(recipientPeerID)
|
||||||
|
|
||||||
|
let packet = BitchatPacket(
|
||||||
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
|
senderID: Data(hexString: senderPeerID) ?? Data(),
|
||||||
|
recipientID: Data(hexString: recipientIDHex),
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: payload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: 7
|
||||||
|
)
|
||||||
|
|
||||||
|
guard let data = packet.toBinaryData() else { return nil }
|
||||||
|
return "bitchat1:" + base64URLEncode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func normalizeRecipientPeerID(_ recipientPeerID: String) -> String {
|
||||||
|
if let maybeData = Data(hexString: recipientPeerID) {
|
||||||
|
if maybeData.count == 32 {
|
||||||
|
// Treat as Noise static public key; derive peerID from fingerprint
|
||||||
|
return PeerIDUtils.derivePeerID(fromPublicKey: maybeData)
|
||||||
|
} else if maybeData.count == 8 {
|
||||||
|
// Already an 8-byte peer ID
|
||||||
|
return recipientPeerID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: return as-is (expecting 16 hex chars) – caller should pass a valid peer ID
|
||||||
|
return recipientPeerID
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base64url encode without padding
|
||||||
|
private static func base64URLEncode(_ data: Data) -> String {
|
||||||
|
let b64 = data.base64EncodedString()
|
||||||
|
return b64
|
||||||
|
.replacingOccurrences(of: "+", with: "-")
|
||||||
|
.replacingOccurrences(of: "/", with: "_")
|
||||||
|
.replacingOccurrences(of: "=", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 SimplifiedBluetoothService
|
/// 4. **Transmission**: Sent via BLEService
|
||||||
/// 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
|
||||||
@@ -461,6 +461,10 @@ protocol BitchatDelegate: AnyObject {
|
|||||||
func isFavorite(fingerprint: String) -> Bool
|
func isFavorite(fingerprint: String) -> Bool
|
||||||
|
|
||||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
||||||
|
|
||||||
|
// Low-level events for better separation of concerns
|
||||||
|
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date)
|
||||||
|
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide default implementation to make it effectively optional
|
// Provide default implementation to make it effectively optional
|
||||||
@@ -472,6 +476,14 @@ extension BitchatDelegate {
|
|||||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||||
// Default empty implementation
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||||
|
// Default empty implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
||||||
|
// Default empty implementation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Noise Payload Helpers
|
// MARK: - Noise Payload Helpers
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Protocol TLV Packets
|
||||||
|
|
||||||
|
struct AnnouncementPacket {
|
||||||
|
let nickname: String
|
||||||
|
let publicKey: Data
|
||||||
|
|
||||||
|
private enum TLVType: UInt8 {
|
||||||
|
case nickname = 0x01
|
||||||
|
case noisePublicKey = 0x02
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode() -> Data? {
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
// TLV for nickname
|
||||||
|
guard let nicknameData = nickname.data(using: .utf8), nicknameData.count <= 255 else { return nil }
|
||||||
|
data.append(TLVType.nickname.rawValue)
|
||||||
|
data.append(UInt8(nicknameData.count))
|
||||||
|
data.append(nicknameData)
|
||||||
|
|
||||||
|
// TLV for public key
|
||||||
|
guard publicKey.count <= 255 else { return nil }
|
||||||
|
data.append(TLVType.noisePublicKey.rawValue)
|
||||||
|
data.append(UInt8(publicKey.count))
|
||||||
|
data.append(publicKey)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(from data: Data) -> AnnouncementPacket? {
|
||||||
|
var offset = 0
|
||||||
|
var nickname: String?
|
||||||
|
var publicKey: Data?
|
||||||
|
|
||||||
|
while offset + 2 <= data.count {
|
||||||
|
guard let type = TLVType(rawValue: data[offset]) else { return nil }
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
let length = Int(data[offset])
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
guard offset + length <= data.count else { return nil }
|
||||||
|
let value = data[offset..<offset + length]
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .nickname:
|
||||||
|
nickname = String(data: value, encoding: .utf8)
|
||||||
|
case .noisePublicKey:
|
||||||
|
publicKey = Data(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let nickname = nickname, let publicKey = publicKey else { return nil }
|
||||||
|
return AnnouncementPacket(nickname: nickname, publicKey: publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrivateMessagePacket {
|
||||||
|
let messageID: String
|
||||||
|
let content: String
|
||||||
|
|
||||||
|
private enum TLVType: UInt8 {
|
||||||
|
case messageID = 0x00
|
||||||
|
case content = 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode() -> Data? {
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
// TLV for messageID
|
||||||
|
guard let messageIDData = messageID.data(using: .utf8), messageIDData.count <= 255 else { return nil }
|
||||||
|
data.append(TLVType.messageID.rawValue)
|
||||||
|
data.append(UInt8(messageIDData.count))
|
||||||
|
data.append(messageIDData)
|
||||||
|
|
||||||
|
// TLV for content
|
||||||
|
guard let contentData = content.data(using: .utf8), contentData.count <= 255 else { return nil }
|
||||||
|
data.append(TLVType.content.rawValue)
|
||||||
|
data.append(UInt8(contentData.count))
|
||||||
|
data.append(contentData)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(from data: Data) -> PrivateMessagePacket? {
|
||||||
|
var offset = 0
|
||||||
|
var messageID: String?
|
||||||
|
var content: String?
|
||||||
|
|
||||||
|
while offset + 2 <= data.count {
|
||||||
|
guard let type = TLVType(rawValue: data[offset]) else { return nil }
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
let length = Int(data[offset])
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
guard offset + length <= data.count else { return nil }
|
||||||
|
let value = data[offset..<offset + length]
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .messageID:
|
||||||
|
messageID = String(data: value, encoding: .utf8)
|
||||||
|
case .content:
|
||||||
|
content = String(data: value, encoding: .utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let messageID = messageID, let content = content else { return nil }
|
||||||
|
return PrivateMessagePacket(messageID: messageID, content: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
// MARK: - Peer ID Utilities
|
||||||
|
|
||||||
|
struct PeerIDUtils {
|
||||||
|
/// Derive the stable 16-hex peer ID from a Noise static public key
|
||||||
|
static func derivePeerID(fromPublicKey publicKey: Data) -> String {
|
||||||
|
let digest = SHA256.hash(data: publicKey)
|
||||||
|
let hex = digest.map { String(format: "%02x", $0) }.joined()
|
||||||
|
return String(hex.prefix(16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+136
-532
@@ -6,14 +6,19 @@ import CryptoKit
|
|||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Simplified Bluetooth Mesh Service - Core functionality only
|
/// BLEService — Bluetooth Mesh Transport
|
||||||
/// Target: <1500 lines (from 6470)
|
/// - Emits events exclusively via `BitchatDelegate` for UI.
|
||||||
final class SimplifiedBluetoothService: NSObject {
|
/// - ChatViewModel must consume delegate callbacks (`didReceivePublicMessage`, `didReceiveNoisePayload`).
|
||||||
|
/// - A lightweight `peerSnapshotPublisher` is provided for non-UI services.
|
||||||
|
final class BLEService: NSObject {
|
||||||
|
|
||||||
// MARK: - Constants
|
// MARK: - Constants
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A") // testnet
|
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A") // testnet
|
||||||
//static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
#else
|
||||||
|
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
||||||
|
#endif
|
||||||
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
||||||
|
|
||||||
private let maxFragmentSize = 469 // 512 MTU - headers
|
private let maxFragmentSize = 469 // 512 MTU - headers
|
||||||
@@ -97,44 +102,38 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
private weak var maintenanceTimer: Timer? // Single timer for all maintenance tasks
|
private weak var maintenanceTimer: Timer? // Single timer for all maintenance tasks
|
||||||
private var maintenanceCounter = 0 // Track maintenance cycles
|
private var maintenanceCounter = 0 // Track maintenance cycles
|
||||||
|
|
||||||
// MARK: - Publishers
|
// MARK: - Peer snapshots publisher (non-UI convenience)
|
||||||
|
private let peerSnapshotSubject = PassthroughSubject<[TransportPeerSnapshot], Never>()
|
||||||
let messagesPublisher = PassthroughSubject<BitchatMessage, Never>()
|
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
||||||
let peersPublisher = PassthroughSubject<[String: String], Never>() // Legacy - for backward compatibility
|
peerSnapshotSubject.eraseToAnyPublisher()
|
||||||
|
|
||||||
// NEW: Full peer data publisher for UnifiedPeerService
|
|
||||||
struct PeerInfoSnapshot {
|
|
||||||
let id: String
|
|
||||||
let nickname: String
|
|
||||||
let isConnected: Bool
|
|
||||||
let noisePublicKey: Data?
|
|
||||||
let lastSeen: Date
|
|
||||||
}
|
}
|
||||||
let fullPeersPublisher = CurrentValueSubject<[String: PeerInfoSnapshot], Never>([:])
|
|
||||||
|
func currentPeerSnapshots() -> [TransportPeerSnapshot] {
|
||||||
// Helper to convert internal PeerInfo to public snapshot
|
collectionsQueue.sync {
|
||||||
private func createPeerSnapshot(_ info: PeerInfo) -> PeerInfoSnapshot {
|
peers.values.map { info in
|
||||||
PeerInfoSnapshot(
|
TransportPeerSnapshot(
|
||||||
id: info.id,
|
id: info.id,
|
||||||
nickname: info.nickname,
|
nickname: info.nickname,
|
||||||
isConnected: info.isConnected,
|
isConnected: info.isConnected,
|
||||||
noisePublicKey: info.noisePublicKey,
|
noisePublicKey: info.noisePublicKey,
|
||||||
lastSeen: info.lastSeen
|
lastSeen: info.lastSeen
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delegate
|
// MARK: - Delegate
|
||||||
|
|
||||||
weak var delegate: BitchatDelegate?
|
weak var delegate: BitchatDelegate?
|
||||||
|
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
/// Notify UI on main thread (only if needed)
|
/// Notify UI on the MainActor to satisfy Swift concurrency isolation
|
||||||
private func notifyUI(_ block: @escaping () -> Void) {
|
private func notifyUI(_ block: @escaping () -> Void) {
|
||||||
if Thread.isMainThread {
|
// Always hop onto the MainActor so calls to @MainActor delegates are safe
|
||||||
|
Task { @MainActor in
|
||||||
block()
|
block()
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async(execute: block)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +260,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
// Start BLE services if not already running
|
// Start BLE services if not already running
|
||||||
if centralManager?.state == .poweredOn {
|
if centralManager?.state == .poweredOn {
|
||||||
centralManager?.scanForPeripherals(
|
centralManager?.scanForPeripherals(
|
||||||
withServices: [SimplifiedBluetoothService.serviceUUID],
|
withServices: [BLEService.serviceUUID],
|
||||||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -272,12 +271,18 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
self?.sendAnnounce(forceSend: true)
|
self?.sendAnnounce(forceSend: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transport protocol conformance helper: simplified public message send
|
||||||
|
func sendMessage(_ content: String, mentions: [String]) {
|
||||||
|
// Delegate to the full API with default routing
|
||||||
|
sendMessage(content, mentions: mentions, to: nil, messageID: nil, timestamp: nil)
|
||||||
|
}
|
||||||
|
|
||||||
func stopServices() {
|
func stopServices() {
|
||||||
// Send leave message synchronously to ensure delivery
|
// Send leave message synchronously to ensure delivery
|
||||||
let leavePacket = BitchatPacket(
|
let leavePacket = BitchatPacket(
|
||||||
type: MessageType.leave.rawValue,
|
type: MessageType.leave.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: nil,
|
recipientID: nil,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: Data(),
|
payload: Data(),
|
||||||
@@ -322,9 +327,14 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isPeerConnected(_ peerID: String) -> Bool {
|
func isPeerConnected(_ peerID: String) -> Bool {
|
||||||
return collectionsQueue.sync {
|
// Accept both 16-hex short IDs and 64-hex Noise keys
|
||||||
return peers[peerID]?.isConnected ?? false
|
let shortID: String = {
|
||||||
}
|
if peerID.count == 64, let key = Data(hexString: peerID) {
|
||||||
|
return PeerIDUtils.derivePeerID(fromPublicKey: key)
|
||||||
|
}
|
||||||
|
return peerID
|
||||||
|
}()
|
||||||
|
return collectionsQueue.sync { peers[shortID]?.isConnected ?? false }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPeerNicknames() -> [String: String] {
|
func getPeerNicknames() -> [String: String] {
|
||||||
@@ -376,8 +386,8 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
let encrypted = try noiseService.encrypt(receiptPayload, for: peerID)
|
let encrypted = try noiseService.encrypt(receiptPayload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: hexStringToData(peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
@@ -406,11 +416,14 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
func getPeerFingerprint(_ peerID: String) -> String? {
|
func getPeerFingerprint(_ peerID: String) -> String? {
|
||||||
return collectionsQueue.sync {
|
return collectionsQueue.sync {
|
||||||
if let publicKey = peers[peerID]?.noisePublicKey {
|
if let publicKey = peers[peerID]?.noisePublicKey {
|
||||||
return dataToHexString(publicKey)
|
return publicKey.hexEncodedString()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transport compatibility: generic naming
|
||||||
|
|
||||||
|
|
||||||
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
|
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
|
||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
@@ -531,15 +544,15 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: recipientData,
|
recipientID: recipientData,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
// Call directly if already on messageQueue, otherwise dispatch
|
// Call directly if already on messageQueue, otherwise dispatch
|
||||||
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
|
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
@@ -587,8 +600,8 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
// Send handshake init
|
// Send handshake init
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: hexStringToData(peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: handshakeData,
|
payload: handshakeData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
@@ -622,39 +635,43 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
|
|
||||||
// Send each pending message directly (we know session is established)
|
// Send each pending message directly (we know session is established)
|
||||||
for (content, messageID) in messages {
|
for (content, messageID) in messages {
|
||||||
// Encrypt and send directly without checking session again
|
|
||||||
do {
|
do {
|
||||||
// Create message payload with ID: [type byte] + [ID:xxxxx|content]
|
// Use the same TLV format as normal sends to keep receiver decoding consistent
|
||||||
|
let privateMessage = PrivateMessagePacket(messageID: messageID, content: content)
|
||||||
|
guard let tlvData = privateMessage.encode() else {
|
||||||
|
SecureLogger.log("Failed to encode pending private message TLV", category: SecureLogger.noise, level: .error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var messagePayload = Data([NoisePayloadType.privateMessage.rawValue])
|
var messagePayload = Data([NoisePayloadType.privateMessage.rawValue])
|
||||||
let messageWithID = "ID:\(messageID)|\(content)"
|
messagePayload.append(tlvData)
|
||||||
messagePayload.append(contentsOf: messageWithID.utf8)
|
|
||||||
|
|
||||||
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: hexStringToData(peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
|
|
||||||
// We're already on messageQueue from the callback
|
// We're already on messageQueue from the callback
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
|
|
||||||
// Notify delegate that message was sent
|
// Notify delegate that message was sent
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake",
|
SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("Failed to send pending message after handshake: \(error)",
|
SecureLogger.log("Failed to send pending message after handshake: \(error)",
|
||||||
category: SecureLogger.noise, level: .error)
|
category: SecureLogger.noise, level: .error)
|
||||||
|
|
||||||
// Notify delegate of failure
|
// Notify delegate of failure
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .failed(reason: "Encryption failed"))
|
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .failed(reason: "Encryption failed"))
|
||||||
@@ -691,7 +708,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
// Handshakes need broader delivery to establish encryption
|
// Handshakes need broader delivery to establish encryption
|
||||||
if packet.type == MessageType.noiseEncrypted.rawValue,
|
if packet.type == MessageType.noiseEncrypted.rawValue,
|
||||||
let recipientID = packet.recipientID {
|
let recipientID = packet.recipientID {
|
||||||
let recipientPeerID = dataToHexString(recipientID)
|
let recipientPeerID = recipientID.hexEncodedString()
|
||||||
var sentEncrypted = false
|
var sentEncrypted = false
|
||||||
|
|
||||||
// Check routing availability (only log if there's an issue)
|
// Check routing availability (only log if there's an issue)
|
||||||
@@ -756,7 +773,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
// 1. First try sending as central via writes to connected peripherals
|
// 1. First try sending as central via writes to connected peripherals
|
||||||
// This is the preferred path when we have direct peripheral connections
|
// This is the preferred path when we have direct peripheral connections
|
||||||
for state in peripherals.values where state.isConnected {
|
for state in peripherals.values where state.isConnected {
|
||||||
if let characteristic = state.characteristic {
|
if let characteristic = state.characteristic {
|
||||||
state.peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
state.peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
||||||
sentToPeripherals += 1
|
sentToPeripherals += 1
|
||||||
}
|
}
|
||||||
@@ -907,7 +924,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
|
|
||||||
private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String) {
|
private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String) {
|
||||||
// Deduplication (thread-safe)
|
// Deduplication (thread-safe)
|
||||||
let senderID = dataToHexString(packet.senderID)
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
// Include packet type in message ID to prevent collisions between different packet types
|
// Include packet type in message ID to prevent collisions between different packet types
|
||||||
let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)"
|
let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)"
|
||||||
|
|
||||||
@@ -983,7 +1000,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
|
|
||||||
// Verify that the sender's derived ID from the announced public key matches the packet senderID
|
// Verify that the sender's derived ID from the announced public key matches the packet senderID
|
||||||
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
|
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
|
||||||
let derivedFromKey = Self.derivePeerID(fromPublicKey: announcement.publicKey)
|
let derivedFromKey = PeerIDUtils.derivePeerID(fromPublicKey: announcement.publicKey)
|
||||||
if derivedFromKey != peerID {
|
if derivedFromKey != peerID {
|
||||||
SecureLogger.log("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))…", category: SecureLogger.security, level: .warning)
|
SecureLogger.log("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))…", category: SecureLogger.security, level: .warning)
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -1062,8 +1079,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
self.delegate?.didConnectToPeer(peerID)
|
self.delegate?.didConnectToPeer(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.peersPublisher.send(self.getPeers())
|
self.publishFullPeerData()
|
||||||
self.publishFullPeerData() // NEW: Publish full peer data
|
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1081,29 +1097,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseMentions(from content: String) -> [String] {
|
// Mention parsing moved to ChatViewModel
|
||||||
let pattern = "@([\\p{L}0-9_]+)"
|
|
||||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
|
||||||
let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? []
|
|
||||||
|
|
||||||
var mentions: [String] = []
|
|
||||||
|
|
||||||
// Get all known nicknames including our own
|
|
||||||
var allNicknames = Set(peers.values.map { $0.nickname })
|
|
||||||
allNicknames.insert(myNickname)
|
|
||||||
|
|
||||||
for match in matches {
|
|
||||||
if let range = Range(match.range(at: 1), in: content) {
|
|
||||||
let mentionedName = String(content[range])
|
|
||||||
// Check if this is a valid nickname
|
|
||||||
if allNicknames.contains(mentionedName) {
|
|
||||||
mentions.append(mentionedName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(Set(mentions)) // Remove duplicates
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleMessage(_ packet: BitchatPacket, from peerID: String) {
|
private func handleMessage(_ packet: BitchatPacket, from peerID: String) {
|
||||||
// Don't process our own messages
|
// Don't process our own messages
|
||||||
@@ -1119,43 +1113,24 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
let senderNickname = peers[peerID]?.nickname ?? "Unknown"
|
let senderNickname = peers[peerID]?.nickname ?? "Unknown"
|
||||||
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
// Parse mentions from the message content
|
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||||
let mentions = parseMentions(from: content)
|
|
||||||
|
|
||||||
let message = BitchatMessage(
|
|
||||||
id: UUID().uuidString,
|
|
||||||
sender: senderNickname,
|
|
||||||
content: content,
|
|
||||||
timestamp: Date(timeIntervalSince1970: Double(packet.timestamp) / 1000),
|
|
||||||
isRelay: false,
|
|
||||||
originalSender: nil,
|
|
||||||
isPrivate: false,
|
|
||||||
recipientNickname: nil,
|
|
||||||
senderPeerID: peerID,
|
|
||||||
mentions: mentions.isEmpty ? nil : mentions
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send on main thread (without capturing self strongly)
|
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
guard let self = self else { return }
|
self?.delegate?.didReceivePublicMessage(from: peerID, nickname: senderNickname, content: content, timestamp: ts)
|
||||||
// Deliver to UI
|
|
||||||
self.messagesPublisher.send(message)
|
|
||||||
self.delegate?.didReceiveMessage(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleNoiseHandshake(_ packet: BitchatPacket, from peerID: String) {
|
private func handleNoiseHandshake(_ packet: BitchatPacket, from peerID: String) {
|
||||||
// Use NoiseEncryptionService for handshake processing
|
// Use NoiseEncryptionService for handshake processing
|
||||||
if let recipientID = packet.recipientID,
|
if let recipientID = packet.recipientID,
|
||||||
dataToHexString(recipientID) == myPeerID {
|
recipientID.hexEncodedString() == myPeerID {
|
||||||
// Handshake is for us
|
// Handshake is for us
|
||||||
do {
|
do {
|
||||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
||||||
// Send response
|
// Send response
|
||||||
let responsePacket = BitchatPacket(
|
let responsePacket = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: hexStringToData(peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: response,
|
payload: response,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
@@ -1186,7 +1161,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let recipientHex = dataToHexString(recipientID)
|
let recipientHex = recipientID.hexEncodedString()
|
||||||
if recipientHex != myPeerID {
|
if recipientHex != myPeerID {
|
||||||
SecureLogger.log("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: SecureLogger.session, level: .debug)
|
||||||
return
|
return
|
||||||
@@ -1205,71 +1180,20 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
|
|
||||||
switch NoisePayloadType(rawValue: payloadType) {
|
switch NoisePayloadType(rawValue: payloadType) {
|
||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
// Try to decode as TLV first
|
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||||
guard let privateMessage = PrivateMessagePacket.decode(from: Data(payloadData)) else {
|
|
||||||
SecureLogger.log("⚠️ Failed to decode private message with TLV format",
|
|
||||||
category: SecureLogger.noise, level: .warning)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Successfully decoded TLV format
|
|
||||||
let messageID = privateMessage.messageID
|
|
||||||
let messageContent = privateMessage.content
|
|
||||||
|
|
||||||
// Parse mentions even in private messages
|
|
||||||
let mentions = parseMentions(from: messageContent)
|
|
||||||
|
|
||||||
let message = BitchatMessage(
|
|
||||||
id: messageID,
|
|
||||||
sender: peers[peerID]?.nickname ?? "Unknown",
|
|
||||||
content: messageContent,
|
|
||||||
timestamp: Date(timeIntervalSince1970: Double(packet.timestamp) / 1000),
|
|
||||||
isRelay: false,
|
|
||||||
originalSender: nil,
|
|
||||||
isPrivate: true,
|
|
||||||
recipientNickname: myNickname,
|
|
||||||
senderPeerID: peerID,
|
|
||||||
mentions: mentions.isEmpty ? nil : mentions
|
|
||||||
)
|
|
||||||
|
|
||||||
SecureLogger.log("🔓 Decrypted TLV PM from \(message.sender): \(messageContent.prefix(30))...", category: SecureLogger.session, level: .debug)
|
|
||||||
|
|
||||||
// Send on main thread
|
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
if let delegate = self?.delegate {
|
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .privateMessage, payload: Data(payloadData), timestamp: ts)
|
||||||
SecureLogger.log("📨 Forwarding PM to ChatViewModel delegate", category: SecureLogger.session, level: .debug)
|
|
||||||
delegate.didReceiveMessage(message)
|
|
||||||
} else {
|
|
||||||
SecureLogger.log("⚠️ Delegate is nil, cannot forward PM to ChatViewModel", category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send delivery ACK
|
|
||||||
sendDeliveryAck(for: messageID, to: peerID)
|
|
||||||
|
|
||||||
|
|
||||||
case .delivered:
|
case .delivered:
|
||||||
// Handle delivery ACK
|
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||||
guard let messageID = String(data: payloadData, encoding: .utf8) else { return }
|
|
||||||
// Delivery ACK received - no need to log
|
|
||||||
|
|
||||||
// Update delivery status
|
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .delivered(to: peerID, at: Date()))
|
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .delivered, payload: Data(payloadData), timestamp: ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .readReceipt:
|
case .readReceipt:
|
||||||
// Handle read receipt
|
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||||
guard let messageID = String(data: payloadData, encoding: .utf8) else { return }
|
|
||||||
|
|
||||||
SecureLogger.log("📖 Received READ receipt for message \(messageID) from \(peerID)",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
|
|
||||||
// Update read status
|
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
let nickname = self?.peers[peerID]?.nickname ?? "Unknown"
|
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .readReceipt, payload: Data(payloadData), timestamp: ts)
|
||||||
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .read(by: nickname, at: Date()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning)
|
SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning)
|
||||||
}
|
}
|
||||||
@@ -1299,7 +1223,6 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
// Get current peer list (after removal)
|
// Get current peer list (after removal)
|
||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||||
|
|
||||||
self.peersPublisher.send(self.getPeers())
|
|
||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(peerID)
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
@@ -1361,7 +1284,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendDeliveryAck(for messageID: String, to peerID: String) {
|
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
||||||
// Send encrypted delivery ACK
|
// Send encrypted delivery ACK
|
||||||
guard noiseService.hasSession(with: peerID) else {
|
guard noiseService.hasSession(with: peerID) else {
|
||||||
SecureLogger.log("Cannot send ACK - no Noise session with \(peerID)", category: SecureLogger.noise, level: .warning)
|
SecureLogger.log("Cannot send ACK - no Noise session with \(peerID)", category: SecureLogger.noise, level: .warning)
|
||||||
@@ -1376,8 +1299,8 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
let encrypted = try noiseService.encrypt(ackPayload, for: peerID)
|
let encrypted = try noiseService.encrypt(ackPayload, for: peerID)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: hexStringToData(myPeerID),
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
recipientID: hexStringToData(peerID),
|
recipientID: Data(hexString: peerID),
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
@@ -1400,14 +1323,25 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Publish full peer data to subscribers
|
// NEW: Publish peer snapshots to subscribers and notify Transport delegates
|
||||||
private func publishFullPeerData() {
|
private func publishFullPeerData() {
|
||||||
let snapshot = collectionsQueue.sync { () -> [String: PeerInfoSnapshot] in
|
let transportPeers: [TransportPeerSnapshot] = collectionsQueue.sync {
|
||||||
Dictionary(uniqueKeysWithValues: peers.map { (id, info) in
|
peers.values.map { info in
|
||||||
(id, createPeerSnapshot(info))
|
TransportPeerSnapshot(
|
||||||
})
|
id: info.id,
|
||||||
|
nickname: info.nickname,
|
||||||
|
isConnected: info.isConnected,
|
||||||
|
noisePublicKey: info.noisePublicKey,
|
||||||
|
lastSeen: info.lastSeen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Notify non-UI listeners
|
||||||
|
peerSnapshotSubject.send(transportPeers)
|
||||||
|
// Notify UI on MainActor via delegate
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers)
|
||||||
}
|
}
|
||||||
fullPeersPublisher.send(snapshot)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Consolidated Maintenance
|
// MARK: - Consolidated Maintenance
|
||||||
@@ -1479,7 +1413,6 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
for peerID in disconnectedPeers {
|
for peerID in disconnectedPeers {
|
||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(peerID)
|
||||||
}
|
}
|
||||||
self.peersPublisher.send(self.getPeers())
|
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1505,7 +1438,7 @@ final class SimplifiedBluetoothService: NSObject {
|
|||||||
|
|
||||||
// MARK: - CBCentralManagerDelegate
|
// MARK: - CBCentralManagerDelegate
|
||||||
|
|
||||||
extension SimplifiedBluetoothService: CBCentralManagerDelegate {
|
extension BLEService: CBCentralManagerDelegate {
|
||||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
if central.state == .poweredOn {
|
if central.state == .poweredOn {
|
||||||
// Start scanning - use allow duplicates for faster discovery when active
|
// Start scanning - use allow duplicates for faster discovery when active
|
||||||
@@ -1527,7 +1460,7 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
central.scanForPeripherals(
|
central.scanForPeripherals(
|
||||||
withServices: [SimplifiedBluetoothService.serviceUUID],
|
withServices: [BLEService.serviceUUID],
|
||||||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1630,7 +1563,7 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
|
|||||||
SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
// Discover services
|
// Discover services
|
||||||
peripheral.discoverServices([SimplifiedBluetoothService.serviceUUID])
|
peripheral.discoverServices([BLEService.serviceUUID])
|
||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||||
@@ -1674,8 +1607,7 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
|
|||||||
if let peerID = peerID {
|
if let peerID = peerID {
|
||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(peerID)
|
||||||
}
|
}
|
||||||
self.peersPublisher.send(self.getPeers())
|
self.publishFullPeerData()
|
||||||
self.publishFullPeerData() // NEW: Publish full peer data
|
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1692,14 +1624,14 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
|
|||||||
|
|
||||||
// MARK: - CBPeripheralDelegate
|
// MARK: - CBPeripheralDelegate
|
||||||
|
|
||||||
extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
extension BLEService: CBPeripheralDelegate {
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
SecureLogger.log("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: SecureLogger.session, level: .error)
|
SecureLogger.log("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: SecureLogger.session, level: .error)
|
||||||
// Retry service discovery after a delay
|
// Retry service discovery after a delay
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
guard peripheral.state == .connected else { return }
|
guard peripheral.state == .connected else { return }
|
||||||
peripheral.discoverServices([SimplifiedBluetoothService.serviceUUID])
|
peripheral.discoverServices([BLEService.serviceUUID])
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1709,14 +1641,14 @@ extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let service = services.first(where: { $0.uuid == SimplifiedBluetoothService.serviceUUID }) else {
|
guard let service = services.first(where: { $0.uuid == BLEService.serviceUUID }) else {
|
||||||
// Not a BitChat peer - disconnect
|
// Not a BitChat peer - disconnect
|
||||||
centralManager?.cancelPeripheralConnection(peripheral)
|
centralManager?.cancelPeripheralConnection(peripheral)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discovering BLE characteristics
|
// Discovering BLE characteristics
|
||||||
peripheral.discoverCharacteristics([SimplifiedBluetoothService.characteristicUUID], for: service)
|
peripheral.discoverCharacteristics([BLEService.characteristicUUID], for: service)
|
||||||
}
|
}
|
||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||||
@@ -1725,7 +1657,7 @@ extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let characteristic = service.characteristics?.first(where: { $0.uuid == SimplifiedBluetoothService.characteristicUUID }) else {
|
guard let characteristic = service.characteristics?.first(where: { $0.uuid == BLEService.characteristicUUID }) else {
|
||||||
SecureLogger.log("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .warning)
|
SecureLogger.log("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .warning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1788,7 +1720,7 @@ extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the packet's senderID as the peer identifier
|
// Use the packet's senderID as the peer identifier
|
||||||
let senderID = dataToHexString(packet.senderID)
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
// Only log non-announce packets
|
// Only log non-announce packets
|
||||||
if packet.type != MessageType.announce.rawValue {
|
if packet.type != MessageType.announce.rawValue {
|
||||||
SecureLogger.log("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
|
||||||
@@ -1834,7 +1766,7 @@ extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
|||||||
SecureLogger.log("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .warning)
|
SecureLogger.log("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .warning)
|
||||||
|
|
||||||
// Check if our service was invalidated (peer app quit)
|
// Check if our service was invalidated (peer app quit)
|
||||||
let hasOurService = peripheral.services?.contains { $0.uuid == SimplifiedBluetoothService.serviceUUID } ?? false
|
let hasOurService = peripheral.services?.contains { $0.uuid == BLEService.serviceUUID } ?? false
|
||||||
|
|
||||||
if !hasOurService {
|
if !hasOurService {
|
||||||
// Service is gone - disconnect
|
// Service is gone - disconnect
|
||||||
@@ -1842,7 +1774,7 @@ extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
|||||||
centralManager?.cancelPeripheralConnection(peripheral)
|
centralManager?.cancelPeripheralConnection(peripheral)
|
||||||
} else {
|
} else {
|
||||||
// Try to rediscover
|
// Try to rediscover
|
||||||
peripheral.discoverServices([SimplifiedBluetoothService.serviceUUID])
|
peripheral.discoverServices([BLEService.serviceUUID])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1863,7 +1795,7 @@ extension SimplifiedBluetoothService: CBPeripheralDelegate {
|
|||||||
|
|
||||||
// MARK: - CBPeripheralManagerDelegate
|
// MARK: - CBPeripheralManagerDelegate
|
||||||
|
|
||||||
extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
extension BLEService: CBPeripheralManagerDelegate {
|
||||||
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
||||||
SecureLogger.log("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
@@ -1873,14 +1805,14 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Create characteristic
|
// Create characteristic
|
||||||
characteristic = CBMutableCharacteristic(
|
characteristic = CBMutableCharacteristic(
|
||||||
type: SimplifiedBluetoothService.characteristicUUID,
|
type: BLEService.characteristicUUID,
|
||||||
properties: [.notify, .write, .writeWithoutResponse, .read],
|
properties: [.notify, .write, .writeWithoutResponse, .read],
|
||||||
value: nil,
|
value: nil,
|
||||||
permissions: [.readable, .writeable]
|
permissions: [.readable, .writeable]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create service
|
// Create service
|
||||||
let service = CBMutableService(type: SimplifiedBluetoothService.serviceUUID, primary: true)
|
let service = CBMutableService(type: BLEService.serviceUUID, primary: true)
|
||||||
service.characteristics = [characteristic!]
|
service.characteristics = [characteristic!]
|
||||||
|
|
||||||
// Add service (advertising will start in didAdd delegate)
|
// Add service (advertising will start in didAdd delegate)
|
||||||
@@ -1941,7 +1873,6 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||||
|
|
||||||
self.delegate?.didDisconnectFromPeer(peerID)
|
self.delegate?.didDisconnectFromPeer(peerID)
|
||||||
self.peersPublisher.send(self.getPeers())
|
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2019,7 +1950,7 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
if let packet = BinaryProtocol.decode(data) {
|
if let packet = BinaryProtocol.decode(data) {
|
||||||
// Use the packet's senderID as the peer identifier
|
// Use the packet's senderID as the peer identifier
|
||||||
let senderID = dataToHexString(packet.senderID)
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
// Only log non-announce packets
|
// Only log non-announce packets
|
||||||
if packet.type != MessageType.announce.rawValue {
|
if packet.type != MessageType.announce.rawValue {
|
||||||
SecureLogger.log("📦 Decoded packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("📦 Decoded packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
|
||||||
@@ -2052,45 +1983,15 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
|
|||||||
SecureLogger.log("❌ Failed to decode packet from central, full data: \(data.map { String(format: "%02x", $0) }.joined(separator: " "))", category: SecureLogger.session, level: .error)
|
SecureLogger.log("❌ Failed to decode packet from central, full data: \(data.map { String(format: "%02x", $0) }.joined(separator: " "))", category: SecureLogger.session, level: .error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Functions
|
|
||||||
|
|
||||||
private func hexStringToData(_ hex: String) -> Data {
|
|
||||||
var data = Data()
|
|
||||||
var tempID = hex
|
|
||||||
while tempID.count >= 2 {
|
|
||||||
let hexByte = String(tempID.prefix(2))
|
|
||||||
if let byte = UInt8(hexByte, radix: 16) {
|
|
||||||
data.append(byte)
|
|
||||||
}
|
|
||||||
tempID = String(tempID.dropFirst(2))
|
|
||||||
}
|
|
||||||
if tempID.count == 1 {
|
|
||||||
if let byte = UInt8(tempID, radix: 16) {
|
|
||||||
data.append(byte)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private func dataToHexString(_ data: Data) -> String {
|
|
||||||
return data.map { String(format: "%02x", $0) }.joined()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Advertising Builders & Alias Rotation
|
// MARK: - Advertising Builders & Alias Rotation
|
||||||
|
|
||||||
extension SimplifiedBluetoothService {
|
extension BLEService {
|
||||||
static func derivePeerID(fromPublicKey publicKey: Data) -> String {
|
|
||||||
let digest = SHA256.hash(data: publicKey)
|
|
||||||
let hex = digest.map { String(format: "%02x", $0) }.joined()
|
|
||||||
return String(hex.prefix(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func buildAdvertisementData() -> [String: Any] {
|
private func buildAdvertisementData() -> [String: Any] {
|
||||||
let data: [String: Any] = [
|
let data: [String: Any] = [
|
||||||
CBAdvertisementDataServiceUUIDsKey: [SimplifiedBluetoothService.serviceUUID]
|
CBAdvertisementDataServiceUUIDsKey: [BLEService.serviceUUID]
|
||||||
]
|
]
|
||||||
// No Local Name for privacy
|
// No Local Name for privacy
|
||||||
return data
|
return data
|
||||||
@@ -2098,300 +1999,3 @@ extension SimplifiedBluetoothService {
|
|||||||
|
|
||||||
// No alias rotation or advertising restarts required.
|
// No alias rotation or advertising restarts required.
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Nostr Embedding Helpers
|
|
||||||
|
|
||||||
extension SimplifiedBluetoothService {
|
|
||||||
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a private message
|
|
||||||
/// for transport over Nostr DMs. The payload is a plaintext typed NoisePayload
|
|
||||||
/// (no inner Noise encryption; NIP-17 provides transport-layer E2E).
|
|
||||||
func buildNostrEmbeddedPrivateMessageContent(content: String, to recipientPeerID: String, messageID: String) -> String? {
|
|
||||||
// TLV-encode the private message
|
|
||||||
let pm = PrivateMessagePacket(messageID: messageID, content: content)
|
|
||||||
guard let tlv = pm.encode() else { return nil }
|
|
||||||
|
|
||||||
// Prefix with NoisePayloadType
|
|
||||||
var payload = Data([NoisePayloadType.privateMessage.rawValue])
|
|
||||||
payload.append(tlv)
|
|
||||||
|
|
||||||
// Build BitChat packet (noiseEncrypted type used as a typed envelope)
|
|
||||||
// Determine correct 8-byte recipient ID (peerID) to embed
|
|
||||||
let recipientIDHex: String = {
|
|
||||||
if let maybeData = Data(hexString: recipientPeerID) {
|
|
||||||
if maybeData.count == 32 {
|
|
||||||
// Treat as Noise static public key; derive peerID from fingerprint
|
|
||||||
return Self.derivePeerID(fromPublicKey: maybeData)
|
|
||||||
} else if maybeData.count == 8 {
|
|
||||||
// Already an 8-byte peer ID
|
|
||||||
return recipientPeerID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback (should not happen): use myPeerID to avoid dropping
|
|
||||||
return recipientPeerID.count == 16 ? recipientPeerID : myPeerID
|
|
||||||
}()
|
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
|
||||||
senderID: hexStringToData(myPeerID),
|
|
||||||
recipientID: hexStringToData(recipientIDHex),
|
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
||||||
payload: payload,
|
|
||||||
signature: nil,
|
|
||||||
ttl: messageTTL
|
|
||||||
)
|
|
||||||
|
|
||||||
guard let data = packet.toBinaryData() else { return nil }
|
|
||||||
return "bitchat1:" + Self.base64URLEncode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a delivery/read ack
|
|
||||||
/// for transport over Nostr DMs. Payload is plaintext typed NoisePayload.
|
|
||||||
func buildNostrEmbeddedAckContent(type: NoisePayloadType, messageID: String, to recipientPeerID: String) -> String? {
|
|
||||||
guard type == .delivered || type == .readReceipt else { return nil }
|
|
||||||
|
|
||||||
var payload = Data([type.rawValue])
|
|
||||||
payload.append(Data(messageID.utf8))
|
|
||||||
|
|
||||||
// Determine correct 8-byte recipient ID (peerID) to embed
|
|
||||||
let recipientIDHex: String = {
|
|
||||||
if let maybeData = Data(hexString: recipientPeerID) {
|
|
||||||
if maybeData.count == 32 {
|
|
||||||
return Self.derivePeerID(fromPublicKey: maybeData)
|
|
||||||
} else if maybeData.count == 8 {
|
|
||||||
return recipientPeerID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return recipientPeerID.count == 16 ? recipientPeerID : myPeerID
|
|
||||||
}()
|
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
|
||||||
senderID: hexStringToData(myPeerID),
|
|
||||||
recipientID: hexStringToData(recipientIDHex),
|
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
||||||
payload: payload,
|
|
||||||
signature: nil,
|
|
||||||
ttl: messageTTL
|
|
||||||
)
|
|
||||||
|
|
||||||
guard let data = packet.toBinaryData() else { return nil }
|
|
||||||
return "bitchat1:" + Self.base64URLEncode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base64url encode without padding
|
|
||||||
private static func base64URLEncode(_ data: Data) -> String {
|
|
||||||
let b64 = data.base64EncodedString()
|
|
||||||
let urlSafe = b64
|
|
||||||
.replacingOccurrences(of: "+", with: "-")
|
|
||||||
.replacingOccurrences(of: "/", with: "_")
|
|
||||||
.replacingOccurrences(of: "=", with: "")
|
|
||||||
return urlSafe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Message Deduplicator
|
|
||||||
|
|
||||||
/// Efficient message deduplication with time-based cleanup
|
|
||||||
private class MessageDeduplicator {
|
|
||||||
private struct Entry {
|
|
||||||
let messageID: String
|
|
||||||
let timestamp: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
private var entries: [Entry] = []
|
|
||||||
private var lookup = Set<String>()
|
|
||||||
private let lock = NSLock()
|
|
||||||
private let maxAge: TimeInterval = 300 // 5 minutes
|
|
||||||
private let maxCount = 1000
|
|
||||||
|
|
||||||
/// Check if message is duplicate and add if not
|
|
||||||
func isDuplicate(_ messageID: String) -> Bool {
|
|
||||||
lock.lock()
|
|
||||||
defer { lock.unlock() }
|
|
||||||
|
|
||||||
// Clean old entries first
|
|
||||||
cleanupOldEntries()
|
|
||||||
|
|
||||||
if lookup.contains(messageID) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new entry
|
|
||||||
entries.append(Entry(messageID: messageID, timestamp: Date()))
|
|
||||||
lookup.insert(messageID)
|
|
||||||
|
|
||||||
// Efficient cleanup when over limit
|
|
||||||
if entries.count > maxCount {
|
|
||||||
// Remove oldest 100 entries
|
|
||||||
let toRemove = entries.prefix(100)
|
|
||||||
toRemove.forEach { lookup.remove($0.messageID) }
|
|
||||||
entries.removeFirst(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an ID without checking (for announce-back tracking)
|
|
||||||
func markProcessed(_ messageID: String) {
|
|
||||||
lock.lock()
|
|
||||||
defer { lock.unlock() }
|
|
||||||
|
|
||||||
if !lookup.contains(messageID) {
|
|
||||||
entries.append(Entry(messageID: messageID, timestamp: Date()))
|
|
||||||
lookup.insert(messageID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if ID exists without adding
|
|
||||||
func contains(_ messageID: String) -> Bool {
|
|
||||||
lock.lock()
|
|
||||||
defer { lock.unlock() }
|
|
||||||
return lookup.contains(messageID)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all entries
|
|
||||||
func reset() {
|
|
||||||
lock.lock()
|
|
||||||
defer { lock.unlock() }
|
|
||||||
|
|
||||||
entries.removeAll()
|
|
||||||
lookup.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Periodic cleanup (called from timer)
|
|
||||||
func cleanup() {
|
|
||||||
lock.lock()
|
|
||||||
defer { lock.unlock() }
|
|
||||||
|
|
||||||
cleanupOldEntries()
|
|
||||||
|
|
||||||
// Additional memory optimization if needed
|
|
||||||
if entries.capacity > maxCount * 2 {
|
|
||||||
entries.reserveCapacity(maxCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func cleanupOldEntries() {
|
|
||||||
let cutoff = Date().addingTimeInterval(-maxAge)
|
|
||||||
|
|
||||||
// Remove all entries older than cutoff
|
|
||||||
while let first = entries.first, first.timestamp < cutoff {
|
|
||||||
lookup.remove(first.messageID)
|
|
||||||
entries.removeFirst()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Supporting Types
|
|
||||||
|
|
||||||
private struct AnnouncementPacket {
|
|
||||||
let nickname: String
|
|
||||||
let publicKey: Data
|
|
||||||
|
|
||||||
private enum TLVType: UInt8 {
|
|
||||||
case nickname = 0x01
|
|
||||||
case noisePublicKey = 0x02
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode() -> Data? {
|
|
||||||
var data = Data()
|
|
||||||
|
|
||||||
// TLV for nickname
|
|
||||||
guard let nicknameData = nickname.data(using: .utf8), nicknameData.count <= 255 else { return nil }
|
|
||||||
data.append(TLVType.nickname.rawValue)
|
|
||||||
data.append(UInt8(nicknameData.count))
|
|
||||||
data.append(nicknameData)
|
|
||||||
|
|
||||||
// TLV for public key
|
|
||||||
guard publicKey.count <= 255 else { return nil }
|
|
||||||
data.append(TLVType.noisePublicKey.rawValue)
|
|
||||||
data.append(UInt8(publicKey.count))
|
|
||||||
data.append(publicKey)
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
static func decode(from data: Data) -> AnnouncementPacket? {
|
|
||||||
var offset = 0
|
|
||||||
var nickname: String?
|
|
||||||
var publicKey: Data?
|
|
||||||
|
|
||||||
while offset + 2 <= data.count {
|
|
||||||
guard let type = TLVType(rawValue: data[offset]) else { return nil }
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
let length = Int(data[offset])
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
guard offset + length <= data.count else { return nil }
|
|
||||||
let value = data[offset..<offset + length]
|
|
||||||
offset += length
|
|
||||||
|
|
||||||
switch type {
|
|
||||||
case .nickname:
|
|
||||||
nickname = String(data: value, encoding: .utf8)
|
|
||||||
case .noisePublicKey:
|
|
||||||
publicKey = Data(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let nickname = nickname, let publicKey = publicKey else { return nil }
|
|
||||||
return AnnouncementPacket(nickname: nickname, publicKey: publicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct PrivateMessagePacket {
|
|
||||||
let messageID: String
|
|
||||||
let content: String
|
|
||||||
|
|
||||||
private enum TLVType: UInt8 {
|
|
||||||
case messageID = 0x00
|
|
||||||
case content = 0x01
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode() -> Data? {
|
|
||||||
var data = Data()
|
|
||||||
|
|
||||||
// TLV for messageID
|
|
||||||
guard let messageIDData = messageID.data(using: .utf8), messageIDData.count <= 255 else { return nil }
|
|
||||||
data.append(TLVType.messageID.rawValue)
|
|
||||||
data.append(UInt8(messageIDData.count))
|
|
||||||
data.append(messageIDData)
|
|
||||||
|
|
||||||
// TLV for content
|
|
||||||
guard let contentData = content.data(using: .utf8), contentData.count <= 255 else { return nil }
|
|
||||||
data.append(TLVType.content.rawValue)
|
|
||||||
data.append(UInt8(contentData.count))
|
|
||||||
data.append(contentData)
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
static func decode(from data: Data) -> PrivateMessagePacket? {
|
|
||||||
var offset = 0
|
|
||||||
var messageID: String?
|
|
||||||
var content: String?
|
|
||||||
|
|
||||||
while offset + 2 <= data.count {
|
|
||||||
guard let type = TLVType(rawValue: data[offset]) else { return nil }
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
let length = Int(data[offset])
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
guard offset + length <= data.count else { return nil }
|
|
||||||
let value = data[offset..<offset + length]
|
|
||||||
offset += length
|
|
||||||
|
|
||||||
switch type {
|
|
||||||
case .messageID:
|
|
||||||
messageID = String(data: value, encoding: .utf8)
|
|
||||||
case .content:
|
|
||||||
content = String(data: value, encoding: .utf8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let messageID = messageID, let content = content else { return nil }
|
|
||||||
return PrivateMessagePacket(messageID: messageID, content: content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,9 +19,9 @@ enum CommandResult {
|
|||||||
@MainActor
|
@MainActor
|
||||||
class CommandProcessor {
|
class CommandProcessor {
|
||||||
weak var chatViewModel: ChatViewModel?
|
weak var chatViewModel: ChatViewModel?
|
||||||
weak var meshService: SimplifiedBluetoothService?
|
weak var meshService: Transport?
|
||||||
|
|
||||||
init(chatViewModel: ChatViewModel? = nil, meshService: SimplifiedBluetoothService? = nil) {
|
init(chatViewModel: ChatViewModel? = nil, meshService: Transport? = nil) {
|
||||||
self.chatViewModel = chatViewModel
|
self.chatViewModel = chatViewModel
|
||||||
self.meshService = meshService
|
self.meshService = meshService
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ class CommandProcessor {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// In public chat
|
// In public chat
|
||||||
meshService?.sendMessage(emoteContent)
|
meshService?.sendMessage(emoteContent, mentions: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
return .handled
|
return .handled
|
||||||
@@ -145,7 +145,7 @@ class CommandProcessor {
|
|||||||
var blockedNicknames: [String] = []
|
var blockedNicknames: [String] = []
|
||||||
if let peers = meshService?.getPeerNicknames() {
|
if let peers = meshService?.getPeerNicknames() {
|
||||||
for (peerID, nickname) in peers {
|
for (peerID, nickname) in peers {
|
||||||
if let fingerprint = meshService?.getPeerFingerprint(peerID),
|
if let fingerprint = meshService?.getFingerprint(for: peerID),
|
||||||
blockedUsers.contains(fingerprint) {
|
blockedUsers.contains(fingerprint) {
|
||||||
blockedNicknames.append(nickname)
|
blockedNicknames.append(nickname)
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ class CommandProcessor {
|
|||||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||||
|
|
||||||
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getPeerFingerprint(peerID) else {
|
let fingerprint = meshService?.getFingerprint(for: peerID) else {
|
||||||
return .error(message: "cannot block \(nickname): not found or unable to verify identity")
|
return .error(message: "cannot block \(nickname): not found or unable to verify identity")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ class CommandProcessor {
|
|||||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||||
|
|
||||||
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
guard let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getPeerFingerprint(peerID) else {
|
let fingerprint = meshService?.getFingerprint(for: peerID) else {
|
||||||
return .error(message: "cannot unblock \(nickname): not found")
|
return .error(message: "cannot unblock \(nickname): not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Routes messages between BLE and Nostr transports
|
||||||
|
@MainActor
|
||||||
|
final class MessageRouter {
|
||||||
|
private let mesh: Transport
|
||||||
|
private let nostr: NostrTransport
|
||||||
|
|
||||||
|
init(mesh: Transport, nostr: NostrTransport) {
|
||||||
|
self.mesh = mesh
|
||||||
|
self.nostr = nostr
|
||||||
|
self.nostr.senderPeerID = mesh.myPeerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
||||||
|
if mesh.isPeerConnected(peerID) {
|
||||||
|
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||||
|
} else {
|
||||||
|
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
||||||
|
if mesh.isPeerConnected(peerID) {
|
||||||
|
mesh.sendReadReceipt(receipt, to: peerID)
|
||||||
|
} else {
|
||||||
|
nostr.sendReadReceipt(receipt, to: peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendDeliveryAck(_ messageID: String, to peerID: String) {
|
||||||
|
if mesh.isPeerConnected(peerID) {
|
||||||
|
mesh.sendDeliveryAck(for: messageID, to: peerID)
|
||||||
|
} else {
|
||||||
|
nostr.sendDeliveryAck(for: messageID, to: peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
||||||
|
if mesh.isPeerConnected(peerID) {
|
||||||
|
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||||
|
} else {
|
||||||
|
nostr.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (SimplifiedBluetoothService) and the cryptographic layer (NoiseProtocol).
|
/// layer (BLEService) 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
|
||||||
/// - **SimplifiedBluetoothService**: Calls this service for all private messages
|
/// - **BLEService**: 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
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// Minimal Nostr transport conforming to Transport for offline sending
|
||||||
|
final class NostrTransport: Transport {
|
||||||
|
weak var delegate: BitchatDelegate?
|
||||||
|
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||||
|
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
||||||
|
Just([]).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] }
|
||||||
|
|
||||||
|
// Provide BLE short peer ID for BitChat embedding
|
||||||
|
var senderPeerID: String = ""
|
||||||
|
|
||||||
|
var myPeerID: String { senderPeerID }
|
||||||
|
var myNickname: String { "" }
|
||||||
|
func setNickname(_ nickname: String) { /* not used for Nostr */ }
|
||||||
|
|
||||||
|
func startServices() { /* no-op */ }
|
||||||
|
func stopServices() { /* no-op */ }
|
||||||
|
func emergencyDisconnectAll() { /* no-op */ }
|
||||||
|
|
||||||
|
func isPeerConnected(_ peerID: String) -> Bool { false }
|
||||||
|
func getPeerNicknames() -> [String : String] { [:] }
|
||||||
|
|
||||||
|
func getFingerprint(for peerID: String) -> String? { nil }
|
||||||
|
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
|
||||||
|
func triggerHandshake(with peerID: String) { /* no-op */ }
|
||||||
|
func getNoiseService() -> NoiseEncryptionService { NoiseEncryptionService() }
|
||||||
|
|
||||||
|
// Public broadcast not supported over Nostr here
|
||||||
|
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
||||||
|
|
||||||
|
func sendPrivateMessage(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
|
let recipientNostrPubkey = fav.peerNostrPublicKey else { return }
|
||||||
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
|
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID),
|
||||||
|
let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientNostrPubkey, senderIdentity: senderIdentity) else { return }
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
|
let recipientNostrPubkey = fav.peerNostrPublicKey else { return }
|
||||||
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
|
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: receipt.originalMessageID, recipientPeerID: peerID, senderPeerID: senderPeerID),
|
||||||
|
let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientNostrPubkey, senderIdentity: senderIdentity) else { return }
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
|
let recipientNostrPubkey = fav.peerNostrPublicKey else { return }
|
||||||
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
|
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
||||||
|
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID),
|
||||||
|
let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientNostrPubkey, senderIdentity: senderIdentity) else { return }
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendBroadcastAnnounce() { /* no-op for Nostr */ }
|
||||||
|
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
|
let recipientNostrPubkey = fav.peerNostrPublicKey else { return }
|
||||||
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
|
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID),
|
||||||
|
let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientNostrPubkey, senderIdentity: senderIdentity) else { return }
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@ class PrivateChatManager: ObservableObject {
|
|||||||
private var selectedPeerFingerprint: String? = nil
|
private var selectedPeerFingerprint: String? = nil
|
||||||
var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel
|
var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel
|
||||||
|
|
||||||
weak var meshService: SimplifiedBluetoothService?
|
weak var meshService: Transport?
|
||||||
|
|
||||||
init(meshService: SimplifiedBluetoothService? = nil) {
|
init(meshService: Transport? = nil) {
|
||||||
self.meshService = meshService
|
self.meshService = meshService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class PrivateChatManager: ObservableObject {
|
|||||||
selectedPeer = peerID
|
selectedPeer = peerID
|
||||||
|
|
||||||
// Store fingerprint for persistence across reconnections
|
// Store fingerprint for persistence across reconnections
|
||||||
if let fingerprint = meshService?.getPeerFingerprint(peerID) {
|
if let fingerprint = meshService?.getFingerprint(for: peerID) {
|
||||||
selectedPeerFingerprint = fingerprint
|
selectedPeerFingerprint = fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ class PrivateChatManager: ObservableObject {
|
|||||||
|
|
||||||
// Find peer with matching fingerprint
|
// Find peer with matching fingerprint
|
||||||
for (peerID, _) in peers {
|
for (peerID, _) in peers {
|
||||||
if meshService?.getPeerFingerprint(peerID) == fingerprint {
|
if meshService?.getFingerprint(for: peerID) == fingerprint {
|
||||||
selectedPeer = peerID
|
selectedPeer = peerID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -186,4 +186,4 @@ class PrivateChatManager: ObservableObject {
|
|||||||
// Send through mesh service's read receipt method
|
// Send through mesh service's read receipt method
|
||||||
meshService?.sendReadReceipt(receipt, to: senderPeerID)
|
meshService?.sendReadReceipt(receipt, to: senderPeerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Abstract transport interface used by ChatViewModel and services.
|
||||||
|
/// BLEService implements this protocol; a future Nostr transport can too.
|
||||||
|
struct TransportPeerSnapshot {
|
||||||
|
let id: String
|
||||||
|
let nickname: String
|
||||||
|
let isConnected: Bool
|
||||||
|
let noisePublicKey: Data?
|
||||||
|
let lastSeen: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol Transport: AnyObject {
|
||||||
|
// Peer events (preferred over publishers for UI)
|
||||||
|
var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
|
||||||
|
// Event sink
|
||||||
|
var delegate: BitchatDelegate? { get set }
|
||||||
|
|
||||||
|
// Identity
|
||||||
|
var myPeerID: String { get }
|
||||||
|
var myNickname: String { get }
|
||||||
|
func setNickname(_ nickname: String)
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
func startServices()
|
||||||
|
func stopServices()
|
||||||
|
func emergencyDisconnectAll()
|
||||||
|
|
||||||
|
// Connectivity and peers
|
||||||
|
func isPeerConnected(_ peerID: String) -> Bool
|
||||||
|
func getPeerNicknames() -> [String: String]
|
||||||
|
|
||||||
|
// Protocol utilities
|
||||||
|
func getFingerprint(for peerID: String) -> String?
|
||||||
|
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState
|
||||||
|
func triggerHandshake(with peerID: String)
|
||||||
|
func getNoiseService() -> NoiseEncryptionService
|
||||||
|
|
||||||
|
// Messaging
|
||||||
|
func sendMessage(_ content: String, mentions: [String])
|
||||||
|
func sendPrivateMessage(_ content: String, to peerID: String, recipientNickname: String, messageID: String)
|
||||||
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String)
|
||||||
|
func sendFavoriteNotification(to peerID: String, isFavorite: Bool)
|
||||||
|
func sendBroadcastAnnounce()
|
||||||
|
func sendDeliveryAck(for messageID: String, to peerID: String)
|
||||||
|
|
||||||
|
// Peer snapshots (for non-UI services)
|
||||||
|
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
|
||||||
|
func currentPeerSnapshots() -> [TransportPeerSnapshot]
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol TransportPeerEventsDelegate: AnyObject {
|
||||||
|
@MainActor func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot])
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BLEService: Transport {}
|
||||||
@@ -13,7 +13,7 @@ import CryptoKit
|
|||||||
|
|
||||||
/// Single source of truth for peer state, combining mesh connectivity and favorites
|
/// Single source of truth for peer state, combining mesh connectivity and favorites
|
||||||
@MainActor
|
@MainActor
|
||||||
class UnifiedPeerService: ObservableObject {
|
class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||||
|
|
||||||
// MARK: - Published Properties
|
// MARK: - Published Properties
|
||||||
|
|
||||||
@@ -26,13 +26,13 @@ class UnifiedPeerService: ObservableObject {
|
|||||||
|
|
||||||
private var peerIndex: [String: BitchatPeer] = [:]
|
private var peerIndex: [String: BitchatPeer] = [:]
|
||||||
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
|
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
|
||||||
private let meshService: SimplifiedBluetoothService
|
private let meshService: Transport
|
||||||
private let favoritesService = FavoritesPersistenceService.shared
|
private let favoritesService = FavoritesPersistenceService.shared
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
init(meshService: SimplifiedBluetoothService) {
|
init(meshService: Transport) {
|
||||||
self.meshService = meshService
|
self.meshService = meshService
|
||||||
|
|
||||||
// Subscribe to changes from both services
|
// Subscribe to changes from both services
|
||||||
@@ -47,14 +47,8 @@ class UnifiedPeerService: ObservableObject {
|
|||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
private func setupSubscriptions() {
|
private func setupSubscriptions() {
|
||||||
// Subscribe to mesh peer updates
|
// Subscribe to mesh peer updates via delegate (preferred over publishers)
|
||||||
meshService.fullPeersPublisher
|
meshService.peerEventsDelegate = self
|
||||||
.combineLatest(favoritesService.$favorites)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.updatePeers()
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
// Also listen for favorite change notifications
|
// Also listen for favorite change notifications
|
||||||
NotificationCenter.default.publisher(for: .favoriteStatusChanged)
|
NotificationCenter.default.publisher(for: .favoriteStatusChanged)
|
||||||
@@ -64,11 +58,16 @@ class UnifiedPeerService: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransportPeerEventsDelegate
|
||||||
|
func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot]) {
|
||||||
|
updatePeers()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Core Update Logic
|
// MARK: - Core Update Logic
|
||||||
|
|
||||||
private func updatePeers() {
|
private func updatePeers() {
|
||||||
let meshPeers = meshService.fullPeersPublisher.value
|
let meshPeers = meshService.currentPeerSnapshots()
|
||||||
let favorites = favoritesService.favorites
|
let favorites = favoritesService.favorites
|
||||||
|
|
||||||
var enrichedPeers: [BitchatPeer] = []
|
var enrichedPeers: [BitchatPeer] = []
|
||||||
@@ -76,11 +75,11 @@ class UnifiedPeerService: ObservableObject {
|
|||||||
var addedPeerIDs: Set<String> = []
|
var addedPeerIDs: Set<String> = []
|
||||||
|
|
||||||
// Phase 1: Add all connected mesh peers
|
// Phase 1: Add all connected mesh peers
|
||||||
for (peerID, peerInfo) in meshPeers where peerInfo.isConnected {
|
for peerInfo in meshPeers where peerInfo.isConnected {
|
||||||
|
let peerID = peerInfo.id
|
||||||
guard peerID != meshService.myPeerID else { continue } // Never add self
|
guard peerID != meshService.myPeerID else { continue } // Never add self
|
||||||
|
|
||||||
let peer = buildPeerFromMesh(
|
let peer = buildPeerFromMesh(
|
||||||
peerID: peerID,
|
|
||||||
peerInfo: peerInfo,
|
peerInfo: peerInfo,
|
||||||
favorites: favorites
|
favorites: favorites
|
||||||
)
|
)
|
||||||
@@ -162,12 +161,11 @@ class UnifiedPeerService: ObservableObject {
|
|||||||
// MARK: - Peer Building Helpers
|
// MARK: - Peer Building Helpers
|
||||||
|
|
||||||
private func buildPeerFromMesh(
|
private func buildPeerFromMesh(
|
||||||
peerID: String,
|
peerInfo: TransportPeerSnapshot,
|
||||||
peerInfo: SimplifiedBluetoothService.PeerInfoSnapshot,
|
|
||||||
favorites: [Data: FavoritesPersistenceService.FavoriteRelationship]
|
favorites: [Data: FavoritesPersistenceService.FavoriteRelationship]
|
||||||
) -> BitchatPeer {
|
) -> BitchatPeer {
|
||||||
var peer = BitchatPeer(
|
var peer = BitchatPeer(
|
||||||
id: peerID,
|
id: peerInfo.id,
|
||||||
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
||||||
nickname: peerInfo.nickname,
|
nickname: peerInfo.nickname,
|
||||||
lastSeen: peerInfo.lastSeen,
|
lastSeen: peerInfo.lastSeen,
|
||||||
@@ -365,7 +363,7 @@ class UnifiedPeerService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from mesh service
|
// Try to get from mesh service
|
||||||
if let fingerprint = meshService.getPeerFingerprint(peerID) {
|
if let fingerprint = meshService.getFingerprint(for: peerID) {
|
||||||
fingerprintCache[peerID] = fingerprint
|
fingerprintCache[peerID] = fingerprint
|
||||||
return fingerprint
|
return fingerprint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Message Deduplicator (shared)
|
||||||
|
|
||||||
|
final class MessageDeduplicator {
|
||||||
|
private struct Entry {
|
||||||
|
let messageID: String
|
||||||
|
let timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
private var entries: [Entry] = []
|
||||||
|
private var lookup = Set<String>()
|
||||||
|
private let lock = NSLock()
|
||||||
|
private let maxAge: TimeInterval = 300 // 5 minutes
|
||||||
|
private let maxCount = 1000
|
||||||
|
|
||||||
|
/// Check if message is duplicate and add if not
|
||||||
|
func isDuplicate(_ messageID: String) -> Bool {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
|
||||||
|
cleanupOldEntries()
|
||||||
|
|
||||||
|
if lookup.contains(messageID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(Entry(messageID: messageID, timestamp: Date()))
|
||||||
|
lookup.insert(messageID)
|
||||||
|
|
||||||
|
if entries.count > maxCount {
|
||||||
|
let toRemove = entries.prefix(100)
|
||||||
|
toRemove.forEach { lookup.remove($0.messageID) }
|
||||||
|
entries.removeFirst(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an ID without checking (for announce-back tracking)
|
||||||
|
func markProcessed(_ messageID: String) {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
|
||||||
|
if !lookup.contains(messageID) {
|
||||||
|
entries.append(Entry(messageID: messageID, timestamp: Date()))
|
||||||
|
lookup.insert(messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if ID exists without adding
|
||||||
|
func contains(_ messageID: String) -> Bool {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return lookup.contains(messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all entries
|
||||||
|
func reset() {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
|
||||||
|
entries.removeAll()
|
||||||
|
lookup.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periodic cleanup
|
||||||
|
func cleanup() {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
|
||||||
|
cleanupOldEntries()
|
||||||
|
|
||||||
|
if entries.capacity > maxCount * 2 {
|
||||||
|
entries.reserveCapacity(maxCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cleanupOldEntries() {
|
||||||
|
let cutoff = Date().addingTimeInterval(-maxAge)
|
||||||
|
while let first = entries.first, first.timestamp < cutoff {
|
||||||
|
lookup.remove(first.messageID)
|
||||||
|
entries.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PeerIDResolver {
|
||||||
|
/// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed
|
||||||
|
static func toShortID(_ id: String) -> String {
|
||||||
|
if id.count == 64, let data = Data(hexString: id) {
|
||||||
|
return PeerIDUtils.derivePeerID(fromPublicKey: data)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isShortID(_ id: String) -> Bool {
|
||||||
|
return id.count == 16 && Data(hexString: id) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isNoiseKeyHex(_ id: String) -> Bool {
|
||||||
|
return id.count == 64 && Data(hexString: id) != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
///
|
///
|
||||||
/// ## Architecture
|
/// ## Architecture
|
||||||
/// The ViewModel acts as:
|
/// The ViewModel acts as:
|
||||||
/// - **BitchatDelegate**: Receives messages and events from SimplifiedBluetoothService
|
/// - **BitchatDelegate**: Receives messages and events from BLEService
|
||||||
/// - **State Manager**: Maintains all UI-relevant state with @Published properties
|
/// - **State Manager**: Maintains all UI-relevant state with @Published properties
|
||||||
/// - **Command Processor**: Handles IRC-style commands (/msg, /who, etc.)
|
/// - **Command Processor**: Handles IRC-style commands (/msg, /who, etc.)
|
||||||
/// - **Message Router**: Directs messages to appropriate chats (public/private)
|
/// - **Message Router**: Directs messages to appropriate chats (public/private)
|
||||||
@@ -116,6 +116,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// MARK: - Service Delegates
|
// MARK: - Service Delegates
|
||||||
|
|
||||||
private let commandProcessor: CommandProcessor
|
private let commandProcessor: CommandProcessor
|
||||||
|
private let messageRouter: MessageRouter
|
||||||
private let privateChatManager: PrivateChatManager
|
private let privateChatManager: PrivateChatManager
|
||||||
private let unifiedPeerService: UnifiedPeerService
|
private let unifiedPeerService: UnifiedPeerService
|
||||||
private let autocompleteService: AutocompleteService
|
private let autocompleteService: AutocompleteService
|
||||||
@@ -181,7 +182,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// MARK: - Services and Storage
|
// MARK: - Services and Storage
|
||||||
|
|
||||||
var meshService = SimplifiedBluetoothService()
|
var meshService: Transport = BLEService()
|
||||||
private var nostrRelayManager: NostrRelayManager?
|
private var nostrRelayManager: NostrRelayManager?
|
||||||
// PeerManager replaced by UnifiedPeerService
|
// PeerManager replaced by UnifiedPeerService
|
||||||
private var processedNostrEvents = Set<String>() // Simple deduplication
|
private var processedNostrEvents = Set<String>() // Simple deduplication
|
||||||
@@ -274,6 +275,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.commandProcessor = CommandProcessor()
|
self.commandProcessor = CommandProcessor()
|
||||||
self.privateChatManager = PrivateChatManager(meshService: meshService)
|
self.privateChatManager = PrivateChatManager(meshService: meshService)
|
||||||
self.unifiedPeerService = UnifiedPeerService(meshService: meshService)
|
self.unifiedPeerService = UnifiedPeerService(meshService: meshService)
|
||||||
|
let nostrTransport = NostrTransport()
|
||||||
|
self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport)
|
||||||
self.autocompleteService = AutocompleteService()
|
self.autocompleteService = AutocompleteService()
|
||||||
|
|
||||||
// Wire up dependencies
|
// Wire up dependencies
|
||||||
@@ -857,14 +860,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Trigger UI update for sent message
|
// Trigger UI update for sent message
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
||||||
// Send via appropriate transport
|
// Send via appropriate transport (BLE if connected, else Nostr when possible)
|
||||||
if isConnected {
|
if isConnected || (isMutualFavorite && hasNostrKey) {
|
||||||
// Send via mesh
|
messageRouter.sendPrivate(content, to: peerID, recipientNickname: recipientNickname ?? "user", messageID: messageID)
|
||||||
meshService.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname ?? "user", messageID: messageID)
|
|
||||||
} else if isMutualFavorite && hasNostrKey,
|
|
||||||
let recipientNostrPubkey = favoriteStatus?.peerNostrPublicKey {
|
|
||||||
// Mutual favorite offline - send via Nostr
|
|
||||||
sendViaNostr(content, to: recipientNostrPubkey, recipientPeerID: peerID, messageId: messageID)
|
|
||||||
} else {
|
} else {
|
||||||
// Update delivery status to failed
|
// Update delivery status to failed
|
||||||
if let index = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
if let index = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
@@ -1304,6 +1302,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@objc private func userDidTakeScreenshot() {
|
@objc private func userDidTakeScreenshot() {
|
||||||
// Send screenshot notification based on current context
|
// Send screenshot notification based on current context
|
||||||
let screenshotMessage = "* \(nickname) took a screenshot *"
|
let screenshotMessage = "* \(nickname) took a screenshot *"
|
||||||
@@ -1317,7 +1316,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
switch sessionState {
|
switch sessionState {
|
||||||
case .established:
|
case .established:
|
||||||
// Send the message directly without going through sendPrivateMessage to avoid local echo
|
// Send the message directly without going through sendPrivateMessage to avoid local echo
|
||||||
meshService.sendPrivateMessage(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString)
|
messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString)
|
||||||
default:
|
default:
|
||||||
// Don't send screenshot notification if no session exists
|
// Don't send screenshot notification if no session exists
|
||||||
SecureLogger.log("Skipping screenshot notification to \(peerID) - no established session", category: SecureLogger.security, level: .debug)
|
SecureLogger.log("Skipping screenshot notification to \(peerID) - no established session", category: SecureLogger.security, level: .debug)
|
||||||
@@ -1414,7 +1413,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// The radical simplification plan says to accept occasional loss
|
// The radical simplification plan says to accept occasional loss
|
||||||
} else if meshService.getPeerNicknames()[actualPeerID] != nil {
|
} else if meshService.getPeerNicknames()[actualPeerID] != nil {
|
||||||
// Use mesh for connected peers (default behavior)
|
// Use mesh for connected peers (default behavior)
|
||||||
meshService.sendReadReceipt(receipt, to: actualPeerID)
|
messageRouter.sendReadReceipt(receipt, to: actualPeerID)
|
||||||
} else {
|
} else {
|
||||||
// Skip read receipts for offline peers - fire and forget principle
|
// Skip read receipts for offline peers - fire and forget principle
|
||||||
}
|
}
|
||||||
@@ -1447,7 +1446,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send Nostr read ACKs if peer has Nostr capability
|
// Send Nostr read ACKs if peer has Nostr capability
|
||||||
if let nostrPubkey = peerNostrPubkey {
|
if peerNostrPubkey != nil {
|
||||||
// Check messages under both ephemeral peer ID and stable Noise key
|
// Check messages under both ephemeral peer ID and stable Noise key
|
||||||
let messagesToAck = getPrivateChatMessages(for: peerID)
|
let messagesToAck = getPrivateChatMessages(for: peerID)
|
||||||
|
|
||||||
@@ -1459,7 +1458,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
// Use stable Noise key hex if available; else fall back to peerID
|
// Use stable Noise key hex if available; else fall back to peerID
|
||||||
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID)
|
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID)
|
||||||
sendNostrAcknowledgment(messageId: message.id, to: nostrPubkey, type: "READ", recipientPeerID: recipPeer)
|
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
|
||||||
|
messageRouter.sendReadReceipt(receipt, to: recipPeer)
|
||||||
sentReadReceipts.insert(message.id)
|
sentReadReceipts.insert(message.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2324,6 +2324,74 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
sendHapticFeedback(for: message)
|
sendHapticFeedback(for: message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Low-level BLE events
|
||||||
|
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||||
|
Task { @MainActor in
|
||||||
|
switch type {
|
||||||
|
case .privateMessage:
|
||||||
|
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
|
||||||
|
let senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
|
||||||
|
let pmMentions = parseMentions(from: pm.content)
|
||||||
|
let msg = BitchatMessage(
|
||||||
|
id: pm.messageID,
|
||||||
|
sender: senderName,
|
||||||
|
content: pm.content,
|
||||||
|
timestamp: timestamp,
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: true,
|
||||||
|
recipientNickname: nickname,
|
||||||
|
senderPeerID: peerID,
|
||||||
|
mentions: pmMentions.isEmpty ? nil : pmMentions
|
||||||
|
)
|
||||||
|
handlePrivateMessage(msg)
|
||||||
|
// Send delivery ACK back over BLE
|
||||||
|
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
||||||
|
|
||||||
|
case .delivered:
|
||||||
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
|
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
||||||
|
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||||
|
privateChats[peerID]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .readReceipt:
|
||||||
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
|
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
||||||
|
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||||
|
privateChats[peerID]?[idx].deliveryStatus = .read(by: name, at: Date())
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
||||||
|
Task { @MainActor in
|
||||||
|
let publicMentions = parseMentions(from: content)
|
||||||
|
let msg = BitchatMessage(
|
||||||
|
id: UUID().uuidString,
|
||||||
|
sender: nickname,
|
||||||
|
content: content,
|
||||||
|
timestamp: timestamp,
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: false,
|
||||||
|
recipientNickname: nil,
|
||||||
|
senderPeerID: peerID,
|
||||||
|
mentions: publicMentions.isEmpty ? nil : publicMentions
|
||||||
|
)
|
||||||
|
handlePublicMessage(msg)
|
||||||
|
checkForMentions(msg)
|
||||||
|
sendHapticFeedback(for: msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mention parsing moved from BLE – use the existing non-optional helper below
|
||||||
// MARK: - Peer Connection Events
|
// MARK: - Peer Connection Events
|
||||||
|
|
||||||
func didConnectToPeer(_ peerID: String) {
|
func didConnectToPeer(_ peerID: String) {
|
||||||
@@ -2688,122 +2756,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// MARK: - Simplified Nostr Integration (Inlined from MessageRouter)
|
// MARK: - Simplified Nostr Integration (Inlined from MessageRouter)
|
||||||
|
|
||||||
@MainActor
|
// Removed inlined Nostr send helpers in favor of MessageRouter
|
||||||
private func sendViaNostr(_ content: String, to recipientNostrPubkey: String, recipientPeerID: String, messageId: String) {
|
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
|
||||||
SecureLogger.log("No Nostr identity available", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode npub to hex if necessary
|
|
||||||
var recipientHexPubkey = recipientNostrPubkey
|
|
||||||
if recipientNostrPubkey.hasPrefix("npub") {
|
|
||||||
do {
|
|
||||||
let (hrp, data) = try Bech32.decode(recipientNostrPubkey)
|
|
||||||
if hrp == "npub" {
|
|
||||||
recipientHexPubkey = data.hexEncodedString()
|
|
||||||
SecureLogger.log("Decoded npub to hex: \(recipientHexPubkey)", category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
SecureLogger.log("Failed to decode recipient npub: \(error)", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build embedded BitChat packet content (bitchat1:...)
|
|
||||||
guard let embeddedContent = meshService.buildNostrEmbeddedPrivateMessageContent(
|
|
||||||
content: content,
|
|
||||||
to: recipientPeerID,
|
|
||||||
messageID: messageId
|
|
||||||
) else {
|
|
||||||
SecureLogger.log("Failed to build embedded BitChat content for Nostr", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let event = try? NostrProtocol.createPrivateMessage(
|
|
||||||
content: embeddedContent,
|
|
||||||
recipientPubkey: recipientHexPubkey,
|
|
||||||
senderIdentity: senderIdentity
|
|
||||||
) else {
|
|
||||||
SecureLogger.log("Failed to create Nostr message for recipient: \(recipientHexPubkey)", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SecureLogger.log("📝 Created Nostr event ID: \(event.id.prefix(16))... for recipient: \(recipientHexPubkey.prefix(16))...",
|
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
|
|
||||||
if let relayManager = nostrRelayManager {
|
|
||||||
relayManager.sendEvent(event)
|
|
||||||
SecureLogger.log("Sent Nostr message via relay", category: SecureLogger.session, level: .debug)
|
|
||||||
|
|
||||||
// Update delivery status to sent for Nostr messages
|
|
||||||
// Need to find which peerID (ephemeral or Noise key) contains this message
|
|
||||||
var messageUpdated = false
|
|
||||||
|
|
||||||
// First try to find the message in any private chat
|
|
||||||
for (chatPeerID, messages) in privateChats {
|
|
||||||
if let index = messages.firstIndex(where: { $0.id == messageId }) {
|
|
||||||
privateChats[chatPeerID]?[index].deliveryStatus = .sent
|
|
||||||
messageUpdated = true
|
|
||||||
SecureLogger.log("✅ Updated delivery status to sent for message \(messageId) in chat \(chatPeerID)",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
objectWillChange.send()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !messageUpdated {
|
|
||||||
SecureLogger.log("⚠️ Could not find message \(messageId) to update delivery status",
|
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SecureLogger.log("NostrRelayManager is nil - cannot send message", category: SecureLogger.session, level: .error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func sendNostrAcknowledgment(messageId: String, to recipientNostrPubkey: String, type: String, recipientPeerID: String) {
|
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
|
||||||
SecureLogger.log("No Nostr identity for sending ACK", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode npub to hex if necessary
|
|
||||||
var recipientHexPubkey = recipientNostrPubkey
|
|
||||||
if recipientNostrPubkey.hasPrefix("npub") {
|
|
||||||
do {
|
|
||||||
let (hrp, data) = try Bech32.decode(recipientNostrPubkey)
|
|
||||||
if hrp == "npub" {
|
|
||||||
recipientHexPubkey = data.hexEncodedString()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
SecureLogger.log("Failed to decode recipient npub for ACK: \(error)",
|
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build embedded BitChat ACK content
|
|
||||||
let ackType: NoisePayloadType? = (type == "DELIVERED") ? .delivered : (type == "READ" ? .readReceipt : nil)
|
|
||||||
guard let ackTypeUnwrapped = ackType,
|
|
||||||
let ackContent = meshService.buildNostrEmbeddedAckContent(type: ackTypeUnwrapped, messageID: messageId, to: recipientPeerID),
|
|
||||||
let event = try? NostrProtocol.createPrivateMessage(
|
|
||||||
content: ackContent,
|
|
||||||
recipientPubkey: recipientHexPubkey,
|
|
||||||
senderIdentity: senderIdentity
|
|
||||||
) else {
|
|
||||||
SecureLogger.log("Failed to create Nostr ACK for message \(messageId)",
|
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SecureLogger.log("📮 Sending \(type) ACK for message \(messageId.prefix(16))... to \(recipientHexPubkey.prefix(16))...",
|
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
|
|
||||||
if let relayManager = nostrRelayManager {
|
|
||||||
relayManager.sendEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func setupNostrMessageHandling() {
|
private func setupNostrMessageHandling() {
|
||||||
@@ -2893,7 +2846,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
switch noisePayload.type {
|
switch noisePayload.type {
|
||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
guard let (messageId, messageContent) = Self.decodePrivateMessageTLV(noisePayload.data) else { return }
|
guard let pm = PrivateMessagePacket.decode(from: noisePayload.data) else { return }
|
||||||
|
let messageId = pm.messageID
|
||||||
|
let messageContent = pm.content
|
||||||
|
|
||||||
// Favorite/unfavorite notifications embedded as private messages
|
// Favorite/unfavorite notifications embedded as private messages
|
||||||
if messageContent.hasPrefix("[FAVORITED]") || messageContent.hasPrefix("[UNFAVORITED]") {
|
if messageContent.hasPrefix("[FAVORITED]") || messageContent.hasPrefix("[UNFAVORITED]") {
|
||||||
@@ -2963,7 +2918,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Send delivery ack via Nostr embedded if not previously read and we know sender's Noise key
|
// Send delivery ack via Nostr embedded if not previously read and we know sender's Noise key
|
||||||
if !wasReadBefore, let key = actualSenderNoiseKey {
|
if !wasReadBefore, let key = actualSenderNoiseKey {
|
||||||
sendNostrAcknowledgment(messageId: messageId, to: senderPubkey, type: "DELIVERED", recipientPeerID: key.hexEncodedString())
|
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if wasReadBefore {
|
if wasReadBefore {
|
||||||
@@ -2975,7 +2930,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
unreadPrivateMessages.remove(ephemeralPeerID)
|
unreadPrivateMessages.remove(ephemeralPeerID)
|
||||||
}
|
}
|
||||||
if !sentReadReceipts.contains(messageId), let key = actualSenderNoiseKey {
|
if !sentReadReceipts.contains(messageId), let key = actualSenderNoiseKey {
|
||||||
sendNostrAcknowledgment(messageId: messageId, to: senderPubkey, type: "READ", recipientPeerID: key.hexEncodedString())
|
let receipt = ReadReceipt(originalMessageID: messageId, readerID: meshService.myPeerID, readerNickname: nickname)
|
||||||
|
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
|
||||||
sentReadReceipts.insert(messageId)
|
sentReadReceipts.insert(messageId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -3101,26 +3057,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return Data(base64Encoded: str)
|
return Data(base64Encoded: str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode PrivateMessagePacket TLV (local minimal decoder)
|
// Removed local TLV decoder; using PrivateMessagePacket.decode from Protocols
|
||||||
private static func decodePrivateMessageTLV(_ data: Data) -> (String, String)? {
|
|
||||||
var offset = 0
|
|
||||||
var messageID: String?
|
|
||||||
var content: String?
|
|
||||||
while offset + 2 <= data.count {
|
|
||||||
let type = data[offset]; offset += 1
|
|
||||||
let length = Int(data[offset]); offset += 1
|
|
||||||
guard offset + length <= data.count else { return nil }
|
|
||||||
let value = data[offset..<offset+length]
|
|
||||||
offset += length
|
|
||||||
if type == 0x00 {
|
|
||||||
messageID = String(data: value, encoding: .utf8)
|
|
||||||
} else if type == 0x01 {
|
|
||||||
content = String(data: value, encoding: .utf8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let id = messageID, let c = content { return (id, c) }
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: String, senderNickname: String) {
|
private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: String, senderNickname: String) {
|
||||||
@@ -3331,25 +3268,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
|
private func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
|
||||||
// Send favorite notification via Nostr when we don't have ephemeral peer ID
|
let peerIDHex = noisePublicKey.hexEncodedString()
|
||||||
if let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey),
|
messageRouter.sendFavoriteNotification(to: peerIDHex, isFavorite: isFavorite)
|
||||||
let recipientNostrPubkey = favoriteStatus.peerNostrPublicKey {
|
|
||||||
|
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
|
||||||
|
|
||||||
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
|
||||||
let recipientPeerID = noisePublicKey.hexEncodedString()
|
|
||||||
guard let embedded = meshService.buildNostrEmbeddedPrivateMessageContent(content: content, to: recipientPeerID, messageID: UUID().uuidString),
|
|
||||||
let event = try? NostrProtocol.createPrivateMessage(
|
|
||||||
content: embedded,
|
|
||||||
recipientPubkey: recipientNostrPubkey,
|
|
||||||
senderIdentity: senderIdentity
|
|
||||||
) else { return }
|
|
||||||
if let relayManager = nostrRelayManager {
|
|
||||||
relayManager.sendEvent(event)
|
|
||||||
SecureLogger.log("Sent favorite notification via Nostr", category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -3369,34 +3289,12 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Try mesh first for connected peers
|
// Try mesh first for connected peers
|
||||||
if meshService.isPeerConnected(peerID) {
|
if meshService.isPeerConnected(peerID) {
|
||||||
meshService.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||||
SecureLogger.log("📤 Sent favorite notification via BLE to \(peerID)", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("📤 Sent favorite notification via BLE to \(peerID)", category: SecureLogger.session, level: .debug)
|
||||||
} else if let key = noiseKey,
|
} else if let key = noiseKey {
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: key),
|
// Send via Nostr for offline peers (using router)
|
||||||
let recipientNostrPubkey = favoriteStatus.peerNostrPublicKey {
|
|
||||||
// Send via Nostr for offline peers
|
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
|
|
||||||
SecureLogger.log("❌ No Nostr identity for sending favorite notification", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
|
||||||
let recipientPeerID = key.hexEncodedString()
|
let recipientPeerID = key.hexEncodedString()
|
||||||
guard let embedded = meshService.buildNostrEmbeddedPrivateMessageContent(content: content, to: recipientPeerID, messageID: UUID().uuidString),
|
messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite)
|
||||||
let event = try? NostrProtocol.createPrivateMessage(
|
|
||||||
content: embedded,
|
|
||||||
recipientPubkey: recipientNostrPubkey,
|
|
||||||
senderIdentity: senderIdentity
|
|
||||||
) else {
|
|
||||||
SecureLogger.log("❌ Failed to create Nostr message for favorite notification", category: SecureLogger.session, level: .error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let relayManager = nostrRelayManager {
|
|
||||||
relayManager.sendEvent(event)
|
|
||||||
SecureLogger.log("📤 Sent favorite notification via Nostr to \(favoriteStatus.peerNickname)", category: SecureLogger.session, level: .debug)
|
|
||||||
} else {
|
|
||||||
SecureLogger.log("❌ NostrRelayManager is nil - cannot send favorite notification", category: SecureLogger.session, level: .error)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: SecureLogger.session, level: .warning)
|
SecureLogger.log("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: SecureLogger.session, level: .warning)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// SimplifiedBluetoothServiceTests.swift
|
// BLEServiceTests.swift
|
||||||
// bitchatTests
|
// bitchatTests
|
||||||
//
|
//
|
||||||
// This is free and unencumbered software released into the public domain.
|
// This is free and unencumbered software released into the public domain.
|
||||||
@@ -10,13 +10,13 @@ import XCTest
|
|||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
final class SimplifiedBluetoothServiceTests: XCTestCase {
|
final class BLEServiceTests: XCTestCase {
|
||||||
|
|
||||||
var service: MockSimplifiedBluetoothService!
|
var service: MockBLEService!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
service = MockSimplifiedBluetoothService()
|
service = MockBLEService()
|
||||||
service.myPeerID = "TEST1234"
|
service.myPeerID = "TEST1234"
|
||||||
service.mockNickname = "TestUser"
|
service.mockNickname = "TestUser"
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ 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)
|
||||||
|
|
||||||
// Delivery tracking is now handled internally by SimplifiedBluetoothService
|
// Delivery tracking is now handled internally by BLEService
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
@@ -92,13 +92,13 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
|
|
||||||
// MARK: - Delivery Acknowledgment Tests
|
// MARK: - Delivery Acknowledgment Tests
|
||||||
|
|
||||||
// NOTE: DeliveryTracker has been removed in SimplifiedBluetoothService.
|
// NOTE: DeliveryTracker has been removed in BLEService.
|
||||||
// Delivery tracking is now handled internally.
|
// Delivery tracking is now handled internally.
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Message Retry Tests
|
// MARK: - Message Retry Tests
|
||||||
|
|
||||||
// NOTE: MessageRetryService has been removed in SimplifiedBluetoothService.
|
// NOTE: MessageRetryService has been removed in BLEService.
|
||||||
// Retry logic is now handled internally.
|
// Retry logic is now handled internally.
|
||||||
|
|
||||||
|
|
||||||
@@ -337,4 +337,4 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
peer1.simulateConnectedPeer(peer2.peerID)
|
peer1.simulateConnectedPeer(peer2.peerID)
|
||||||
peer2.simulateConnectedPeer(peer1.peerID)
|
peer2.simulateConnectedPeer(peer1.peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// MockSimplifiedBluetoothService.swift
|
// MockBLEService.swift
|
||||||
// bitchatTests
|
// bitchatTests
|
||||||
//
|
//
|
||||||
// This is free and unencumbered software released into the public domain.
|
// This is free and unencumbered software released into the public domain.
|
||||||
@@ -10,10 +10,10 @@ import Foundation
|
|||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
// Mock implementation that mimics SimplifiedBluetoothService behavior
|
// Mock implementation that mimics BLEService behavior
|
||||||
class MockSimplifiedBluetoothService: NSObject {
|
class MockBLEService: NSObject {
|
||||||
|
|
||||||
// MARK: - Properties matching SimplifiedBluetoothService
|
// MARK: - Properties matching BLEService
|
||||||
|
|
||||||
weak var delegate: BitchatDelegate?
|
weak var delegate: BitchatDelegate?
|
||||||
var myPeerID: String = "MOCK1234"
|
var myPeerID: String = "MOCK1234"
|
||||||
@@ -46,7 +46,7 @@ class MockSimplifiedBluetoothService: NSObject {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Methods matching SimplifiedBluetoothService
|
// MARK: - Methods matching BLEService
|
||||||
|
|
||||||
func setNickname(_ nickname: String) {
|
func setNickname(_ nickname: String) {
|
||||||
self.myNickname = nickname
|
self.myNickname = nickname
|
||||||
@@ -224,4 +224,7 @@ class MockSimplifiedBluetoothService: NSObject {
|
|||||||
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
|
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
|
||||||
sendPrivateMessage(content, to: recipientPeerID, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString)
|
sendPrivateMessage(content, to: recipientPeerID, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backward compatibility for older tests
|
||||||
|
typealias MockSimplifiedBluetoothService = MockBLEService
|
||||||
@@ -10,5 +10,5 @@ import Foundation
|
|||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
// Compatibility wrapper for old tests - please use MockSimplifiedBluetoothService directly
|
// Compatibility wrapper for old tests - please use MockBLEService directly
|
||||||
typealias MockBluetoothMeshService = MockSimplifiedBluetoothService
|
typealias MockBluetoothMeshService = MockBLEService
|
||||||
|
|||||||
Reference in New Issue
Block a user