mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Add room-wide mandatory retention, update UI formatting, and documentation
- Implement room-wide message retention controlled by room owners - Change username format from <name> to <@name> throughout UI - Fix text alignment in chat messages (consistent font sizes) - Add comprehensive technical whitepaper with Mermaid diagrams - Update README with current features and commands - Add retention status indicators and announcements - Update command help text to use short versions (/j, /m)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||

|
||||
# bitchat
|
||||
|
||||
A secure, end-to-end encrypted Bluetooth mesh chat application with an IRC-style interface.
|
||||
A secure, decentralized, peer-to-peer messaging app that works over Bluetooth mesh networks. No internet required, no servers, no phone numbers - just pure encrypted communication.
|
||||
|
||||
## License
|
||||
|
||||
@@ -9,13 +9,16 @@ This project is released into the public domain. See the [LICENSE](LICENSE) file
|
||||
|
||||
## Features
|
||||
|
||||
- End-to-end encryption using Curve25519 and AES-GCM
|
||||
- Bluetooth mesh networking with automatic peer discovery
|
||||
- Message relay capability (TTL-based flooding)
|
||||
- IRC-style terminal interface
|
||||
- Persistent nickname storage
|
||||
- Universal app (iOS and macOS)
|
||||
- No internet connection required
|
||||
- **Decentralized Mesh Network**: Automatic peer discovery and multi-hop message relay over Bluetooth LE
|
||||
- **End-to-End Encryption**: X25519 key exchange + AES-256-GCM for private messages
|
||||
- **Room-Based Chats**: Topic-based group messaging with optional password protection
|
||||
- **Store & Forward**: Messages cached for offline peers and delivered when they reconnect
|
||||
- **Privacy First**: No accounts, no phone numbers, no persistent identifiers
|
||||
- **IRC-Style Commands**: Familiar `/join`, `/msg`, `/who` style interface
|
||||
- **Message Retention**: Optional room-wide message saving controlled by room owners
|
||||
- **Universal App**: Native support for iOS and macOS
|
||||
- **Cover Traffic**: Timing obfuscation and dummy messages for enhanced privacy
|
||||
- **Emergency Wipe**: Triple-tap to instantly clear all data
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -56,25 +59,63 @@ This project is released into the public domain. See the [LICENSE](LICENSE) file
|
||||
|
||||
## Usage
|
||||
|
||||
1. Launch the app on multiple devices
|
||||
2. Choose or modify your nickname
|
||||
3. The app will automatically discover nearby peers
|
||||
4. Start chatting! Messages are relayed through the mesh network
|
||||
### Basic Commands
|
||||
|
||||
## Security
|
||||
- `/j #room` - Join or create a room
|
||||
- `/m @user message` - Send a private message
|
||||
- `/w` - List online users
|
||||
- `/rooms` - Show all discovered rooms
|
||||
- `/clear` - Clear chat messages
|
||||
- `/pass [password]` - Set/change room password (owner only)
|
||||
- `/transfer @user` - Transfer room ownership
|
||||
- `/save` - Toggle message retention for room (owner only)
|
||||
|
||||
- All messages are end-to-end encrypted
|
||||
- Public key exchange happens automatically on connection
|
||||
- Messages are signed to prevent tampering
|
||||
- TTL prevents infinite message loops
|
||||
### Getting Started
|
||||
|
||||
## Protocol
|
||||
1. Launch bitchat on your device
|
||||
2. Set your nickname (or use the auto-generated one)
|
||||
3. You'll automatically connect to nearby peers
|
||||
4. Join a room with `/j #general` or start chatting in public
|
||||
5. Messages relay through the mesh network to reach distant peers
|
||||
|
||||
The bitchat protocol uses JSON-encoded packets with the following structure:
|
||||
- Packet versioning for future compatibility
|
||||
- Message types: handshake, message, ack, relay, announce, keyExchange
|
||||
- TTL-based flooding for mesh relay
|
||||
- Signature verification for authenticity
|
||||
### Room Features
|
||||
|
||||
- **Password Protection**: Room owners can set passwords with `/pass`
|
||||
- **Message Retention**: Owners can enable mandatory message saving with `/save`
|
||||
- **@ Mentions**: Use `@nickname` to mention users (with autocomplete)
|
||||
- **Ownership Transfer**: Pass control to trusted users with `/transfer`
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
### Encryption
|
||||
- **Private Messages**: X25519 key exchange + AES-256-GCM encryption
|
||||
- **Room Messages**: Argon2id password derivation + AES-256-GCM
|
||||
- **Digital Signatures**: Ed25519 for message authenticity
|
||||
- **Forward Secrecy**: New key pairs generated each session
|
||||
|
||||
### Privacy Features
|
||||
- **No Registration**: No accounts, emails, or phone numbers required
|
||||
- **Ephemeral by Default**: Messages exist only in device memory
|
||||
- **Cover Traffic**: Random delays and dummy messages prevent traffic analysis
|
||||
- **Emergency Wipe**: Triple-tap logo to instantly clear all data
|
||||
- **Local-First**: Works completely offline, no servers involved
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Binary Protocol
|
||||
bitchat uses an efficient binary protocol optimized for Bluetooth LE:
|
||||
- Compact packet format with 1-byte type field
|
||||
- TTL-based message routing (max 7 hops)
|
||||
- Automatic fragmentation for large messages
|
||||
- Message deduplication via unique IDs
|
||||
|
||||
### Mesh Networking
|
||||
- Each device acts as both client and peripheral
|
||||
- Automatic peer discovery and connection management
|
||||
- Store-and-forward for offline message delivery
|
||||
- Adaptive duty cycling for battery optimization
|
||||
|
||||
For detailed protocol documentation, see the [Technical Whitepaper](WHITEPAPER.md).
|
||||
|
||||
## Building for Production
|
||||
|
||||
|
||||
+744
@@ -0,0 +1,744 @@
|
||||
# bitchat Technical Whitepaper
|
||||
|
||||
## Abstract
|
||||
|
||||
bitchat is a decentralized, peer-to-peer messaging application that operates over Bluetooth Low Energy (BLE) mesh networks. It provides ephemeral, encrypted communication without relying on internet infrastructure, making it resilient to network outages and censorship. This whitepaper details the technical architecture, protocols, and privacy mechanisms that enable secure, decentralized communication.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Architecture Overview](#architecture-overview)
|
||||
3. [Bluetooth Mesh Network](#bluetooth-mesh-network)
|
||||
4. [Message Relay Protocol](#message-relay-protocol)
|
||||
5. [Store and Forward Mechanism](#store-and-forward-mechanism)
|
||||
6. [Encryption and Security](#encryption-and-security)
|
||||
7. [Room-Based Communication](#room-based-communication)
|
||||
8. [Binary Protocol Specification](#binary-protocol-specification)
|
||||
9. [Privacy Features](#privacy-features)
|
||||
10. [Message Fragmentation](#message-fragmentation)
|
||||
11. [Conclusion](#conclusion)
|
||||
|
||||
## Introduction
|
||||
|
||||
bitchat addresses the need for resilient, private communication that doesn't depend on centralized infrastructure. By leveraging Bluetooth Low Energy mesh networking, bitchat enables direct peer-to-peer messaging within physical proximity, with automatic message relay extending the effective range beyond direct Bluetooth connections.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Decentralized**: No servers, no infrastructure dependencies
|
||||
- **Ephemeral**: Messages exist only in device memory by default
|
||||
- **Encrypted**: End-to-end encryption for private messages
|
||||
- **Resilient**: Automatic mesh networking and message relay
|
||||
- **Private**: No phone numbers, emails, or permanent identifiers
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Application Layer"
|
||||
UI[Chat UI]
|
||||
CMD[Commands]
|
||||
ROOM[Room Management]
|
||||
end
|
||||
|
||||
subgraph "Service Layer"
|
||||
ENC[Encryption Service]
|
||||
RETRY[Message Retry Service]
|
||||
RETAIN[Message Retention Service]
|
||||
end
|
||||
|
||||
subgraph "Mesh Network Layer"
|
||||
ROUTE[Message Router]
|
||||
RELAY[Relay Engine]
|
||||
STORE[Store & Forward Cache]
|
||||
end
|
||||
|
||||
subgraph "Transport Layer"
|
||||
PROTO[Binary Protocol]
|
||||
FRAG[Fragment Handler]
|
||||
BLE[BLE Central/Peripheral]
|
||||
end
|
||||
|
||||
UI & CMD & ROOM --> ENC & RETRY & RETAIN
|
||||
ENC & RETRY & RETAIN --> ROUTE & RELAY & STORE
|
||||
ROUTE & RELAY & STORE --> PROTO & FRAG & BLE
|
||||
|
||||
style UI fill:#e1f5fe
|
||||
style CMD fill:#e1f5fe
|
||||
style ROOM fill:#e1f5fe
|
||||
style ENC fill:#f3e5f5
|
||||
style RETRY fill:#f3e5f5
|
||||
style RETAIN fill:#f3e5f5
|
||||
style ROUTE fill:#e8f5e9
|
||||
style RELAY fill:#e8f5e9
|
||||
style STORE fill:#e8f5e9
|
||||
style PROTO fill:#fff3e0
|
||||
style FRAG fill:#fff3e0
|
||||
style BLE fill:#fff3e0
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Bluetooth Mesh Network
|
||||
|
||||
bitchat implements a custom mesh networking protocol over BLE, where each device acts as both a central (client) and peripheral (server), enabling multi-hop message delivery.
|
||||
|
||||
### Network Topology
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Physical Space (e.g., Conference, Protest, Disaster Area)"
|
||||
subgraph "Zone A"
|
||||
A1[Alice<br/>📱]
|
||||
A2[Bob<br/>📱]
|
||||
A3[Carol<br/>📱]
|
||||
end
|
||||
|
||||
subgraph "Zone B"
|
||||
B1[Dave<br/>📱]
|
||||
B2[Eve<br/>📱]
|
||||
B3[Frank<br/>📱]
|
||||
end
|
||||
|
||||
subgraph "Zone C"
|
||||
C1[Grace<br/>📱]
|
||||
C2[Henry<br/>📱]
|
||||
C3[Iris<br/>📱]
|
||||
end
|
||||
end
|
||||
|
||||
A1 -.->|BLE| A2
|
||||
A2 -.->|BLE| A3
|
||||
A1 -.->|BLE| A3
|
||||
|
||||
B1 -.->|BLE| B2
|
||||
B2 -.->|BLE| B3
|
||||
B1 -.->|BLE| B3
|
||||
|
||||
C1 -.->|BLE| C2
|
||||
C2 -.->|BLE| C3
|
||||
C1 -.->|BLE| C3
|
||||
|
||||
A3 ==>|Bridge| B1
|
||||
B3 ==>|Bridge| C1
|
||||
|
||||
style A1 fill:#e3f2fd
|
||||
style A2 fill:#e3f2fd
|
||||
style A3 fill:#e3f2fd
|
||||
style B1 fill:#f3e5f5
|
||||
style B2 fill:#f3e5f5
|
||||
style B3 fill:#f3e5f5
|
||||
style C1 fill:#e8f5e9
|
||||
style C2 fill:#e8f5e9
|
||||
style C3 fill:#e8f5e9
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
In this topology:
|
||||
- **Local clusters** form based on physical proximity (≈30m range)
|
||||
- **Bridge nodes** connect clusters when in overlapping range
|
||||
- **Messages hop** across the network reaching distant peers
|
||||
- **No infrastructure** required - completely peer-to-peer
|
||||
|
||||
### Peer Discovery and Connection
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Device A
|
||||
participant B as Device B
|
||||
participant C as Device C
|
||||
|
||||
Note over A,C: Discovery Phase
|
||||
A->>B: Advertise (Peripheral)
|
||||
B->>C: Advertise (Peripheral)
|
||||
B->>A: Scan & Connect (Central)
|
||||
C->>B: Scan & Connect (Central)
|
||||
|
||||
Note over A,C: Communication Phase
|
||||
A<-->B: Bidirectional Communication
|
||||
B<-->C: Bidirectional Communication
|
||||
|
||||
Note over A: Acts as both<br/>Central & Peripheral
|
||||
Note over B: Acts as both<br/>Central & Peripheral
|
||||
Note over C: Acts as both<br/>Central & Peripheral
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Each device:
|
||||
1. **Advertises** as a BLE peripheral with the bitchat service UUID
|
||||
2. **Scans** for other devices advertising the same service
|
||||
3. **Connects** to discovered peers as a central
|
||||
4. **Maintains** simultaneous connections as both central and peripheral
|
||||
|
||||
### Connection Management
|
||||
|
||||
The mesh network automatically handles:
|
||||
- **Connection limits**: Manages BLE connection constraints
|
||||
- **Duty cycling**: Balances battery life with connectivity
|
||||
- **Peer tracking**: Maintains active peer lists with RSSI values
|
||||
- **Automatic reconnection**: Handles connection drops gracefully
|
||||
|
||||
## Message Relay Protocol
|
||||
|
||||
The relay protocol enables messages to reach peers beyond direct Bluetooth range through multi-hop forwarding.
|
||||
|
||||
### TTL-Based Routing
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Device A<br/>Origin<br/>TTL=3] -->|TTL=3| B[Device B<br/>Relay 1<br/>TTL=2]
|
||||
B -->|TTL=2| C[Device C<br/>Relay 2<br/>TTL=1]
|
||||
C -->|TTL=1| D[Device D<br/>Final<br/>TTL=0]
|
||||
B -->|TTL=2| E[Device E<br/>TTL=1]
|
||||
C -->|TTL=1| F[Device F<br/>TTL=1]
|
||||
|
||||
style A fill:#4caf50,color:#fff
|
||||
style D fill:#2196f3,color:#fff
|
||||
style B fill:#ffc107
|
||||
style C fill:#ffc107
|
||||
style E fill:#ff9800
|
||||
style F fill:#ff9800
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Each message includes a Time-To-Live (TTL) field:
|
||||
- **Initial TTL**: Set to 7 for maximum reach
|
||||
- **Decrement**: Each relay decrements TTL by 1
|
||||
- **Drop**: Messages with TTL=0 are not forwarded
|
||||
- **Loop prevention**: Message IDs prevent circular routing
|
||||
|
||||
### Relay Decision Logic
|
||||
|
||||
```python
|
||||
function shouldRelay(packet):
|
||||
if packet.ttl <= 0:
|
||||
return false
|
||||
if packet.messageID in processedMessages:
|
||||
return false
|
||||
if packet.recipientID == myID:
|
||||
return false # We're the destination
|
||||
if packet.recipientID == broadcast:
|
||||
return true # Always relay broadcasts
|
||||
return true # Relay private messages for mesh
|
||||
```
|
||||
|
||||
## Store and Forward Mechanism
|
||||
|
||||
The store-and-forward system ensures message delivery to temporarily offline peers.
|
||||
|
||||
### Message Caching
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Message Cache Architecture"
|
||||
subgraph "Regular Messages"
|
||||
R1[Message 1]
|
||||
R2[Message 2]
|
||||
R3[Message 3]
|
||||
R4[...]
|
||||
R1 -.->|12hr TTL| R2
|
||||
R2 -.->|100 msg limit| R3
|
||||
R3 -.-> R4
|
||||
end
|
||||
|
||||
subgraph "Favorite Peer Messages"
|
||||
F1[Favorite 1]
|
||||
F2[Favorite 2]
|
||||
F3[Favorite 3]
|
||||
F4[...]
|
||||
F1 -.->|No TTL| F2
|
||||
F2 -.->|1000 msg limit| F3
|
||||
F3 -.-> F4
|
||||
end
|
||||
end
|
||||
|
||||
style R1 fill:#e3f2fd
|
||||
style R2 fill:#e3f2fd
|
||||
style R3 fill:#e3f2fd
|
||||
style F1 fill:#fce4ec
|
||||
style F2 fill:#fce4ec
|
||||
style F3 fill:#fce4ec
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Delivery Flow
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Sender A
|
||||
participant B as Relay B
|
||||
participant C as Recipient C
|
||||
|
||||
Note over C: Offline
|
||||
A->>B: Send Message
|
||||
B->>B: Store in Cache
|
||||
B--xC: Delivery Failed<br/>(Recipient Offline)
|
||||
|
||||
Note over C: Comes Online
|
||||
C->>B: Announce Presence
|
||||
B->>C: Deliver Cached Messages
|
||||
Note over C: Messages Received
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Key features:
|
||||
- **Automatic caching**: Messages cached when recipient unreachable
|
||||
- **Tiered retention**: Regular (12hr) vs favorite peer (indefinite)
|
||||
- **Delivery on reconnect**: Cached messages sent when peer returns
|
||||
- **Duplicate prevention**: Message IDs prevent redundant delivery
|
||||
|
||||
## Encryption and Security
|
||||
|
||||
bitchat implements multiple layers of encryption for secure communication.
|
||||
|
||||
### Key Exchange Protocol
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
|
||||
Alice->>Bob: Announce (includes public key)
|
||||
Note over Bob: Stores Alice's public key
|
||||
|
||||
Bob->>Alice: Key Exchange Request<br/>(Bob's public key)
|
||||
Note over Alice: Derives shared secret<br/>using X25519
|
||||
|
||||
Alice->>Bob: Key Exchange Response<br/>(Encrypted with shared secret)
|
||||
Note over Bob: Derives shared secret<br/>verifies response
|
||||
|
||||
Alice<-->Bob: Encrypted Communication<br/>(AES-256-GCM)
|
||||
|
||||
Note over Alice,Bob: Forward Secrecy Achieved
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Encryption Layers
|
||||
|
||||
1. **Private Messages**: X25519 key exchange + AES-256-GCM
|
||||
2. **Room Messages**: Password-derived keys using Argon2id
|
||||
3. **Digital Signatures**: Ed25519 for message authenticity
|
||||
|
||||
### Key Derivation for Rooms
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
P[Password] --> A[Argon2id]
|
||||
A --> K[256-bit Key]
|
||||
K --> AES[AES-256-GCM]
|
||||
|
||||
S[Salt: SHA256(roomName)] --> A
|
||||
I[Iterations: 10] --> A
|
||||
M[Memory: 64MB] --> A
|
||||
T[Parallelism: 4] --> A
|
||||
|
||||
style P fill:#ffccbc
|
||||
style A fill:#b3e5fc
|
||||
style K fill:#c8e6c9
|
||||
style AES fill:#d1c4e9
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Room-Based Communication
|
||||
|
||||
Rooms provide topic-based group messaging with optional password protection.
|
||||
|
||||
### Room State Machine
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Discovery
|
||||
Discovery --> Joined: /j #room
|
||||
Joined --> PasswordPrompt: Room is protected
|
||||
Joined --> Unlocked: Room is public
|
||||
PasswordPrompt --> Unlocked: Correct password
|
||||
PasswordPrompt --> PasswordPrompt: Wrong password
|
||||
Unlocked --> [*]: Leave room
|
||||
|
||||
state Discovery {
|
||||
[*] --> Scanning
|
||||
Scanning --> Found: Room activity detected
|
||||
}
|
||||
|
||||
state Unlocked {
|
||||
[*] --> Active
|
||||
Active --> Sending: Send message
|
||||
Sending --> Active: Message sent
|
||||
Active --> Receiving: Receive message
|
||||
Receiving --> Active: Message displayed
|
||||
}
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Room Features
|
||||
|
||||
- **Hashtag naming**: Rooms identified by #roomname
|
||||
- **Password protection**: Optional encryption with shared passwords
|
||||
- **Owner privileges**: Transfer ownership, change passwords
|
||||
- **Message retention**: Owner-controlled mandatory retention
|
||||
- **Decentralized discovery**: Rooms discovered through usage
|
||||
|
||||
## Binary Protocol Specification
|
||||
|
||||
bitchat uses an efficient binary protocol to minimize bandwidth usage.
|
||||
|
||||
### Packet Structure
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class BitchatPacket {
|
||||
+uint8 version
|
||||
+uint8 type
|
||||
+bytes[8] senderID
|
||||
+bytes[8] recipientID
|
||||
+uint64 timestamp
|
||||
+uint8 ttl
|
||||
+bytes[] payload
|
||||
+bytes[64] signature
|
||||
}
|
||||
|
||||
class PacketHeader {
|
||||
<<1 byte>> version
|
||||
<<1 byte>> type
|
||||
<<8 bytes>> senderID
|
||||
<<8 bytes>> recipientID
|
||||
}
|
||||
|
||||
class PacketBody {
|
||||
<<8 bytes>> timestamp
|
||||
<<1 byte>> ttl
|
||||
<<variable>> payload
|
||||
}
|
||||
|
||||
class PacketSignature {
|
||||
<<64 bytes>> Ed25519 signature
|
||||
<<optional>> May be omitted
|
||||
}
|
||||
|
||||
BitchatPacket --> PacketHeader
|
||||
BitchatPacket --> PacketBody
|
||||
BitchatPacket --> PacketSignature
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Message Types
|
||||
|
||||
| Type | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| ANNOUNCE | 0x01 | Peer announcement with public key |
|
||||
| KEY_EXCHANGE | 0x02 | Key exchange messages |
|
||||
| LEAVE | 0x03 | Graceful disconnect |
|
||||
| MESSAGE | 0x04 | Chat messages (private/broadcast) |
|
||||
| FRAGMENT_START | 0x05 | Start of fragmented message |
|
||||
| FRAGMENT_CONTINUE | 0x06 | Continuation fragment |
|
||||
| FRAGMENT_END | 0x07 | Final fragment |
|
||||
| ROOM_ANNOUNCE | 0x08 | Room status announcement |
|
||||
| ROOM_RETENTION | 0x09 | Room retention policy |
|
||||
|
||||
## Privacy Features
|
||||
|
||||
bitchat implements several privacy-enhancing mechanisms.
|
||||
|
||||
### Cover Traffic
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Cover Traffic Timeline
|
||||
dateFormat X
|
||||
axisFormat %s
|
||||
|
||||
section Real Messages
|
||||
A to B :done, real1, 0, 1
|
||||
C to D :done, real2, 4, 1
|
||||
E to F :done, real3, 8, 1
|
||||
|
||||
section Cover Traffic
|
||||
A to C (dummy) :crit, cover1, 2, 1
|
||||
B to E (dummy) :crit, cover2, 6, 1
|
||||
D to A (dummy) :crit, cover3, 10, 1
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Cover traffic characteristics:
|
||||
- **Random intervals**: 30-120 seconds between dummy messages
|
||||
- **Realistic content**: Mimics actual user messages
|
||||
- **Marked internally**: Identified and discarded after decryption
|
||||
- **Battery aware**: Disabled when battery < 20%
|
||||
|
||||
### Timing Randomization
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "Without Randomization"
|
||||
U1[User Types] -->|0ms| T1[Transmit]
|
||||
U2[User Types] -->|0ms| T2[Transmit]
|
||||
U3[User Types] -->|0ms| T3[Transmit]
|
||||
end
|
||||
|
||||
subgraph "With Randomization"
|
||||
V1[User Types] -->|127ms| R1[Transmit]
|
||||
V2[User Types] -->|394ms| R2[Transmit]
|
||||
V3[User Types] -->|51ms| R3[Transmit]
|
||||
end
|
||||
|
||||
style U1 fill:#ffcdd2
|
||||
style U2 fill:#ffcdd2
|
||||
style U3 fill:#ffcdd2
|
||||
style V1 fill:#c8e6c9
|
||||
style V2 fill:#c8e6c9
|
||||
style V3 fill:#c8e6c9
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
This prevents timing analysis attacks by adding random delays (50-500ms) to all operations, making it impossible to correlate user actions with network traffic.
|
||||
|
||||
### Ephemeral Identities
|
||||
|
||||
- **No registration**: No account creation or phone numbers
|
||||
- **Random peer IDs**: Generated fresh each session
|
||||
- **Public key fingerprints**: Only persistent identifier for favorites
|
||||
- **Nickname-based**: Human-readable names without permanent binding
|
||||
|
||||
## Message Fragmentation
|
||||
|
||||
Large messages are automatically fragmented for reliable transmission over BLE.
|
||||
|
||||
### Fragmentation Flow
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
O[Original Message<br/>10KB] --> F[Fragment Handler]
|
||||
|
||||
F --> F1[Fragment 1<br/>START<br/>500 bytes]
|
||||
F --> F2[Fragment 2<br/>CONTINUE<br/>500 bytes]
|
||||
F --> F3[Fragment 3<br/>CONTINUE<br/>500 bytes]
|
||||
F --> FN[Fragment N<br/>END<br/>≤500 bytes]
|
||||
|
||||
F1 -->|20ms delay| T1[Transmit]
|
||||
F2 -->|20ms delay| T2[Transmit]
|
||||
F3 -->|20ms delay| T3[Transmit]
|
||||
FN -->|20ms delay| TN[Transmit]
|
||||
|
||||
T1 --> R[Reassembly Buffer]
|
||||
T2 --> R
|
||||
T3 --> R
|
||||
TN --> R
|
||||
|
||||
R --> M[Complete Message<br/>10KB]
|
||||
|
||||
style O fill:#bbdefb
|
||||
style M fill:#c8e6c9
|
||||
style F fill:#fff3e0
|
||||
style R fill:#f8bbd0
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Fragment Structure
|
||||
|
||||
- **Fragment ID**: 8-byte identifier linking fragments
|
||||
- **Sequence tracking**: START, CONTINUE, END types
|
||||
- **Reliability**: Each fragment independently relayed
|
||||
- **Optimization**: 20ms inter-fragment delay for BLE 5.0
|
||||
|
||||
## Complete Message Flow
|
||||
|
||||
To illustrate how all components work together, here's the complete flow of a message through the bitchat system:
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User Interface
|
||||
participant E as Encryption Service
|
||||
participant F as Fragment Handler
|
||||
participant B as BLE Transport
|
||||
participant M as Mesh Router
|
||||
participant S as Store & Forward
|
||||
participant R as Remote Peer
|
||||
|
||||
U->>E: User sends message
|
||||
Note over E: Generate random delay<br/>(50-500ms)
|
||||
|
||||
alt Private Message
|
||||
E->>E: Encrypt with X25519<br/>shared secret
|
||||
else Room Message
|
||||
E->>E: Encrypt with Argon2id<br/>derived key
|
||||
else Broadcast
|
||||
E->>E: Sign with Ed25519
|
||||
end
|
||||
|
||||
E->>F: Encrypted payload
|
||||
|
||||
alt Message > 500 bytes
|
||||
F->>F: Fragment into chunks
|
||||
loop Each fragment
|
||||
F->>B: Send fragment
|
||||
Note over B: 20ms inter-fragment delay
|
||||
end
|
||||
else Message ≤ 500 bytes
|
||||
F->>B: Send complete message
|
||||
end
|
||||
|
||||
B->>M: Transmit packet (TTL=7)
|
||||
|
||||
M->>M: Check recipient
|
||||
alt Recipient online
|
||||
M->>R: Direct delivery
|
||||
else Recipient offline
|
||||
M->>S: Cache message
|
||||
Note over S: Retain 12hrs (regular)<br/>or indefinite (favorite)
|
||||
end
|
||||
|
||||
alt TTL > 0
|
||||
M->>M: Decrement TTL
|
||||
M->>B: Relay to other peers
|
||||
end
|
||||
|
||||
Note over R: When peer comes online
|
||||
S->>R: Deliver cached messages
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Future Considerations: Network Bridge Extension
|
||||
|
||||
While bitchat is designed to operate without internet infrastructure, there are scenarios where selective network bridging could enhance its capabilities without compromising its core principles. The Nostr protocol presents a particularly interesting integration opportunity.
|
||||
|
||||
### Nostr as a Bridge Protocol
|
||||
|
||||
<div align="center">
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Local Mesh Network"
|
||||
L1[Peer A] -.->|BLE| L2[Peer B]
|
||||
L2 -.->|BLE| L3[Peer C]
|
||||
L3 -.->|BLE| GW[Gateway Peer]
|
||||
end
|
||||
|
||||
subgraph "Internet Bridge"
|
||||
GW ==>|Optional| NR[Nostr Relay]
|
||||
end
|
||||
|
||||
subgraph "Remote Mesh Network"
|
||||
NR ==>|Optional| GW2[Gateway Peer]
|
||||
GW2 -.->|BLE| R1[Peer D]
|
||||
GW2 -.->|BLE| R2[Peer E]
|
||||
R1 -.->|BLE| R3[Peer F]
|
||||
end
|
||||
|
||||
style GW fill:#ffeb3b
|
||||
style GW2 fill:#ffeb3b
|
||||
style NR fill:#9c27b0,color:#fff
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### Integration Benefits
|
||||
|
||||
**1. Geographic Bridge**: Connect isolated mesh networks across distances while maintaining local peer-to-peer operation.
|
||||
|
||||
**2. Asynchronous Delivery**: Nostr's event-based model aligns well with bitchat's store-and-forward mechanism, enabling message delivery across time zones and sporadic connectivity.
|
||||
|
||||
**3. Selective Sharing**: Users could opt-in to share specific rooms or conversations beyond the local mesh, maintaining privacy by default.
|
||||
|
||||
**4. Decentralized Architecture**: Nostr's relay model preserves bitchat's decentralization principles - no single point of failure or control.
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as Mesh Network
|
||||
participant G as Gateway Service
|
||||
participant N as Nostr Client
|
||||
participant R as Nostr Relay
|
||||
|
||||
M->>G: Message for remote delivery
|
||||
G->>G: Check opt-in status
|
||||
|
||||
alt Room allows bridging
|
||||
G->>N: Convert to Nostr event
|
||||
Note over N: Add bitchat metadata<br/>Maintain encryption
|
||||
N->>R: Publish event
|
||||
R->>R: Store and relay
|
||||
else Local only
|
||||
G->>M: Keep within mesh
|
||||
end
|
||||
|
||||
R->>N: New bitchat event
|
||||
N->>G: Convert to bitchat message
|
||||
G->>M: Inject into local mesh
|
||||
```
|
||||
|
||||
### Privacy Preservation
|
||||
|
||||
Key considerations for maintaining bitchat's privacy model:
|
||||
|
||||
1. **Opt-in Only**: Network bridging disabled by default, requiring explicit user consent
|
||||
2. **Room-Level Control**: Bridge permissions managed per room, not globally
|
||||
3. **Maintained Encryption**: Messages remain end-to-end encrypted when bridged
|
||||
4. **Ephemeral Options**: Support for Nostr's ephemeral events (NIP-16) for temporary bridging
|
||||
5. **Identity Isolation**: Generate separate Nostr keypairs unlinked to local peer identities
|
||||
|
||||
### Use Cases
|
||||
|
||||
- **Disaster Coordination**: Bridge local emergency mesh networks to coordinate broader relief efforts
|
||||
- **Event Overflow**: Extend large gatherings beyond Bluetooth range while maintaining local clusters
|
||||
- **Checkpoint Sync**: Periodically sync specific rooms when internet is briefly available
|
||||
- **Cross-Community Bridges**: Connect related but geographically separated communities
|
||||
|
||||
This extension would be implemented as an optional module, ensuring the core bitchat system remains fully functional without any network dependencies. Users in pure offline environments would see no change, while those with selective connectivity could benefit from enhanced reach when desired.
|
||||
|
||||
## Conclusion
|
||||
|
||||
bitchat demonstrates that secure, private messaging is possible without centralized infrastructure. By combining Bluetooth mesh networking, end-to-end encryption, and privacy-preserving protocols, bitchat provides resilient communication that works anywhere people gather, regardless of internet availability.
|
||||
|
||||
The system's design prioritizes:
|
||||
- **User privacy**: No persistent identifiers or metadata collection
|
||||
- **Resilience**: Automatic mesh networking and store-and-forward
|
||||
- **Security**: Strong encryption with forward secrecy
|
||||
- **Efficiency**: Binary protocols and intelligent caching
|
||||
- **Simplicity**: No account creation or complex setup
|
||||
|
||||
As a public domain project, bitchat serves as both a practical tool and a reference implementation for decentralized, privacy-preserving communication systems.
|
||||
|
||||
---
|
||||
|
||||
*This document is released into the public domain under The Unlicense.*
|
||||
@@ -73,6 +73,7 @@ enum MessageType: UInt8 {
|
||||
case fragmentContinue = 0x06
|
||||
case fragmentEnd = 0x07
|
||||
case roomAnnounce = 0x08 // Announce password-protected room status
|
||||
case roomRetention = 0x09 // Announce room retention status
|
||||
}
|
||||
|
||||
// Special recipient ID for broadcast messages
|
||||
@@ -165,6 +166,7 @@ protocol BitchatDelegate: AnyObject {
|
||||
func didUpdatePeerList(_ peers: [String])
|
||||
func didReceiveRoomLeave(_ room: String, from peerID: String)
|
||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?, keyCommitment: String?)
|
||||
func didReceiveRoomRetentionAnnouncement(_ room: String, enabled: Bool, creatorID: String?)
|
||||
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String?
|
||||
|
||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||
@@ -185,6 +187,10 @@ extension BitchatDelegate {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveRoomRetentionAnnouncement(_ room: String, enabled: Bool, creatorID: String?) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String? {
|
||||
// Default returns nil (unable to decrypt)
|
||||
return nil
|
||||
|
||||
@@ -710,6 +710,29 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
func sendRoomRetentionAnnouncement(_ room: String, enabled: Bool) {
|
||||
messageQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Payload format: room|enabled|creatorID
|
||||
let enabledFlag = enabled ? "1" : "0"
|
||||
let payload = "\(room)|\(enabledFlag)|\(self.myPeerID)"
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.roomRetention.rawValue,
|
||||
senderID: Data(self.myPeerID.utf8),
|
||||
recipientID: SpecialRecipients.broadcast,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: Data(payload.utf8),
|
||||
signature: nil,
|
||||
ttl: 5 // Allow wider propagation for room announcements
|
||||
)
|
||||
|
||||
bitchatLog("Announcing room \(room) retention status: \(enabled)", category: "room")
|
||||
self.broadcastPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
func sendEncryptedRoomMessage(_ content: String, mentions: [String], room: String, roomKey: SymmetricKey) {
|
||||
messageQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
@@ -1835,6 +1858,30 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
case .roomRetention:
|
||||
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
|
||||
// Parse payload: room|enabled|creatorID
|
||||
let components = payloadStr.split(separator: "|").map(String.init)
|
||||
if components.count >= 3 {
|
||||
let room = components[0]
|
||||
let enabled = components[1] == "1"
|
||||
let creatorID = components[2]
|
||||
|
||||
bitchatLog("Received room retention announcement: \(room) retention \(enabled ? "enabled" : "disabled") by \(creatorID)", category: "room")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.didReceiveRoomRetentionAnnouncement(room, enabled: enabled, creatorID: creatorID)
|
||||
}
|
||||
|
||||
// Relay announcement
|
||||
if packet.ttl > 1 {
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl -= 1
|
||||
self.broadcastPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class ChatViewModel: ObservableObject {
|
||||
@Published var showPasswordPrompt: Bool = false
|
||||
@Published var passwordPromptRoom: String? = nil
|
||||
@Published var savedRooms: Set<String> = [] // Rooms saved for message retention
|
||||
@Published var retentionEnabledRooms: Set<String> = [] // Rooms where owner enabled retention for all members
|
||||
|
||||
let meshService = BluetoothMeshService()
|
||||
private let userDefaults = UserDefaults.standard
|
||||
@@ -59,6 +60,7 @@ class ChatViewModel: ObservableObject {
|
||||
private let roomCreatorsKey = "bitchat.roomCreators"
|
||||
// private let roomPasswordsKey = "bitchat.roomPasswords" // Now using Keychain
|
||||
private let roomKeyCommitmentsKey = "bitchat.roomKeyCommitments"
|
||||
private let retentionEnabledRoomsKey = "bitchat.retentionEnabledRooms"
|
||||
private var nicknameSaveTimer: Timer?
|
||||
|
||||
@Published var favoritePeers: Set<String> = [] // Now stores public key fingerprints instead of peer IDs
|
||||
@@ -129,8 +131,8 @@ class ChatViewModel: ObservableObject {
|
||||
roomMembers[room] = Set()
|
||||
}
|
||||
|
||||
// Load saved messages if this is a saved room
|
||||
if MessageRetentionService.shared.getFavoriteRooms().contains(room) {
|
||||
// Load saved messages if this room has retention enabled
|
||||
if retentionEnabledRooms.contains(room) {
|
||||
let savedMessages = MessageRetentionService.shared.loadMessagesForRoom(room)
|
||||
if !savedMessages.isEmpty {
|
||||
roomMessages[room] = savedMessages
|
||||
@@ -162,6 +164,11 @@ class ChatViewModel: ObservableObject {
|
||||
roomKeyCommitments = savedCommitments
|
||||
}
|
||||
|
||||
// Load retention-enabled rooms
|
||||
if let savedRetentionRooms = userDefaults.stringArray(forKey: retentionEnabledRoomsKey) {
|
||||
retentionEnabledRooms = Set(savedRetentionRooms)
|
||||
}
|
||||
|
||||
// Load room passwords from Keychain
|
||||
let savedPasswords = KeychainManager.shared.getAllRoomPasswords()
|
||||
roomPasswords = savedPasswords
|
||||
@@ -179,6 +186,7 @@ class ChatViewModel: ObservableObject {
|
||||
_ = KeychainManager.shared.saveRoomPassword(password, for: room)
|
||||
}
|
||||
userDefaults.set(roomKeyCommitments, forKey: roomKeyCommitmentsKey)
|
||||
userDefaults.set(Array(retentionEnabledRooms), forKey: retentionEnabledRoomsKey)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
@@ -830,8 +838,8 @@ class ChatViewModel: ObservableObject {
|
||||
}
|
||||
roomMessages[room]?.append(message)
|
||||
|
||||
// Save message if room is a favorite
|
||||
if MessageRetentionService.shared.getFavoriteRooms().contains(room) {
|
||||
// Save message if room has retention enabled
|
||||
if retentionEnabledRooms.contains(room) {
|
||||
MessageRetentionService.shared.saveMessage(message, forRoom: room)
|
||||
}
|
||||
|
||||
@@ -943,6 +951,7 @@ class ChatViewModel: ObservableObject {
|
||||
// Clear all retained messages
|
||||
MessageRetentionService.shared.deleteAllStoredMessages()
|
||||
savedRooms.removeAll()
|
||||
retentionEnabledRooms.removeAll()
|
||||
|
||||
// Clear message retry queue
|
||||
MessageRetryService.shared.clearRetryQueue()
|
||||
@@ -952,6 +961,7 @@ class ChatViewModel: ObservableObject {
|
||||
userDefaults.removeObject(forKey: passwordProtectedRoomsKey)
|
||||
userDefaults.removeObject(forKey: roomCreatorsKey)
|
||||
userDefaults.removeObject(forKey: roomKeyCommitmentsKey)
|
||||
userDefaults.removeObject(forKey: retentionEnabledRoomsKey)
|
||||
|
||||
// Reset nickname to anonymous
|
||||
nickname = "anon\(Int.random(in: 1000...9999))"
|
||||
@@ -1342,6 +1352,74 @@ extension ChatViewModel: BitchatDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func didReceiveRoomRetentionAnnouncement(_ room: String, enabled: Bool, creatorID: String?) {
|
||||
bitchatLog("Received retention announcement for room \(room): \(enabled ? "enabled" : "disabled") by \(creatorID ?? "unknown")", category: "room")
|
||||
|
||||
// Only process if we're a member of this room
|
||||
guard joinedRooms.contains(room) else { return }
|
||||
|
||||
// Verify the announcement is from the room owner
|
||||
if let creatorID = creatorID, roomCreators[room] != creatorID {
|
||||
bitchatLog("Ignoring retention announcement from non-owner \(creatorID) for room \(room)", category: "room")
|
||||
return
|
||||
}
|
||||
|
||||
// Update retention status
|
||||
if enabled {
|
||||
retentionEnabledRooms.insert(room)
|
||||
savedRooms.insert(room)
|
||||
// Ensure room is in favorites if not already
|
||||
if !MessageRetentionService.shared.getFavoriteRooms().contains(room) {
|
||||
_ = MessageRetentionService.shared.toggleFavoriteRoom(room)
|
||||
}
|
||||
|
||||
// Show system message
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "room owner enabled message retention for \(room). all messages will be saved locally.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
if currentRoom == room {
|
||||
messages.append(systemMessage)
|
||||
} else if var roomMsgs = roomMessages[room] {
|
||||
roomMsgs.append(systemMessage)
|
||||
roomMessages[room] = roomMsgs
|
||||
} else {
|
||||
roomMessages[room] = [systemMessage]
|
||||
}
|
||||
} else {
|
||||
retentionEnabledRooms.remove(room)
|
||||
savedRooms.remove(room)
|
||||
|
||||
// Delete all saved messages for this room
|
||||
MessageRetentionService.shared.deleteMessagesForRoom(room)
|
||||
// Remove from favorites if currently set
|
||||
if MessageRetentionService.shared.getFavoriteRooms().contains(room) {
|
||||
_ = MessageRetentionService.shared.toggleFavoriteRoom(room)
|
||||
}
|
||||
|
||||
// Show system message
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "room owner disabled message retention for \(room). all saved messages have been deleted.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
if currentRoom == room {
|
||||
messages.append(systemMessage)
|
||||
} else if var roomMsgs = roomMessages[room] {
|
||||
roomMsgs.append(systemMessage)
|
||||
roomMessages[room] = roomMsgs
|
||||
} else {
|
||||
roomMessages[room] = [systemMessage]
|
||||
}
|
||||
}
|
||||
|
||||
// Persist retention status
|
||||
userDefaults.set(Array(retentionEnabledRooms), forKey: retentionEnabledRoomsKey)
|
||||
}
|
||||
|
||||
private func handleCommand(_ command: String) {
|
||||
let parts = command.split(separator: " ")
|
||||
guard let cmd = parts.first else { return }
|
||||
@@ -1402,7 +1480,7 @@ extension ChatViewModel: BitchatDelegate {
|
||||
// Show usage hint
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "usage: /join #roomname",
|
||||
content: "usage: /j #roomname",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
@@ -1452,7 +1530,7 @@ extension ChatViewModel: BitchatDelegate {
|
||||
} else {
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "usage: /msg @nickname [message] or /msg nickname [message]",
|
||||
content: "usage: /m @nickname [message] or /m nickname [message]",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
@@ -1497,6 +1575,9 @@ extension ChatViewModel: BitchatDelegate {
|
||||
if passwordProtectedRooms.contains(room) {
|
||||
status += " 🔒"
|
||||
}
|
||||
if retentionEnabledRooms.contains(room) {
|
||||
status += " 📌"
|
||||
}
|
||||
if roomCreators[room] == meshService.myPeerID {
|
||||
status += " (owner)"
|
||||
}
|
||||
@@ -1505,7 +1586,7 @@ extension ChatViewModel: BitchatDelegate {
|
||||
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "discovered rooms:\n\(roomList)\n\n✓ = joined",
|
||||
content: "discovered rooms:\n\(roomList)\n\n✓ = joined, 🔒 = password protected, 📌 = retention enabled",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
@@ -1588,11 +1669,11 @@ extension ChatViewModel: BitchatDelegate {
|
||||
bitchatLog("cleared main chat", category: "chat")
|
||||
}
|
||||
case "/save":
|
||||
// Toggle save status for current room
|
||||
// Toggle retention for current room (owner only)
|
||||
guard let room = currentRoom else {
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "you must be in a room to save it.",
|
||||
content: "you must be in a room to toggle message retention.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
@@ -1600,18 +1681,39 @@ extension ChatViewModel: BitchatDelegate {
|
||||
break
|
||||
}
|
||||
|
||||
let isFavorite = MessageRetentionService.shared.toggleFavoriteRoom(room)
|
||||
let status = isFavorite ? "saved" : "unsaved"
|
||||
|
||||
// Update published property
|
||||
if isFavorite {
|
||||
savedRooms.insert(room)
|
||||
} else {
|
||||
savedRooms.remove(room)
|
||||
// Check if user is the room owner
|
||||
guard roomCreators[room] == meshService.myPeerID else {
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "only the room owner can toggle message retention.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
break
|
||||
}
|
||||
|
||||
// If just marked as favorite, load any previously saved messages
|
||||
if isFavorite {
|
||||
// Toggle retention status
|
||||
let isEnabling = !retentionEnabledRooms.contains(room)
|
||||
|
||||
if isEnabling {
|
||||
// Enable retention for this room
|
||||
retentionEnabledRooms.insert(room)
|
||||
savedRooms.insert(room)
|
||||
_ = MessageRetentionService.shared.toggleFavoriteRoom(room) // Enable if not already
|
||||
|
||||
// Announce to all members that retention is enabled
|
||||
meshService.sendRoomRetentionAnnouncement(room, enabled: true)
|
||||
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "message retention enabled for room \(room). all members will save messages locally.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
|
||||
// Load any previously saved messages
|
||||
let savedMessages = MessageRetentionService.shared.loadMessagesForRoom(room)
|
||||
if !savedMessages.isEmpty {
|
||||
// Merge saved messages with current messages, avoiding duplicates
|
||||
@@ -1627,32 +1729,30 @@ extension ChatViewModel: BitchatDelegate {
|
||||
}
|
||||
// Sort by timestamp
|
||||
roomMessages[room]?.sort { $0.timestamp < $1.timestamp }
|
||||
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "room \(room) \(status). loaded \(savedMessages.count) saved messages.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
} else {
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "room \(room) \(status).",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
}
|
||||
} else {
|
||||
// Disable retention for this room
|
||||
retentionEnabledRooms.remove(room)
|
||||
savedRooms.remove(room)
|
||||
|
||||
// Delete all saved messages for this room
|
||||
MessageRetentionService.shared.deleteMessagesForRoom(room)
|
||||
_ = MessageRetentionService.shared.toggleFavoriteRoom(room) // Disable if enabled
|
||||
|
||||
// Announce to all members that retention is disabled
|
||||
meshService.sendRoomRetentionAnnouncement(room, enabled: false)
|
||||
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "room \(room) \(status).",
|
||||
content: "message retention disabled for room \(room). all saved messages will be deleted on all devices.",
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
}
|
||||
|
||||
// Save the updated room data
|
||||
saveRoomData()
|
||||
default:
|
||||
// Unknown command
|
||||
let systemMessage = BitchatMessage(
|
||||
@@ -1840,8 +1940,8 @@ extension ChatViewModel: BitchatDelegate {
|
||||
roomMessages[room]?.append(messageToAdd)
|
||||
roomMessages[room]?.sort { $0.timestamp < $1.timestamp }
|
||||
|
||||
// Save message if room is a favorite
|
||||
if MessageRetentionService.shared.getFavoriteRooms().contains(room) {
|
||||
// Save message if room has retention enabled
|
||||
if retentionEnabledRooms.contains(room) {
|
||||
MessageRetentionService.shared.saveMessage(messageToAdd, forRoom: room)
|
||||
}
|
||||
|
||||
|
||||
@@ -296,15 +296,26 @@ struct ContentView: View {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
// Save button
|
||||
Button(action: {
|
||||
viewModel.sendMessage("/save")
|
||||
}) {
|
||||
Image(systemName: viewModel.savedRooms.contains(currentRoom) ? "bookmark.fill" : "bookmark")
|
||||
// Show retention indicator for all users
|
||||
if viewModel.retentionEnabledRooms.contains(currentRoom) {
|
||||
Image(systemName: "bookmark.fill")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(viewModel.savedRooms.contains(currentRoom) ? Color.yellow : textColor)
|
||||
.foregroundColor(Color.yellow)
|
||||
.help("Messages in this room are being saved locally")
|
||||
}
|
||||
|
||||
// Save button - only for room owner
|
||||
if viewModel.roomCreators[currentRoom] == viewModel.meshService.myPeerID {
|
||||
Button(action: {
|
||||
viewModel.sendMessage("/save")
|
||||
}) {
|
||||
Image(systemName: viewModel.retentionEnabledRooms.contains(currentRoom) ? "bookmark.slash" : "bookmark")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help(viewModel.retentionEnabledRooms.contains(currentRoom) ? "Disable message retention" : "Enable message retention")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Password button for room creator only
|
||||
if viewModel.roomCreators[currentRoom] == viewModel.meshService.myPeerID {
|
||||
@@ -442,10 +453,10 @@ struct ContentView: View {
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
// Regular messages with tappable sender name
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
||||
// Timestamp
|
||||
Text("[\(viewModel.formatTimestamp(message.timestamp))] ")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.textSelection(.enabled)
|
||||
|
||||
@@ -457,15 +468,15 @@ struct ContentView: View {
|
||||
}
|
||||
}) {
|
||||
let senderColor = viewModel.getSenderColor(for: message, colorScheme: colorScheme)
|
||||
Text("<\(message.sender)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
Text("<@\(message.sender)>")
|
||||
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(senderColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
// Own messages not tappable
|
||||
Text("<\(message.sender)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
Text("<@\(message.sender)>")
|
||||
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -572,21 +583,21 @@ struct ContentView: View {
|
||||
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
if viewModel.selectedPrivateChatPeer != nil {
|
||||
Text("<\(viewModel.nickname)> →")
|
||||
Text("<@\(viewModel.nickname)> →")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(Color.orange)
|
||||
.lineLimit(1)
|
||||
.fixedSize()
|
||||
.padding(.leading, 12)
|
||||
} else if let currentRoom = viewModel.currentRoom, viewModel.passwordProtectedRooms.contains(currentRoom) {
|
||||
Text("<\(viewModel.nickname)> →")
|
||||
Text("<@\(viewModel.nickname)> →")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(Color.orange)
|
||||
.lineLimit(1)
|
||||
.fixedSize()
|
||||
.padding(.leading, 12)
|
||||
} else {
|
||||
Text("<\(viewModel.nickname)>")
|
||||
Text("<@\(viewModel.nickname)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
|
||||
Reference in New Issue
Block a user