//! v2 CLI command implementations — thin wrappers over the SDK. use quicprochat_sdk::client::QpqClient; use quicprochat_sdk::error::SdkError; /// Register a new user account via OPAQUE. pub async fn cmd_register_user( client: &mut QpqClient, username: &str, password: &str, ) -> Result<(), SdkError> { client.register(username, password).await?; let key = client.identity_key().unwrap_or_default(); println!("registered user: {username}"); println!("identity key : {}", hex::encode(key)); Ok(()) } /// Log in via OPAQUE and print session info. pub async fn cmd_login( client: &mut QpqClient, username: &str, password: &str, ) -> Result<(), SdkError> { client.login(username, password).await?; println!("logged in as: {username}"); if let Some(key) = client.identity_key() { println!("identity key: {}", hex::encode(key)); } Ok(()) } /// Print local identity information. pub fn cmd_whoami(client: &QpqClient) { match client.username() { Some(u) => println!("username : {u}"), None => println!("username : (not logged in)"), } match client.identity_key() { Some(k) => println!("identity key: {}", hex::encode(k)), None => println!("identity key: (none)"), } println!("connected : {}", client.is_connected()); println!("authenticated: {}", client.is_authenticated()); } /// Health check — connect to the server and report status. pub async fn cmd_health(client: &mut QpqClient) -> Result<(), SdkError> { let start = std::time::Instant::now(); // The SDK connect() already establishes a QUIC connection. // If we're already connected, just report success. if !client.is_connected() { client.connect().await?; } let rtt_ms = start.elapsed().as_millis(); println!("status : ok"); println!("rtt : {rtt_ms}ms"); Ok(()) } /// Resolve a username to its identity key. pub async fn cmd_resolve(client: &mut QpqClient, username: &str) -> Result<(), SdkError> { let rpc = client.rpc()?; match quicprochat_sdk::users::resolve_user(rpc, username).await? { Some(key) => { println!("{username} -> {}", hex::encode(&key)); } None => { println!("{username}: not found"); } } Ok(()) } /// List registered devices. pub async fn cmd_devices_list(client: &mut QpqClient) -> Result<(), SdkError> { let rpc = client.rpc()?; let devices = quicprochat_sdk::devices::list_devices(rpc).await?; if devices.is_empty() { println!("no devices registered"); } else { println!("{:<36} {:<20} {}", "DEVICE ID", "NAME", "REGISTERED AT"); for d in &devices { println!( "{:<36} {:<20} {}", hex::encode(&d.device_id), d.device_name, d.registered_at, ); } } Ok(()) } /// Register a new device. pub async fn cmd_devices_register( client: &mut QpqClient, device_id: &str, device_name: &str, ) -> Result<(), SdkError> { let rpc = client.rpc()?; let id_bytes = hex::decode(device_id) .map_err(|e| SdkError::Other(anyhow::anyhow!("invalid device_id hex: {e}")))?; let was_new = quicprochat_sdk::devices::register_device(rpc, &id_bytes, device_name).await?; if was_new { println!("device registered: {device_name}"); } else { println!("device already registered: {device_name}"); } Ok(()) } /// Revoke a device. pub async fn cmd_devices_revoke( client: &mut QpqClient, device_id: &str, ) -> Result<(), SdkError> { let rpc = client.rpc()?; let id_bytes = hex::decode(device_id) .map_err(|e| SdkError::Other(anyhow::anyhow!("invalid device_id hex: {e}")))?; let revoked = quicprochat_sdk::devices::revoke_device(rpc, &id_bytes).await?; if revoked { println!("device revoked: {device_id}"); } else { println!("device not found: {device_id}"); } Ok(()) } /// Set up account recovery — generate codes and upload encrypted bundles. pub async fn cmd_recovery_setup(client: &mut QpqClient) -> Result<(), SdkError> { // Load identity seed from state file. let state_path = client.config_state_path(); let stored = quicprochat_sdk::state::load_state(&state_path, None) .map_err(|e| SdkError::Crypto(format!("load identity for recovery: {e}")))?; let rpc = client.rpc()?; let codes = quicprochat_sdk::recovery::setup_recovery(rpc, &stored.identity_seed, &[]).await?; println!("=== RECOVERY CODES ==="); println!("Save these codes securely. They will NOT be shown again."); println!("Each code can independently recover your account."); println!(); for (i, code) in codes.iter().enumerate() { println!(" {}. {}", i + 1, code); } println!(); println!("{} codes generated and uploaded.", codes.len()); Ok(()) } // ── Outbox commands ────────────────────────────────────────────────────────── /// List pending outbox entries. pub fn cmd_outbox_list(client: &QpqClient) -> Result<(), SdkError> { let store = client.conversations()?; let entries = quicprochat_sdk::outbox::list_pending(store)?; if entries.is_empty() { println!("outbox is empty — no pending messages"); } else { println!("{:<6} {:<34} {:<8} PAYLOAD SIZE", "ID", "CONVERSATION", "RETRIES"); for e in &entries { println!( "{:<6} {:<34} {:<8} {} bytes", e.id, e.conversation_id.hex(), e.retry_count, e.payload.len(), ); } println!("\n{} pending entries", entries.len()); } Ok(()) } /// Retry sending all pending outbox entries. pub async fn cmd_outbox_retry(client: &mut QpqClient) -> Result<(), SdkError> { let rpc = client.rpc()?; let store = client.conversations()?; let (sent, failed) = quicprochat_sdk::outbox::flush_outbox(rpc, store).await?; println!("outbox flush: {sent} sent, {failed} permanently failed"); Ok(()) } /// Clear permanently failed outbox entries. pub fn cmd_outbox_clear(client: &QpqClient) -> Result<(), SdkError> { let store = client.conversations()?; let cleared = quicprochat_sdk::outbox::clear_failed(store)?; println!("cleared {cleared} failed outbox entries"); Ok(()) } /// Recover an account from a recovery code. pub async fn cmd_recovery_restore( client: &mut QpqClient, code: &str, ) -> Result<(), SdkError> { let rpc = client.rpc()?; let (identity_seed, conversation_ids) = quicprochat_sdk::recovery::recover_account(rpc, code).await?; // Restore identity. let keypair = quicprochat_core::IdentityKeypair::from_seed(identity_seed); client.set_identity_key(keypair.public_key_bytes().to_vec()); println!("account recovered successfully"); println!("identity key: {}", hex::encode(keypair.public_key_bytes())); if !conversation_ids.is_empty() { println!( "{} conversations need rejoin (peers must re-invite this device)", conversation_ids.len() ); } // Save recovered state. let state = quicprochat_sdk::state::StoredState { identity_seed, group: None, hybrid_key: None, member_keys: Vec::new(), }; let state_path = client.config_state_path(); quicprochat_sdk::state::save_state(&state_path, &state, None)?; println!("state saved to {}", state_path.display()); Ok(()) }