use super::RpcHandler; use crate::mesh; use crate::mesh::message_types::{ self, AlertPayload, AlertType, Coordinate, InvoicePayload, MeshMessageType, TypedEnvelope, }; use anyhow::Result; use tracing::info; impl RpcHandler { /// mesh.status — Get mesh radio status, device info, and peer count. pub(super) async fn handle_mesh_status(&self) -> Result { let service = self.mesh_service.read().await; if let Some(svc) = service.as_ref() { let status = svc.status().await; Ok(serde_json::to_value(status)?) } else { // No service running — return basic config + device detection let config = mesh::load_config(&self.config.data_dir).await?; let devices = mesh::detect_devices().await; Ok(serde_json::json!({ "enabled": config.enabled, "device_connected": false, "device_type": "unknown", "device_path": config.device_path, "channel_name": config.channel_name.unwrap_or_else(|| "archipelago".to_string()), "detected_devices": devices, "peer_count": 0, "messages_sent": 0, "messages_received": 0, })) } } /// mesh.peers — List discovered mesh peers. pub(super) async fn handle_mesh_peers(&self) -> Result { let service = self.mesh_service.read().await; if let Some(svc) = service.as_ref() { let peers = svc.peers().await; Ok(serde_json::json!({ "peers": peers, "count": peers.len(), })) } else { Ok(serde_json::json!({ "peers": [], "count": 0, })) } } /// mesh.messages — Get recent mesh message history. pub(super) async fn handle_mesh_messages( &self, params: Option, ) -> Result { let limit = params .as_ref() .and_then(|p| p.get("limit")) .and_then(|v| v.as_u64()) .map(|n| n as usize); let service = self.mesh_service.read().await; if let Some(svc) = service.as_ref() { let messages = svc.messages(limit).await; Ok(serde_json::json!({ "messages": messages, "count": messages.len(), })) } else { Ok(serde_json::json!({ "messages": [], "count": 0, })) } } /// mesh.send — Send an encrypted message to a mesh peer. pub(super) async fn handle_mesh_send( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let contact_id = params .get("contact_id") .and_then(|v| v.as_u64()) .ok_or_else(|| anyhow::anyhow!("Missing contact_id"))? as u32; let message = params .get("message") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing message"))?; if message.is_empty() { anyhow::bail!("Message cannot be empty"); } let service = self.mesh_service.read().await; let svc = service .as_ref() .ok_or_else(|| anyhow::anyhow!("Mesh service not running. Enable mesh first."))?; let msg = svc.send_message(contact_id, message).await?; info!(contact_id, encrypted = msg.encrypted, "Sent mesh message"); Ok(serde_json::json!({ "sent": true, "message_id": msg.id, "encrypted": msg.encrypted, })) } /// mesh.broadcast — Broadcast our node identity over mesh. pub(super) async fn handle_mesh_broadcast(&self) -> Result { let service = self.mesh_service.read().await; let svc = service .as_ref() .ok_or_else(|| anyhow::anyhow!("Mesh service not running. Enable mesh first."))?; svc.broadcast_identity().await?; info!("Broadcast identity over mesh"); Ok(serde_json::json!({ "broadcast": true })) } /// mesh.configure — Enable/disable mesh and set device path. pub(super) async fn handle_mesh_configure( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let mut config = mesh::load_config(&self.config.data_dir).await?; if let Some(enabled) = params.get("enabled").and_then(|v| v.as_bool()) { config.enabled = enabled; } if let Some(device) = params.get("device_path").and_then(|v| v.as_str()) { config.device_path = Some(device.to_string()); } if let Some(channel) = params.get("channel_name").and_then(|v| v.as_str()) { config.channel_name = Some(channel.to_string()); } if let Some(broadcast) = params.get("broadcast_identity").and_then(|v| v.as_bool()) { config.broadcast_identity = broadcast; } if let Some(name) = params.get("advert_name").and_then(|v| v.as_str()) { config.advert_name = Some(name.to_string()); } mesh::save_config(&self.config.data_dir, &config).await?; // If we have a running service, update its config let mut service = self.mesh_service.write().await; if let Some(svc) = service.as_mut() { svc.configure(config.clone()).await?; } info!("Mesh config updated"); Ok(serde_json::json!({ "configured": true, "enabled": config.enabled, "device_path": config.device_path, })) } // ─── Phase 3: Typed Messages ──────────────────────────────────────── /// mesh.send-invoice — Create a Lightning invoice and send bolt11 to mesh peer. pub(super) async fn handle_mesh_send_invoice( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let contact_id = params["contact_id"] .as_u64() .ok_or_else(|| anyhow::anyhow!("Missing contact_id"))? as u32; let amount_sats = params["amount_sats"] .as_u64() .ok_or_else(|| anyhow::anyhow!("Missing amount_sats"))?; let memo = params["memo"].as_str().map(|s| s.to_string()); // Build invoice payload let invoice = InvoicePayload { bolt11: format!("lnbc{}n1pjmesh...", amount_sats), // Placeholder — real LND call in Phase 4 amount_sats, memo: memo.clone(), payment_hash: None, }; let payload = message_types::encode_payload(&invoice)?; let envelope = TypedEnvelope::new(MeshMessageType::Invoice, payload); let wire = envelope.to_wire()?; // Send via mesh let service = self.mesh_service.read().await; let svc = service .as_ref() .ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?; let wire_str = String::from_utf8_lossy(&wire).to_string(); let msg = svc.send_message(contact_id, &wire_str).await?; info!(contact_id, amount_sats, "Sent invoice over mesh"); Ok(serde_json::json!({ "sent": true, "message_id": msg.id, "amount_sats": amount_sats, "bolt11": invoice.bolt11, })) } /// mesh.send-coordinate — Send GPS coordinates to a mesh peer. pub(super) async fn handle_mesh_send_coordinate( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let contact_id = params["contact_id"] .as_u64() .ok_or_else(|| anyhow::anyhow!("Missing contact_id"))? as u32; let lat = params["lat"] .as_f64() .ok_or_else(|| anyhow::anyhow!("Missing lat"))?; let lng = params["lng"] .as_f64() .ok_or_else(|| anyhow::anyhow!("Missing lng"))?; let label = params["label"].as_str().map(|s| s.to_string()); let coord = Coordinate::from_degrees(lat, lng, label); let payload = message_types::encode_payload(&coord)?; let envelope = TypedEnvelope::new(MeshMessageType::Coordinate, payload); let wire = envelope.to_wire()?; let service = self.mesh_service.read().await; let svc = service .as_ref() .ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?; let wire_str = String::from_utf8_lossy(&wire).to_string(); let msg = svc.send_message(contact_id, &wire_str).await?; info!(contact_id, "Sent coordinate over mesh"); Ok(serde_json::json!({ "sent": true, "message_id": msg.id, "lat": coord.lat, "lng": coord.lng, })) } /// mesh.send-alert — Send a signed emergency alert over mesh. pub(super) async fn handle_mesh_send_alert( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let message = params["message"] .as_str() .ok_or_else(|| anyhow::anyhow!("Missing message"))?; let alert_type_str = params["alert_type"] .as_str() .unwrap_or("status"); let broadcast = params["broadcast"].as_bool().unwrap_or(false); let alert_type = match alert_type_str { "emergency" => AlertType::Emergency, "dead_man" => AlertType::DeadMan, _ => AlertType::Status, }; // Optional GPS let coordinate = if let (Some(lat), Some(lng)) = ( params["lat"].as_f64(), params["lng"].as_f64(), ) { Some(Coordinate::from_degrees(lat, lng, None)) } else { None }; let alert = AlertPayload { alert_type, message: message.to_string(), coordinate, }; let payload = message_types::encode_payload(&alert)?; // Sign the alert with node identity let (data, _) = self.state_manager.get_snapshot().await; let identity_dir = self.config.data_dir.join("identity"); let node_key_path = identity_dir.join("node_key"); let envelope = if node_key_path.exists() { let key_bytes = tokio::fs::read(&node_key_path).await?; if key_bytes.len() == 32 { let mut seed = [0u8; 32]; seed.copy_from_slice(&key_bytes); let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed); TypedEnvelope::new_signed(MeshMessageType::Alert, payload, &signing_key) } else { TypedEnvelope::new(MeshMessageType::Alert, payload) } } else { TypedEnvelope::new(MeshMessageType::Alert, payload) }; let wire = envelope.to_wire()?; let service = self.mesh_service.read().await; let svc = service .as_ref() .ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?; let wire_str = String::from_utf8_lossy(&wire).to_string(); if broadcast { // Send on channel (all peers) svc.send_message(0, &wire_str).await?; info!(alert_type = alert_type_str, "Broadcast alert over mesh"); } else if let Some(contact_id) = params["contact_id"].as_u64() { svc.send_message(contact_id as u32, &wire_str).await?; info!(contact_id, alert_type = alert_type_str, "Sent alert to peer"); } else { anyhow::bail!("Must specify contact_id or broadcast: true"); } Ok(serde_json::json!({ "sent": true, "alert_type": alert_type_str, "signed": envelope.sig.is_some(), })) } /// mesh.outbox — List pending store-and-forward messages. pub(super) async fn handle_mesh_outbox( &self, params: Option, ) -> Result { let limit = params .as_ref() .and_then(|p| p["limit"].as_u64()) .map(|n| n as usize); // Check if outbox file exists let outbox = mesh::outbox::MeshOutbox::load(&self.config.data_dir).await?; let messages = outbox.list(limit).await; let count = outbox.count().await; Ok(serde_json::json!({ "messages": messages.iter().map(|m| serde_json::json!({ "id": m.id, "dest_did": m.dest_did, "from_did": m.from_did, "created_at": m.created_at, "ttl_secs": m.ttl_secs, "retry_count": m.retry_count, "relay_hops": m.relay_hops, "expired": m.is_expired(), })).collect::>(), "count": count, })) } /// mesh.session-status — Get ratchet session info for a peer. pub(super) async fn handle_mesh_session_status( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let contact_id = params["contact_id"] .as_u64() .ok_or_else(|| anyhow::anyhow!("Missing contact_id"))? as u32; // Look up peer DID from mesh service let service = self.mesh_service.read().await; let peer_did = if let Some(svc) = service.as_ref() { let peers = svc.peers().await; peers.iter().find(|p| p.contact_id == contact_id).and_then(|p| p.did.clone()) } else { None }; if let Some(did) = peer_did { let session_mgr = mesh::session::SessionManager::new(&self.config.data_dir); if let Some(info) = session_mgr.session_info(&did).await { Ok(serde_json::json!({ "has_session": info.has_session, "forward_secrecy": info.forward_secrecy, "message_count": info.message_count, "ratchet_generation": info.ratchet_generation, "peer_did": did, })) } else { Ok(serde_json::json!({ "has_session": false, "forward_secrecy": false, "message_count": 0, "ratchet_generation": 0, "peer_did": did, })) } } else { Ok(serde_json::json!({ "has_session": false, "forward_secrecy": false, "message_count": 0, "ratchet_generation": 0, "peer_did": null, })) } } /// mesh.rotate-prekeys — Force prekey rotation for X3DH. pub(super) async fn handle_mesh_rotate_prekeys(&self) -> Result { // Load identity signing key let identity_dir = self.config.data_dir.join("identity"); let node_key_path = identity_dir.join("node_key"); let key_bytes = tokio::fs::read(&node_key_path) .await .map_err(|_| anyhow::anyhow!("Node identity not found"))?; if key_bytes.len() != 32 { anyhow::bail!("Invalid node key"); } let mut seed = [0u8; 32]; seed.copy_from_slice(&key_bytes); let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed); // Generate new prekey bundle let (bundle, _secrets) = mesh::x3dh::generate_prekey_bundle(&signing_key, 10)?; // Save bundle for distribution let bundle_bytes = mesh::x3dh::encode_bundle(&bundle)?; let prekey_dir = self.config.data_dir.join("prekeys"); tokio::fs::create_dir_all(&prekey_dir).await?; tokio::fs::write(prekey_dir.join("bundle.cbor"), &bundle_bytes).await?; info!( one_time_keys = bundle.one_time_prekeys.len(), "Prekey bundle rotated" ); Ok(serde_json::json!({ "rotated": true, "signed_prekey_id": bundle.signed_prekey.id, "one_time_prekeys": bundle.one_time_prekeys.len(), })) } }