161 lines
5.5 KiB
Rust
161 lines
5.5 KiB
Rust
use clap::{CommandFactory, Parser, Subcommand};
|
|
|
|
#[derive(Subcommand)]
|
|
enum ServiceCommand {
|
|
/// Install and enable the systemd user service
|
|
Install,
|
|
/// Stop the systemd user service
|
|
Stop,
|
|
/// Show the systemd user service status
|
|
Status,
|
|
/// Start the systemd user service
|
|
Start,
|
|
/// Restart the systemd user service
|
|
Restart,
|
|
/// Stop, disable, and remove the systemd user service
|
|
Uninstall,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "picobot")]
|
|
#[command(about = "A CLI chatbot", long_about = None)]
|
|
#[command(version = "1.1.1")]
|
|
enum Command {
|
|
/// Connect to gateway
|
|
Chat {
|
|
/// Gateway WebSocket URL (e.g., ws://127.0.0.1:19876/ws)
|
|
#[arg(long)]
|
|
gateway_url: Option<String>,
|
|
/// One-time pairing code; saves the issued client token locally
|
|
#[arg(long)]
|
|
pair_code: Option<String>,
|
|
},
|
|
/// Start gateway server
|
|
Gateway {
|
|
/// Host to bind to
|
|
#[arg(long)]
|
|
host: Option<String>,
|
|
/// Port to listen on
|
|
#[arg(long)]
|
|
port: Option<u16>,
|
|
},
|
|
/// Generate a one-time browser pairing code from the local gateway
|
|
Pair {
|
|
/// Gateway WebSocket or HTTP URL
|
|
#[arg(long)]
|
|
gateway_url: Option<String>,
|
|
/// Revoke every paired browser and CLI token before issuing the code
|
|
#[arg(long)]
|
|
revoke_all: bool,
|
|
},
|
|
/// Manage the PicoBot systemd user service
|
|
Service {
|
|
#[command(subcommand)]
|
|
command: ServiceCommand,
|
|
},
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut cmd = Command::command();
|
|
|
|
// If no arguments, print help
|
|
if std::env::args().len() <= 1 {
|
|
cmd.print_help()?;
|
|
println!();
|
|
return Ok(());
|
|
}
|
|
|
|
match Command::parse() {
|
|
Command::Chat {
|
|
gateway_url,
|
|
pair_code,
|
|
} => {
|
|
let config = picobot::config::Config::load_default().ok();
|
|
let url = gateway_url
|
|
.or_else(|| config.as_ref().map(|c| c.client.gateway_url.clone()))
|
|
.unwrap_or_else(|| "ws://127.0.0.1:19876/ws".to_string());
|
|
picobot::client::run(&url, pair_code.as_deref()).await?;
|
|
}
|
|
Command::Gateway { host, port } => {
|
|
picobot::gateway::run(host, port).await?;
|
|
}
|
|
Command::Pair {
|
|
gateway_url,
|
|
revoke_all,
|
|
} => {
|
|
let config = picobot::config::Config::load_default().ok();
|
|
let url = gateway_url
|
|
.or_else(|| config.as_ref().map(|c| c.client.gateway_url.clone()))
|
|
.unwrap_or_else(|| "ws://127.0.0.1:19876/ws".to_string());
|
|
let mut endpoint = gateway_api_url(&url, "/api/auth/code")?;
|
|
if revoke_all {
|
|
endpoint.query_pairs_mut().append_pair("revoke_all", "true");
|
|
}
|
|
let admin_token_path = picobot::config::get_user_config_dir().join("web_admin_token");
|
|
let admin_token = std::fs::read_to_string(&admin_token_path).map_err(|error| {
|
|
format!(
|
|
"cannot read local gateway admin token {}: {error}",
|
|
admin_token_path.display()
|
|
)
|
|
})?;
|
|
let response = reqwest::Client::new()
|
|
.post(endpoint)
|
|
.header("X-Picobot-Admin-Token", admin_token.trim())
|
|
.send()
|
|
.await?;
|
|
let status = response.status();
|
|
let body: serde_json::Value = response.json().await?;
|
|
if !status.is_success() {
|
|
return Err(body
|
|
.get("error")
|
|
.and_then(serde_json::Value::as_str)
|
|
.unwrap_or("failed to generate pairing code")
|
|
.to_string()
|
|
.into());
|
|
}
|
|
let code = body
|
|
.get("pairing_code")
|
|
.and_then(serde_json::Value::as_str)
|
|
.ok_or("gateway did not return a pairing code")?;
|
|
println!("Pairing code: {code}");
|
|
println!("Expires in 5 minutes and can be used once.");
|
|
if revoke_all {
|
|
println!("All existing paired devices were revoked.");
|
|
}
|
|
}
|
|
Command::Service { command } => {
|
|
let command = match command {
|
|
ServiceCommand::Install => picobot::service::ServiceCommand::Install,
|
|
ServiceCommand::Stop => picobot::service::ServiceCommand::Stop,
|
|
ServiceCommand::Status => picobot::service::ServiceCommand::Status,
|
|
ServiceCommand::Start => picobot::service::ServiceCommand::Start,
|
|
ServiceCommand::Restart => picobot::service::ServiceCommand::Restart,
|
|
ServiceCommand::Uninstall => picobot::service::ServiceCommand::Uninstall,
|
|
};
|
|
picobot::service::execute(command).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn gateway_api_url(
|
|
gateway_url: &str,
|
|
path: &str,
|
|
) -> Result<reqwest::Url, Box<dyn std::error::Error>> {
|
|
let mut url = reqwest::Url::parse(gateway_url)?;
|
|
let scheme = match url.scheme() {
|
|
"ws" => "http",
|
|
"wss" => "https",
|
|
"http" => "http",
|
|
"https" => "https",
|
|
other => return Err(format!("unsupported gateway URL scheme: {other}").into()),
|
|
};
|
|
url.set_scheme(scheme)
|
|
.map_err(|_| "failed to set gateway URL scheme")?;
|
|
url.set_path(path);
|
|
url.set_query(None);
|
|
url.set_fragment(None);
|
|
Ok(url)
|
|
}
|