Rename all crate directories, package names, binary names, proto package/module paths, ALPN strings, env var prefixes, config filenames, mDNS service names, and plugin ABI symbols from quicproquo/qpq to quicprochat/qpc.
229 lines
7.5 KiB
Rust
229 lines
7.5 KiB
Rust
//! 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(())
|
|
}
|