Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This project is released into the public domain. See the [LICENSE](LICENSE) file
- **Location-Based Channels**: Geographic chat rooms using geohash coordinates over global Nostr relays
- **Intelligent Message Routing**: Automatically chooses best transport (Bluetooth → Nostr fallback)
- **Decentralized Mesh Network**: Automatic peer discovery and multi-hop message relay over Bluetooth LE
- **Open Source AI Integration**: Access to LLM inference via [Morpheus AI Gateway](https://apidocs.mor.org) - chat with AI even offline through mesh bridges
- **Privacy First**: No accounts, no phone numbers, no persistent identifiers
- **Private Message End-to-End Encryption**: [Noise Protocol](https://noiseprotocol.org) for mesh, NIP-17 for Nostr
- **IRC-Style Commands**: Familiar `/slap`, `/msg`, `/who` style interface
Expand Down Expand Up @@ -87,6 +88,30 @@ Private messages use **intelligent transport selection**:
- Messages queued until transport becomes available
- Automatic delivery when connection established

### MorpheusAI Integration

BitChat integrates with the [Morpheus decentralized AI network](https://apidocs.mor.org), bringing open-source LLM inference to the mesh:

**How it works:**
- A bridge device with internet runs the MorpheusAI bot
- Users on the mesh (even offline) can query AI through public chat or encrypted DMs
- Responses route back through the Bluetooth mesh network

**Usage for regular users (no setup required):**

| Method | Command | Privacy |
|--------|---------|---------|
| Public AI | `@MorpheusAI What is Bitcoin?` | Visible to all in mesh |
| Private AI | `/msg @BridgeNick !ai What is Bitcoin?` | End-to-end encrypted |

**Bridge operator setup:**
```bash
/ai-key YOUR_MORPHEUS_API_KEY # Get key at https://app.mor.org
/ai-bridge on # Activate the bot
```

For detailed documentation, see [MorpheusAI FAQ](docs/MORPHEUS_AI_FAQ.md).

For detailed protocol documentation, see the [Technical Whitepaper](WHITEPAPER.md).

## Setup
Expand Down
2 changes: 1 addition & 1 deletion bitchat.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions bitchat/Protocols/BitchatProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,18 @@ import CoreBluetooth
enum MessageType: UInt8 {
// Public messages (unencrypted)
case announce = 0x01 // "I'm here" with nickname
case message = 0x02 // Public chat message
case message = 0x02 // Public chat message
case leave = 0x03 // "I'm leaving"
case requestSync = 0x21 // GCS filter-based sync request (local-only)

// Noise encryption
case noiseHandshake = 0x10 // Handshake (init or response determined by payload)
case noiseEncrypted = 0x11 // All encrypted payloads (messages, receipts, etc.)

// Fragmentation (simplified)
case fragment = 0x20 // Single fragment type for large messages
case fileTransfer = 0x22 // Binary file/audio/image payloads

var description: String {
switch self {
case .announce: return "announce"
Expand Down Expand Up @@ -108,7 +108,7 @@ enum NoisePayloadType: UInt8 {
// Verification (QR-based OOB binding)
case verifyChallenge = 0x10 // Verification challenge
case verifyResponse = 0x11 // Verification response

var description: String {
switch self {
case .privateMessage: return "privateMessage"
Expand Down
5 changes: 3 additions & 2 deletions bitchat/Services/AutocompleteService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ final class AutocompleteService {
private let commands = [
"/msg", "/who", "/clear",
"/hug", "/slap", "/fav", "/unfav",
"/block", "/unblock"
"/block", "/unblock",
"/ai", "/ai-key", "/ai-model", "/ai-bridge", "/ai-clear", "/ai-help"
]

/// Get autocomplete suggestions for current text
Expand Down Expand Up @@ -95,7 +96,7 @@ final class AutocompleteService {

private func needsArgument(command: String) -> Bool {
switch command {
case "/who", "/clear":
case "/who", "/clear", "/ai-clear", "/ai-help":
return false
default:
return true
Expand Down
12 changes: 6 additions & 6 deletions bitchat/Services/BLE/BLEService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1328,15 +1328,15 @@ final class BLEService: NSObject {
// Send on main thread
notifyUI { [weak self] in
guard let self = self else { return }

// Get current peer list (after removal)
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }

self.delegate?.didDisconnectFromPeer(peerID)
self.delegate?.didUpdatePeerList(currentPeerIDs)
}
}

// MARK: - Helper Functions

private func applicationFilesDirectory() throws -> URL {
Expand Down Expand Up @@ -3783,7 +3783,7 @@ extension BLEService {

case .leave:
handleLeave(packet, from: senderID)

case .none:
SecureLogger.warning("⚠️ Unknown message type: \(packet.type)", category: .session)
break
Expand Down Expand Up @@ -4163,12 +4163,12 @@ extension BLEService {

private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: PeerID) {
SecureLogger.debug("🔐 handleNoiseEncrypted called for packet from \(peerID)")

guard let recipientID = PeerID(hexData: packet.recipientID) else {
SecureLogger.warning("⚠️ Encrypted message has no recipient ID", category: .session)
return
}

if recipientID != myPeerID {
SecureLogger.debug("🔐 Encrypted message not for me (for \(recipientID), I am \(myPeerID))", category: .session)
return
Expand Down
139 changes: 138 additions & 1 deletion bitchat/Services/CommandProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum CommandResult {
case success(message: String?)
case error(message: String)
case handled // Command handled, no message needed
case asyncPending // Command is being processed asynchronously
}

/// Simple struct for geo participant info used by CommandProcessor
Expand Down Expand Up @@ -50,6 +51,9 @@ protocol CommandContextProvider: AnyObject {
// MARK: - Favorites
func toggleFavorite(peerID: PeerID)
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool)

// MARK: - Morpheus AI
func getCurrentGeohash() -> String?
}

/// Processes chat commands in a focused, efficient way
Expand Down Expand Up @@ -102,6 +106,18 @@ final class CommandProcessor {
case "/unfav":
if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") }
return handleFavorite(args, add: false)
case "/ai":
return handleAI(args)
case "/ai-clear":
return handleAIClear()
case "/ai-model":
return handleAIModel(args)
case "/ai-key":
return handleAIKey(args)
case "/ai-bridge":
return handleAIBridge(args)
case "/ai-help":
return handleAIHelp()
default:
return .error(message: "unknown command: \(cmd)")
}
Expand Down Expand Up @@ -344,5 +360,126 @@ final class CommandProcessor {
return .success(message: "removed \(nickname) from favorites")
}
}


// MARK: - Morpheus AI Commands

private func handleAI(_ args: String) -> CommandResult {
let message = args.trimmingCharacters(in: .whitespaces)
guard !message.isEmpty else {
return .error(message: "usage: /ai <message> - chat with MorpheusAI (or use /msg @MorpheusAI)")
}

// Check location restriction
guard let geohash = contextProvider?.getCurrentGeohash(), !geohash.isEmpty else {
return .error(message: "location required for MorpheusChat - enable location services")
}

guard MorpheusAIService.shared.isLocationAllowed(geohash: geohash) else {
return .error(message: "MorpheusChat is not available in your region")
}

// Check API key (direct command requires local API key)
guard MorpheusAIService.shared.hasAPIKey else {
return .error(message: "API key not configured. Use /ai-key <key> or /msg @MorpheusAI to use a bridge")
}

// Show thinking indicator
contextProvider?.addPublicSystemMessage("MorpheusAI is thinking...")

// Process async - direct API call
Task { @MainActor in
do {
let response = try await MorpheusAIService.shared.sendMessage(message, geohash: geohash)
let formattedResponse = "MorpheusAI: \(response)"
contextProvider?.addPublicSystemMessage(formattedResponse)
} catch let error as MorpheusAIError {
contextProvider?.addPublicSystemMessage("MorpheusAI error: \(error.localizedDescription)")
} catch {
contextProvider?.addPublicSystemMessage("MorpheusAI error: \(error.localizedDescription)")
}
}

return .asyncPending
}

private func handleAIClear() -> CommandResult {
MorpheusAIService.shared.clearHistory()
return .success(message: "MorpheusAI conversation history cleared")
}

private func handleAIModel(_ args: String) -> CommandResult {
let model = args.trimmingCharacters(in: .whitespaces)

if model.isEmpty {
// Show current model
return .success(message: "current MorpheusAI model: \(MorpheusAIConfig.defaultModel)")
}

// Set new model
MorpheusAIService.shared.setModel(model)
return .success(message: "MorpheusAI model set to: \(model)")
}

private func handleAIKey(_ args: String) -> CommandResult {
let key = args.trimmingCharacters(in: .whitespaces)

if key.isEmpty {
// Show status (not the actual key for security)
let status = MorpheusAIService.shared.hasAPIKey ? "configured" : "not configured"
return .success(message: "MorpheusAI API key: \(status). Get your key at app.mor.org")
}

// Set new API key
MorpheusAIService.shared.setAPIKey(key)
return .success(message: "MorpheusAI API key saved securely")
}

private func handleAIBridge(_ args: String) -> CommandResult {
let arg = args.trimmingCharacters(in: .whitespaces).lowercased()

if arg.isEmpty {
// Show bot status
return .success(message: MorpheusVirtualBot.shared.statusInfo)
}

switch arg {
case "on", "enable":
// Activate the virtual bot
let result = MorpheusVirtualBot.shared.activate()
switch result {
case .success(let message):
return .success(message: "\(message)")
case .failure(let error):
return .error(message: error.localizedDescription)
}
case "off", "disable":
MorpheusVirtualBot.shared.deactivate()
return .success(message: "MorpheusAI bot deactivated")
default:
return .error(message: "usage: /ai-bridge [on|off] - activate/deactivate MorpheusAI bot")
}
}

private func handleAIHelp() -> CommandResult {
let help = """
MorpheusAI Commands:

FOR ALL USERS (no setup needed):
/msg @MorpheusAI <message> - chat with AI via mesh bridge
/who - check if MorpheusAI is online

FOR BRIDGE OPERATORS (requires API key):
/ai-key <key> - set API key (get one at https://app.mor.org)
/ai-bridge on - activate MorpheusAI bot for mesh
/ai-bridge off - deactivate bot
/ai-model [name] - view/set AI model (default: llama-3.3-70b)
/ai <message> - direct AI query (requires local API key)
/ai-clear - clear conversation history
/ai-help - show this help

Available in: US, Bulgaria, Iran
"""
return .success(message: help)
}

}
Loading
Loading