Add v2_main.rs and v2_commands.rs as thin wrappers over QpqClient from quicproquo-sdk. Activated via --features v2 (feature-gated so v1 main.rs remains the default). Includes auto-server-launch, clap-based command surface (register-user, login, whoami, health, resolve, dm, group, devices), and ClientConfig-based connection setup. MLS-dependent commands (send, recv, group create/invite) print stubs pointing to the REPL.
884 lines
29 KiB
Rust
884 lines
29 KiB
Rust
//! quicproquo CLI client.
|
|
|
|
// ── v2 feature gate: when compiled with --features v2, use the SDK-based CLI.
|
|
#[cfg(feature = "v2")]
|
|
mod v2_commands;
|
|
#[cfg(feature = "v2")]
|
|
mod v2_main;
|
|
|
|
#[cfg(feature = "v2")]
|
|
fn main() {
|
|
v2_main::main();
|
|
}
|
|
|
|
// ── v1 CLI (default) ─────────────────────────────────────────────────────────
|
|
#[cfg(not(feature = "v2"))]
|
|
use std::path::{Path, PathBuf};
|
|
#[cfg(not(feature = "v2"))]
|
|
use anyhow::Context;
|
|
#[cfg(not(feature = "v2"))]
|
|
use clap::{Parser, Subcommand};
|
|
#[cfg(not(feature = "v2"))]
|
|
use quicproquo_client::{
|
|
cmd_chat, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_export, cmd_export_verify,
|
|
cmd_fetch_key, cmd_health, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_recv, cmd_register,
|
|
cmd_register_state, cmd_refresh_keypackage, cmd_register_user, cmd_send, cmd_whoami,
|
|
init_auth, run_repl, set_insecure_skip_verify, ClientAuth,
|
|
};
|
|
#[cfg(all(feature = "tui", not(feature = "v2")))]
|
|
use quicproquo_client::client::tui::run_tui;
|
|
|
|
// ── CLI ───────────────────────────────────────────────────────────────────────
|
|
#[cfg(not(feature = "v2"))]
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(name = "qpq", about = "quicproquo CLI client", version)]
|
|
struct Args {
|
|
/// Path to the server's TLS certificate (self-signed by default).
|
|
#[arg(
|
|
long,
|
|
global = true,
|
|
default_value = "data/server-cert.der",
|
|
env = "QPQ_CA_CERT"
|
|
)]
|
|
ca_cert: PathBuf,
|
|
|
|
/// Expected TLS server name (must match the certificate SAN).
|
|
#[arg(
|
|
long,
|
|
global = true,
|
|
default_value = "localhost",
|
|
env = "QPQ_SERVER_NAME"
|
|
)]
|
|
server_name: String,
|
|
|
|
/// Bearer token or OPAQUE session token for authenticated requests.
|
|
/// Not required for register-user and login commands.
|
|
#[arg(
|
|
long,
|
|
global = true,
|
|
env = "QPQ_ACCESS_TOKEN",
|
|
default_value = ""
|
|
)]
|
|
access_token: String,
|
|
|
|
/// Optional device identifier (UUID bytes encoded as hex or raw string).
|
|
#[arg(long, global = true, env = "QPQ_DEVICE_ID")]
|
|
device_id: Option<String>,
|
|
|
|
/// Password to encrypt/decrypt client state files (QPCE format).
|
|
/// If set, state files are encrypted at rest with Argon2id + ChaCha20Poly1305.
|
|
#[arg(long, global = true, env = "QPQ_STATE_PASSWORD")]
|
|
state_password: Option<String>,
|
|
|
|
/// DANGER: Skip TLS certificate verification. Development only.
|
|
/// Disables all certificate checks, making the connection vulnerable to MITM attacks.
|
|
#[arg(
|
|
long = "danger-accept-invalid-certs",
|
|
global = true,
|
|
env = "QPQ_DANGER_ACCEPT_INVALID_CERTS"
|
|
)]
|
|
danger_accept_invalid_certs: bool,
|
|
|
|
// ── Default-repl args (used when no subcommand is given) ─────────
|
|
/// State file path (identity + MLS state). Used when running the default REPL.
|
|
#[arg(long, default_value = "qpq-state.bin", env = "QPQ_STATE")]
|
|
state: PathBuf,
|
|
|
|
/// Server address (host:port). Used when running the default REPL.
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
|
|
/// OPAQUE username for automatic registration/login.
|
|
#[arg(long, env = "QPQ_USERNAME")]
|
|
username: Option<String>,
|
|
|
|
/// OPAQUE password (prompted securely if --username is set but --password is not).
|
|
#[arg(long, env = "QPQ_PASSWORD")]
|
|
password: Option<String>,
|
|
|
|
/// Do not auto-start a local qpq-server (useful when connecting to a remote server).
|
|
#[arg(long, env = "QPQ_NO_SERVER")]
|
|
no_server: bool,
|
|
|
|
#[command(subcommand)]
|
|
command: Option<Command>,
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[derive(Debug, Subcommand)]
|
|
enum Command {
|
|
/// Register a new user via OPAQUE (password never leaves the client).
|
|
RegisterUser {
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
/// Username for the new account.
|
|
#[arg(long)]
|
|
username: String,
|
|
/// Password (will be used in OPAQUE PAKE; server never sees it).
|
|
#[arg(long)]
|
|
password: String,
|
|
},
|
|
|
|
/// Log in via OPAQUE and receive a session token.
|
|
Login {
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
#[arg(long)]
|
|
username: String,
|
|
#[arg(long)]
|
|
password: String,
|
|
/// Hex-encoded Ed25519 identity key (64 hex chars). Optional if --state is provided.
|
|
#[arg(long)]
|
|
identity_key: Option<String>,
|
|
/// State file to derive the identity key (requires same password if encrypted).
|
|
#[arg(long)]
|
|
state: Option<PathBuf>,
|
|
/// Password for the encrypted state file (if any).
|
|
#[arg(long)]
|
|
state_password: Option<String>,
|
|
},
|
|
|
|
/// Show local identity key, fingerprint, group status, and hybrid key status.
|
|
Whoami {
|
|
/// State file path (identity + MLS state).
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
},
|
|
|
|
/// Check server connectivity and print status.
|
|
Health {
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Check if a peer has registered a hybrid key (non-consuming lookup).
|
|
CheckKey {
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
|
|
/// Peer's Ed25519 identity public key (64 hex chars = 32 bytes).
|
|
identity_key: String,
|
|
},
|
|
|
|
/// Send a Ping to the server and print the round-trip time.
|
|
Ping {
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Generate a fresh MLS KeyPackage and upload it to the Authentication Service.
|
|
Register {
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Fetch a peer's KeyPackage from the Authentication Service.
|
|
FetchKey {
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
|
|
/// Target peer's Ed25519 identity public key (64 hex chars = 32 bytes).
|
|
identity_key: String,
|
|
},
|
|
|
|
/// Run a two-party MLS demo (creator + joiner) against live AS and DS.
|
|
DemoGroup {
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Upload the persistent identity's KeyPackage to the AS (uses state file).
|
|
RegisterState {
|
|
/// State file path (identity + MLS state).
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
|
|
/// Authentication Service address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Refresh the KeyPackage on the server (existing state only).
|
|
/// Run periodically (e.g. before server TTL ~24h) or after your KeyPackage was consumed so others can invite you.
|
|
RefreshKeypackage {
|
|
/// State file path (identity + MLS state).
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Create a persistent group and save state to disk.
|
|
CreateGroup {
|
|
/// State file path (identity + MLS state).
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
|
|
/// Group identifier (arbitrary bytes, typically a human-readable name).
|
|
#[arg(long)]
|
|
group_id: String,
|
|
},
|
|
|
|
/// Invite a peer into the group and deliver a Welcome via DS.
|
|
Invite {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
/// Peer identity public key (64 hex chars = 32 bytes).
|
|
#[arg(long)]
|
|
peer_key: String,
|
|
},
|
|
|
|
/// Join a group by fetching the Welcome from the DS.
|
|
Join {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
},
|
|
|
|
/// Send an application message via the DS.
|
|
Send {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
/// Recipient identity key (hex, 32 bytes -> 64 chars). Omit when using --all.
|
|
#[arg(long)]
|
|
peer_key: Option<String>,
|
|
/// Send to all other group members (N-way groups).
|
|
#[arg(long)]
|
|
all: bool,
|
|
/// Plaintext message to send.
|
|
#[arg(long)]
|
|
msg: String,
|
|
},
|
|
|
|
/// Receive and decrypt all pending messages from the DS.
|
|
Recv {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
|
|
/// Wait for up to this many milliseconds if no messages are queued.
|
|
#[arg(long, default_value_t = 0)]
|
|
wait_ms: u64,
|
|
|
|
/// Continuously long-poll for messages.
|
|
#[arg(long)]
|
|
stream: bool,
|
|
},
|
|
|
|
/// Interactive multi-conversation REPL. Supports /dm, /create-group, /invite, /join, /switch, and more.
|
|
/// Automatically registers and logs in if --username/--password are provided (or prompts interactively).
|
|
Repl {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
/// OPAQUE username for automatic registration/login.
|
|
#[arg(long, env = "QPQ_USERNAME")]
|
|
username: Option<String>,
|
|
/// OPAQUE password (prompted securely if --username is set but --password is not).
|
|
#[arg(long, env = "QPQ_PASSWORD")]
|
|
password: Option<String>,
|
|
/// Do not auto-start a local qpq-server.
|
|
#[arg(long, env = "QPQ_NO_SERVER")]
|
|
no_server: bool,
|
|
},
|
|
|
|
/// Full-screen Ratatui TUI (requires --features tui).
|
|
/// Channels sidebar, scrollable message view, and inline input bar.
|
|
#[cfg(feature = "tui")]
|
|
Tui {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
/// OPAQUE username for automatic registration/login.
|
|
#[arg(long, env = "QPQ_USERNAME")]
|
|
username: Option<String>,
|
|
/// OPAQUE password (prompted securely if --username is set but --password is not).
|
|
#[arg(long, env = "QPQ_PASSWORD")]
|
|
password: Option<String>,
|
|
},
|
|
|
|
/// Interactive 1:1 chat: type to send, incoming messages printed as [peer] <msg>. Ctrl+D to exit.
|
|
/// In a two-person group, peer is chosen automatically; use --peer-key only with 3+ members.
|
|
Chat {
|
|
#[arg(
|
|
long,
|
|
default_value = "qpq-state.bin",
|
|
env = "QPQ_STATE"
|
|
)]
|
|
state: PathBuf,
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
/// Peer identity key (hex, 64 chars). Omit in a two-person group to use the only other member.
|
|
#[arg(long)]
|
|
peer_key: Option<String>,
|
|
/// How often to poll for incoming messages (milliseconds).
|
|
#[arg(long, default_value_t = 500)]
|
|
poll_interval_ms: u64,
|
|
},
|
|
|
|
/// Export a conversation's message history to an encrypted, tamper-evident transcript file.
|
|
///
|
|
/// The output file uses Argon2id + ChaCha20-Poly1305 encryption with a SHA-256 hash chain
|
|
/// linking every record. Use `qpq export verify` to check chain integrity without decrypting.
|
|
Export {
|
|
/// Path to the conversation database (.convdb file).
|
|
#[arg(long, default_value = "qpq-convdb.sqlite", env = "QPQ_CONV_DB")]
|
|
conv_db: PathBuf,
|
|
|
|
/// Conversation ID to export (32 hex chars = 16 bytes).
|
|
#[arg(long)]
|
|
conv_id: String,
|
|
|
|
/// Output path for the .qpqt transcript file.
|
|
#[arg(long, default_value = "transcript.qpqt")]
|
|
output: PathBuf,
|
|
|
|
/// Password used to encrypt the transcript (separate from the state/DB password).
|
|
#[arg(long, env = "QPQ_TRANSCRIPT_PASSWORD")]
|
|
transcript_password: Option<String>,
|
|
|
|
/// Password for the encrypted conversation database (if any).
|
|
#[arg(long, env = "QPQ_STATE_PASSWORD")]
|
|
db_password: Option<String>,
|
|
},
|
|
|
|
/// Verify the hash-chain integrity of a transcript file without decrypting content.
|
|
ExportVerify {
|
|
/// Path to the .qpqt transcript file to verify.
|
|
#[arg(long)]
|
|
input: PathBuf,
|
|
},
|
|
|
|
/// Execute a YAML playbook (scripted command sequence) and exit.
|
|
/// Requires `--features playbook`.
|
|
#[cfg(feature = "playbook")]
|
|
Run {
|
|
/// Path to the YAML playbook file.
|
|
playbook: PathBuf,
|
|
|
|
/// State file path (identity + MLS state).
|
|
#[arg(long, default_value = "qpq-state.bin", env = "QPQ_STATE")]
|
|
state: PathBuf,
|
|
|
|
/// Server address (host:port).
|
|
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
|
server: String,
|
|
|
|
/// OPAQUE username for automatic login.
|
|
#[arg(long, env = "QPQ_USERNAME")]
|
|
username: Option<String>,
|
|
|
|
/// OPAQUE password.
|
|
#[arg(long, env = "QPQ_PASSWORD")]
|
|
password: Option<String>,
|
|
|
|
/// Override playbook variables: KEY=VALUE (repeatable).
|
|
#[arg(long = "var", short = 'V')]
|
|
vars: Vec<String>,
|
|
},
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
#[cfg(not(feature = "v2"))]
|
|
/// Returns `qpq-{username}.bin` when `state` is still at the default
|
|
/// (`qpq-state.bin`) and a username has been provided. Otherwise returns
|
|
/// `state` unchanged. This lets `qpq --username alice` automatically isolate
|
|
/// Alice's state without requiring a manual `--state` flag.
|
|
fn derive_state_path(state: PathBuf, username: Option<&str>) -> PathBuf {
|
|
if state == Path::new("qpq-state.bin") {
|
|
if let Some(uname) = username {
|
|
return PathBuf::from(format!("qpq-{uname}.bin"));
|
|
}
|
|
}
|
|
state
|
|
}
|
|
|
|
// ── Playbook execution ───────────────────────────────────────────────────────
|
|
|
|
#[cfg(all(feature = "playbook", not(feature = "v2")))]
|
|
async fn run_playbook(
|
|
playbook_path: &Path,
|
|
state: &Path,
|
|
server: &str,
|
|
ca_cert: &Path,
|
|
server_name: &str,
|
|
state_pw: Option<&str>,
|
|
username: Option<&str>,
|
|
password: Option<&str>,
|
|
access_token: &str,
|
|
device_id: Option<&str>,
|
|
extra_vars: &[String],
|
|
) -> anyhow::Result<()> {
|
|
use quicproquo_client::PlaybookRunner;
|
|
|
|
let insecure = std::env::var("QPQ_DANGER_ACCEPT_INVALID_CERTS").is_ok();
|
|
|
|
// Connect to server.
|
|
let client =
|
|
quicproquo_client::connect_node_opt(server, ca_cert, server_name, insecure)
|
|
.await
|
|
.context("connect to server")?;
|
|
|
|
// Build session state.
|
|
let mut session = quicproquo_client::client::session::SessionState::load(state, state_pw)
|
|
.context("load session state")?;
|
|
|
|
// If username/password provided, do OPAQUE login.
|
|
if let (Some(uname), Some(pw)) = (username, password) {
|
|
if let Err(e) =
|
|
quicproquo_client::opaque_login(&client, uname, pw, &session.identity.public_key_bytes()).await
|
|
{
|
|
eprintln!("OPAQUE login failed: {e:#}");
|
|
}
|
|
} else if !access_token.is_empty() {
|
|
let auth = ClientAuth::from_parts(access_token.to_string(), device_id.map(String::from));
|
|
init_auth(auth);
|
|
}
|
|
|
|
// Load playbook.
|
|
let mut runner = PlaybookRunner::from_file(playbook_path)
|
|
.with_context(|| format!("load playbook: {}", playbook_path.display()))?;
|
|
|
|
// Inject extra variables from --var KEY=VALUE flags.
|
|
for kv in extra_vars {
|
|
if let Some((k, v)) = kv.split_once('=') {
|
|
runner.set_var(k, v);
|
|
} else {
|
|
eprintln!("warning: ignoring malformed --var '{kv}' (expected KEY=VALUE)");
|
|
}
|
|
}
|
|
|
|
// Inject connection info as variables.
|
|
runner.set_var("_server", server);
|
|
if let Some(u) = username {
|
|
runner.set_var("_username", u);
|
|
}
|
|
|
|
let report = runner.run(&mut session, &client).await;
|
|
print!("{report}");
|
|
|
|
if report.all_passed() {
|
|
Ok(())
|
|
} else {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
// Install the rustls crypto provider before any TLS operations.
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
|
|
)
|
|
.init();
|
|
|
|
let args = Args::parse();
|
|
|
|
if args.danger_accept_invalid_certs {
|
|
eprintln!("WARNING: TLS verification disabled — insecure mode");
|
|
set_insecure_skip_verify(true);
|
|
}
|
|
|
|
// For the REPL and TUI, defer init_auth so they can resolve their own token via OPAQUE.
|
|
// For all other subcommands, initialize auth immediately.
|
|
#[cfg(not(feature = "tui"))]
|
|
let is_repl = matches!(args.command, None | Some(Command::Repl { .. }));
|
|
#[cfg(feature = "tui")]
|
|
let is_repl = matches!(args.command, None | Some(Command::Repl { .. }) | Some(Command::Tui { .. }));
|
|
if !is_repl {
|
|
let auth_ctx = ClientAuth::from_parts(args.access_token.clone(), args.device_id.clone());
|
|
init_auth(auth_ctx);
|
|
}
|
|
|
|
let state_pw = args.state_password.as_deref();
|
|
|
|
// Default to REPL when no subcommand is given.
|
|
let no_server = args.no_server;
|
|
let command = args.command.unwrap_or_else(|| Command::Repl {
|
|
state: derive_state_path(args.state, args.username.as_deref()),
|
|
server: args.server,
|
|
username: args.username,
|
|
password: args.password,
|
|
no_server,
|
|
});
|
|
|
|
match command {
|
|
Command::RegisterUser {
|
|
server,
|
|
username,
|
|
password,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_register_user(
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
&username,
|
|
&password,
|
|
None,
|
|
))
|
|
.await
|
|
}
|
|
Command::Login {
|
|
server,
|
|
username,
|
|
password,
|
|
identity_key,
|
|
state,
|
|
state_password,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_login(
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
&username,
|
|
&password,
|
|
identity_key.as_deref(),
|
|
state.as_deref(),
|
|
state_password.as_deref(),
|
|
))
|
|
.await
|
|
}
|
|
Command::Whoami { state } => cmd_whoami(&state, state_pw),
|
|
Command::Health { server } => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_health(&server, &args.ca_cert, &args.server_name))
|
|
.await
|
|
}
|
|
Command::CheckKey {
|
|
server,
|
|
identity_key,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_check_key(
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
&identity_key,
|
|
))
|
|
.await
|
|
}
|
|
Command::Ping { server } => cmd_ping(&server, &args.ca_cert, &args.server_name).await,
|
|
Command::Register { server } => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_register(&server, &args.ca_cert, &args.server_name))
|
|
.await
|
|
}
|
|
Command::FetchKey {
|
|
server,
|
|
identity_key,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_fetch_key(
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
&identity_key,
|
|
))
|
|
.await
|
|
}
|
|
Command::DemoGroup { server } => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_demo_group(&server, &args.ca_cert, &args.server_name))
|
|
.await
|
|
}
|
|
Command::RegisterState { state, server } => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_register_state(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
state_pw,
|
|
))
|
|
.await
|
|
}
|
|
Command::RefreshKeypackage { state, server } => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_refresh_keypackage(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
state_pw,
|
|
))
|
|
.await
|
|
}
|
|
Command::CreateGroup {
|
|
state,
|
|
server,
|
|
group_id,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_create_group(&state, &server, &group_id, state_pw))
|
|
.await
|
|
}
|
|
Command::Invite {
|
|
state,
|
|
server,
|
|
peer_key,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_invite(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
&peer_key,
|
|
state_pw,
|
|
))
|
|
.await
|
|
}
|
|
Command::Join { state, server } => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_join(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
state_pw,
|
|
))
|
|
.await
|
|
}
|
|
Command::Send {
|
|
state,
|
|
server,
|
|
peer_key,
|
|
all,
|
|
msg,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_send(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
peer_key.as_deref(),
|
|
all,
|
|
&msg,
|
|
state_pw,
|
|
))
|
|
.await
|
|
}
|
|
Command::Recv {
|
|
state,
|
|
server,
|
|
wait_ms,
|
|
stream,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_recv(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
wait_ms,
|
|
stream,
|
|
state_pw,
|
|
))
|
|
.await
|
|
}
|
|
Command::Repl {
|
|
state,
|
|
server,
|
|
username,
|
|
password,
|
|
no_server,
|
|
} => {
|
|
let state = derive_state_path(state, username.as_deref());
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(run_repl(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
state_pw,
|
|
username.as_deref(),
|
|
password.as_deref(),
|
|
&args.access_token,
|
|
args.device_id.as_deref(),
|
|
no_server,
|
|
))
|
|
.await
|
|
}
|
|
Command::Chat {
|
|
state,
|
|
server,
|
|
peer_key,
|
|
poll_interval_ms,
|
|
} => {
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(cmd_chat(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
peer_key.as_deref(),
|
|
state_pw,
|
|
poll_interval_ms,
|
|
))
|
|
.await
|
|
}
|
|
#[cfg(feature = "tui")]
|
|
Command::Tui {
|
|
state,
|
|
server,
|
|
username,
|
|
password,
|
|
} => {
|
|
let state = derive_state_path(state, username.as_deref());
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(run_tui(
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
state_pw,
|
|
username.as_deref(),
|
|
password.as_deref(),
|
|
&args.access_token,
|
|
args.device_id.as_deref(),
|
|
))
|
|
.await
|
|
}
|
|
Command::Export {
|
|
conv_db,
|
|
conv_id,
|
|
output,
|
|
transcript_password,
|
|
db_password,
|
|
} => {
|
|
// Prompt for transcript password if not provided.
|
|
let tp = match transcript_password {
|
|
Some(p) => p,
|
|
None => {
|
|
eprint!("Transcript password: ");
|
|
rpassword::read_password()
|
|
.context("failed to read transcript password")?
|
|
}
|
|
};
|
|
cmd_export(
|
|
&conv_db,
|
|
&conv_id,
|
|
&output,
|
|
&tp,
|
|
db_password.as_deref().or(state_pw),
|
|
)
|
|
}
|
|
Command::ExportVerify { input } => cmd_export_verify(&input),
|
|
#[cfg(feature = "playbook")]
|
|
Command::Run {
|
|
playbook,
|
|
state,
|
|
server,
|
|
username,
|
|
password,
|
|
vars,
|
|
} => {
|
|
let state = derive_state_path(state, username.as_deref());
|
|
let local = tokio::task::LocalSet::new();
|
|
local
|
|
.run_until(run_playbook(
|
|
&playbook,
|
|
&state,
|
|
&server,
|
|
&args.ca_cert,
|
|
&args.server_name,
|
|
state_pw,
|
|
username.as_deref(),
|
|
password.as_deref(),
|
|
&args.access_token,
|
|
args.device_id.as_deref(),
|
|
&vars,
|
|
))
|
|
.await
|
|
}
|
|
}
|
|
}
|