449 lines
16 KiB
Rust
Raw Normal View History

use super::RpcHandler;
2026-03-17 00:03:08 +00:00
use crate::mesh;
use crate::mesh::message_types::{
self, AlertPayload, AlertType, Coordinate, InvoicePayload, MeshMessageType, TypedEnvelope,
};
use anyhow::Result;
use tracing::info;
impl RpcHandler {
2026-03-17 00:03:08 +00:00
/// mesh.status — Get mesh radio status, device info, and peer count.
pub(super) async fn handle_mesh_status(&self) -> Result<serde_json::Value> {
2026-03-17 00:03:08 +00:00
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,
}))
}
}
2026-03-17 00:03:08 +00:00
/// mesh.peers — List discovered mesh peers.
pub(super) async fn handle_mesh_peers(&self) -> Result<serde_json::Value> {
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,
}))
}
}
2026-03-17 00:03:08 +00:00
/// mesh.messages — Get recent mesh message history.
pub(super) async fn handle_mesh_messages(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
2026-03-17 00:03:08 +00:00
let limit = params
.as_ref()
2026-03-17 00:03:08 +00:00
.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<serde_json::Value>,
) -> Result<serde_json::Value> {
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;
2026-03-17 00:03:08 +00:00
let message = params
.get("message")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing message"))?;
2026-03-17 00:03:08 +00:00
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!({
2026-03-17 00:03:08 +00:00
"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<serde_json::Value> {
2026-03-17 00:03:08 +00:00
let service = self.mesh_service.read().await;
let svc = service
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Mesh service not running. Enable mesh first."))?;
2026-03-17 00:03:08 +00:00
svc.broadcast_identity().await?;
info!("Broadcast identity over mesh");
2026-03-17 00:03:08 +00:00
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<serde_json::Value>,
) -> Result<serde_json::Value> {
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;
}
2026-03-17 00:03:08 +00:00
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?;
2026-03-17 00:03:08 +00:00
// 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<serde_json::Value>,
) -> Result<serde_json::Value> {
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<serde_json::Value>,
) -> Result<serde_json::Value> {
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<serde_json::Value>,
) -> Result<serde_json::Value> {
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<serde_json::Value>,
) -> Result<serde_json::Value> {
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::<Vec<_>>(),
"count": count,
}))
}
/// mesh.session-status — Get ratchet session info for a peer.
pub(super) async fn handle_mesh_session_status(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
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<serde_json::Value> {
// 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(),
}))
}
}