improve ssh-cli interaction surface
All checks were successful
Build Container / Build Container (push) Successful in 4m17s
All checks were successful
Build Container / Build Container (push) Successful in 4m17s
- better CLI parameter parsing - an actual help message - a new command to list currently known locks
This commit is contained in:
parent
fb19ca3840
commit
bc0c1b85ca
5 changed files with 320 additions and 215 deletions
|
|
@ -9,114 +9,128 @@
|
|||
//! SetEnv DOORIS_SERVER_URL=https://dooris.ccchh.net/ DOORIS_API_TOKEN=foobar123
|
||||
//! ```
|
||||
|
||||
mod api_client;
|
||||
mod api_models;
|
||||
mod cli;
|
||||
|
||||
use eyre::{Context, OptionExt, eyre};
|
||||
use reqwest::{Url, header::HeaderValue};
|
||||
use std::{env::VarError, process::exit};
|
||||
use std::process::exit;
|
||||
|
||||
use crate::api_models::LockOperationRequest;
|
||||
use eyre::eyre;
|
||||
|
||||
fn main() -> eyre::Result<()> {
|
||||
use crate::{
|
||||
api_client::ApiClient,
|
||||
api_models::{ApiLock, DesiredLockState},
|
||||
};
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
color_eyre::install()?;
|
||||
simple_eyre::install()?;
|
||||
|
||||
let doors = fetch_doors()?;
|
||||
let args = get_args()?;
|
||||
let args = args.split(" ").collect::<Vec<_>>();
|
||||
let args = cli::CliArgs::parse()?;
|
||||
tracing::debug!(cli = ?args, "Parsed CLI parameters");
|
||||
|
||||
if args.len() == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "" {
|
||||
println!("Help is not yet available. Come back later >~<");
|
||||
exit(3);
|
||||
} else if args[0] == "lock" || args[0] == "unlock" {
|
||||
let cli_target_door = args
|
||||
.get(1)
|
||||
.ok_or_eyre("'unlock' and 'lock' cli requires the door name as second argument")?;
|
||||
|
||||
let api_target_door = doors
|
||||
.iter()
|
||||
.find(|i_door| i_door.name.eq_ignore_ascii_case(cli_target_door))
|
||||
.ok_or({
|
||||
let door_names = doors
|
||||
.iter()
|
||||
.map(|i_door| i_door.name.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
eyre!(
|
||||
"No door named {cli_target_door:?} exists. Available doors are {door_names:?}.",
|
||||
)
|
||||
})?;
|
||||
|
||||
println!(
|
||||
"Lock {} ({:?}) identified!",
|
||||
api_target_door.id, api_target_door.name
|
||||
);
|
||||
operate_lock(
|
||||
&api_target_door.id,
|
||||
match args[0] {
|
||||
"lock" => api_models::DesiredLockState::CLOSED,
|
||||
"unlock" => api_models::DesiredLockState::OPEN,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
)?;
|
||||
println!("Lock has been turned. Enjoy :3");
|
||||
return Ok(());
|
||||
} else {
|
||||
println!("Unknown commandline. See --help for more info.");
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get passed arguments from SSH_ORIGINAL_COMMAND environment variable but falling back to direct CLI args
|
||||
fn get_args() -> eyre::Result<String> {
|
||||
match std::env::var("SSH_ORIGINAL_COMMAND") {
|
||||
Ok(original_command) => Ok(original_command),
|
||||
Err(VarError::NotUnicode(_)) => {
|
||||
Err(eyre!("Your command is not very unicoded. Refusing >~<"))
|
||||
match args {
|
||||
cli::CliArgs::Help => {
|
||||
println!(
|
||||
"dooris-over-ssh: Your personal door assistant with an extra serving of chaos"
|
||||
);
|
||||
println!(
|
||||
"Usage: ssh dooris@dooris.ccchh.net help | lock <door> [wait|nowait] | unlock <door> [wait|nowait]"
|
||||
);
|
||||
println!();
|
||||
println!("Commands:");
|
||||
println!();
|
||||
println!(" help -> The help command prints this message. Hello 👋");
|
||||
println!("");
|
||||
println!(" lock <door> [wait|nowait] -> Locks the door identified with <door>.");
|
||||
println!(
|
||||
" Also takes an additional parameter of \"wait\" or \"nowait\"which dictates"
|
||||
);
|
||||
println!(" whether the CLI exit as soon as the lock operation is queued or whether");
|
||||
println!(" it waits until the lock status changes to open.");
|
||||
println!(" Defaults to \"wait\" if not specified.");
|
||||
println!();
|
||||
println!(" unlock <door> [wait|nowait] -> Unlocks the door identified with <door>.");
|
||||
println!(
|
||||
" Also takes an additional parameter of \"wait\" or \"nowait\"which dictates"
|
||||
);
|
||||
println!(" whether the CLI exit as soon as the lock operation is queued or whether");
|
||||
println!(" it waits until the lock status changes to closed.");
|
||||
println!(" Defaults to \"wait\" if not specified.");
|
||||
exit(0);
|
||||
}
|
||||
Err(VarError::NotPresent) => Ok(std::env::args().skip(1).collect::<Vec<_>>().join(" ")),
|
||||
}
|
||||
cli::CliArgs::Lock {
|
||||
lock_name,
|
||||
wait_until_done,
|
||||
} => {
|
||||
let api = ApiClient::new_from_env()?;
|
||||
let lock = find_lock(&api, &lock_name).await?;
|
||||
|
||||
println!("dooris at your service, locking {} now…", lock.name);
|
||||
api.operate_lock(&lock.id, DesiredLockState::CLOSED).await?;
|
||||
|
||||
if wait_until_done {
|
||||
tracing::warn!(
|
||||
"wait feature is not yet implemented. lock should be turning now >~<"
|
||||
);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
cli::CliArgs::Unlock {
|
||||
lock_name,
|
||||
wait_until_done,
|
||||
} => {
|
||||
let api = ApiClient::new_from_env()?;
|
||||
let lock = find_lock(&api, &lock_name).await?;
|
||||
|
||||
println!("dooris at your service, locking {} now…", lock.name);
|
||||
api.operate_lock(&lock.id, DesiredLockState::OPEN).await?;
|
||||
|
||||
if wait_until_done {
|
||||
tracing::warn!(
|
||||
"wait feature is not yet implemented. lock should be turning now >~<"
|
||||
);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
cli::CliArgs::ShowLocks => {
|
||||
let api = ApiClient::new_from_env()?;
|
||||
let locks = api.fetch_locks().await?;
|
||||
|
||||
for i_lock in locks.iter() {
|
||||
println!(">> {}", i_lock.name);
|
||||
if i_lock.status.is_unreachable {
|
||||
println!(" !!! Lock is currently unreachable !!!");
|
||||
}
|
||||
if i_lock.status.is_low_batteray {
|
||||
println!(" !!! Lock has low battery !!!");
|
||||
}
|
||||
if i_lock.status.is_error_jammed {
|
||||
println!(" !!! Lock is jammed !!!");
|
||||
}
|
||||
println!(" State: {}", i_lock.status.lock_state);
|
||||
println!(" Desired State: {}", i_lock.status.lock_target_level);
|
||||
println!(" Acitivty: {}", i_lock.status.activity_state);
|
||||
println!();
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn http_client() -> eyre::Result<reqwest::blocking::Client> {
|
||||
return reqwest::blocking::ClientBuilder::new()
|
||||
.user_agent("dooris-ssh-cli")
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.http1_title_case_headers()
|
||||
.build()
|
||||
.context("Could not construct http client even though all values are statically known to be valid");
|
||||
}
|
||||
|
||||
fn api_token() -> eyre::Result<HeaderValue> {
|
||||
let server_token = std::env::var("DOORIS_API_TOKEN").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_API_TOKEN environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
||||
format!("Static-Token {server_token}")
|
||||
.try_into()
|
||||
.context("Could not convert HTTP API Token to a valid HTTP header")
|
||||
}
|
||||
|
||||
fn server_url() -> eyre::Result<Url> {
|
||||
let server_url = std::env::var("DOORIS_SERVER_URL").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_SERVER_URL environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
||||
Url::parse(&server_url).context("Configured DOORIS_SERVER_URL is not a valid URL. Contact your nearest server maid to fix the dooris setup")
|
||||
}
|
||||
|
||||
fn fetch_doors() -> eyre::Result<Vec<api_models::ApiLock>> {
|
||||
http_client()?
|
||||
.get(server_url()?.join("api/locks/")?)
|
||||
.header(reqwest::header::AUTHORIZATION, api_token()?)
|
||||
.send()?
|
||||
.error_for_status()
|
||||
.context("Could not get list of locks from the dooris API")?
|
||||
.json()
|
||||
.context("Could not parse list-locks response from dooris API")
|
||||
}
|
||||
|
||||
fn operate_lock(lock_id: &str, desired_state: api_models::DesiredLockState) -> eyre::Result<()> {
|
||||
println!("Turning Lock, please stand by…");
|
||||
http_client()?
|
||||
.patch(server_url()?.join(&format!("api/locks/{lock_id}"))?)
|
||||
.header(reqwest::header::AUTHORIZATION, api_token()?)
|
||||
.json(&LockOperationRequest { desired_state })
|
||||
.send()?
|
||||
.error_for_status()
|
||||
.context("Could not operate lock")?;
|
||||
Ok(())
|
||||
async fn find_lock(api: &ApiClient, name: &str) -> eyre::Result<ApiLock> {
|
||||
let locks = api.fetch_locks().await?;
|
||||
tracing::debug!("Comparing locks from API for one with name {name:?}");
|
||||
let lock = locks
|
||||
.into_iter()
|
||||
.find(|i_lock| i_lock.name.eq_ignore_ascii_case(&name))
|
||||
.ok_or_else(|| {
|
||||
eyre!("No lock named {name:?} exists. See \"list-locks\" for a list of valid locks.")
|
||||
})?;
|
||||
tracing::debug!(lock = ?lock,"Identified lock with name matching {name:?}");
|
||||
Ok(lock)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue