From bc0c1b85ca29fea38653df2c01b4c96c21dd15a4 Mon Sep 17 00:00:00 2001
From: lilly
Date: Sun, 19 Jul 2026 20:19:43 +0200
Subject: [PATCH] improve ssh-cli interaction surface
- better CLI parameter parsing
- an actual help message
- a new command to list currently known locks
---
ssh-cli/Cargo.lock | 140 +++++--------------------
ssh-cli/Cargo.toml | 5 +-
ssh-cli/src/api_client.rs | 78 ++++++++++++++
ssh-cli/src/cli.rs | 104 ++++++++++++++++++
ssh-cli/src/main.rs | 214 ++++++++++++++++++++------------------
5 files changed, 323 insertions(+), 218 deletions(-)
create mode 100644 ssh-cli/src/api_client.rs
create mode 100644 ssh-cli/src/cli.rs
diff --git a/ssh-cli/Cargo.lock b/ssh-cli/Cargo.lock
index 228d277..1ccaee5 100644
--- a/ssh-cli/Cargo.lock
+++ b/ssh-cli/Cargo.lock
@@ -2,21 +2,6 @@
# It is not intended for manual editing.
version = 4
-[[package]]
-name = "addr2line"
-version = "0.25.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
-
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -46,21 +31,6 @@ dependencies = [
"pkg-config",
]
-[[package]]
-name = "backtrace"
-version = "0.3.76"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
-dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-link",
-]
-
[[package]]
name = "base64"
version = "0.22.1"
@@ -129,33 +99,6 @@ dependencies = [
"cc",
]
-[[package]]
-name = "color-eyre"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
-dependencies = [
- "backtrace",
- "color-spantrace",
- "eyre",
- "indenter",
- "once_cell",
- "owo-colors",
- "tracing-error",
-]
-
-[[package]]
-name = "color-spantrace"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
-dependencies = [
- "once_cell",
- "owo-colors",
- "tracing-core",
- "tracing-error",
-]
-
[[package]]
name = "combine"
version = "4.6.7"
@@ -277,7 +220,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
dependencies = [
"futures-core",
- "futures-sink",
]
[[package]]
@@ -286,12 +228,6 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
-[[package]]
-name = "futures-io"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
-
[[package]]
name = "futures-sink"
version = "0.3.33"
@@ -311,10 +247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
- "futures-io",
- "futures-sink",
"futures-task",
- "memchr",
"pin-project-lite",
"slab",
]
@@ -346,12 +279,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "gimli"
-version = "0.32.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
-
[[package]]
name = "h2"
version = "0.4.15"
@@ -720,15 +647,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
-dependencies = [
- "adler2",
-]
-
[[package]]
name = "mio"
version = "1.2.2"
@@ -749,15 +667,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "object"
-version = "0.37.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -770,12 +679,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
-[[package]]
-name = "owo-colors"
-version = "4.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
-
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -919,9 +822,7 @@ dependencies = [
"base64",
"bytes",
"encoding_rs",
- "futures-channel",
"futures-core",
- "futures-util",
"h2",
"http",
"http-body",
@@ -966,12 +867,6 @@ dependencies = [
"windows-sys 0.52.0",
]
-[[package]]
-name = "rustc-demangle"
-version = "0.1.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
-
[[package]]
name = "rustc-hash"
version = "2.1.3"
@@ -1189,6 +1084,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+[[package]]
+name = "simple-eyre"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b561532e8ffe7ecf09108c4f662896a9ec3eac4999eba84015ec3dcb8cc630a"
+dependencies = [
+ "eyre",
+ "indenter",
+]
+
[[package]]
name = "slab"
version = "0.4.12"
@@ -1215,10 +1120,11 @@ dependencies = [
name = "ssh-cli"
version = "0.1.0"
dependencies = [
- "color-eyre",
"eyre",
"reqwest",
"serde",
+ "simple-eyre",
+ "tokio",
"tracing",
"tracing-subscriber",
]
@@ -1352,9 +1258,21 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
+ "tokio-macros",
"windows-sys 0.61.2",
]
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@@ -1455,16 +1373,6 @@ dependencies = [
"valuable",
]
-[[package]]
-name = "tracing-error"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
-dependencies = [
- "tracing",
- "tracing-subscriber",
-]
-
[[package]]
name = "tracing-log"
version = "0.2.0"
diff --git a/ssh-cli/Cargo.toml b/ssh-cli/Cargo.toml
index 15136fe..8c6edfc 100644
--- a/ssh-cli/Cargo.toml
+++ b/ssh-cli/Cargo.toml
@@ -4,9 +4,10 @@ version = "0.1.0"
edition = "2024"
[dependencies]
-color-eyre = "0.6.5"
eyre = "0.6.12"
-reqwest = { version = "0.13.4", features = ["blocking", "json"] }
+reqwest = { version = "0.13.4", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
+simple-eyre = "0.3.1"
+tokio = { version = "1.53.0", features = ["macros", "rt"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
diff --git a/ssh-cli/src/api_client.rs b/ssh-cli/src/api_client.rs
new file mode 100644
index 0000000..5e118fc
--- /dev/null
+++ b/ssh-cli/src/api_client.rs
@@ -0,0 +1,78 @@
+use eyre::Context;
+use eyre::eyre;
+use reqwest::Url;
+use reqwest::header::HeaderValue;
+
+use crate::api_models::ApiLock;
+use crate::api_models::DesiredLockState;
+use crate::api_models::LockOperationRequest;
+
+/// Client for interacting with the dooris API
+pub struct ApiClient {
+ client: reqwest::Client,
+ authorization: HeaderValue,
+ server_url: Url,
+}
+
+impl ApiClient {
+ pub fn new_from_env() -> eyre::Result {
+ let client = reqwest::Client::builder()
+ .user_agent("dooris-ssh-cli")
+ .redirect(reqwest::redirect::Policy::none())
+ .http1_title_case_headers()
+ .build()
+ .expect("Could not construct http client even though all values are statically known to be valid");
+
+ let api_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."))?;
+ let api_token = format!("Static-Token {api_token}")
+ .try_into()
+ .context("Could not convert HTTP API Token to a valid HTTP header")?;
+
+ 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."))?;
+ let server_url = Url::parse(&server_url).context("Configured DOORIS_SERVER_URL is not a valid URL. Contact your nearest server maid to fix the dooris setup")?;
+
+ tracing::debug!("Client is connected to dooris at {server_url}");
+ Ok(Self {
+ client,
+ authorization: api_token,
+ server_url,
+ })
+ }
+
+ /// Fetch the current list of locks along with their state from the API
+ pub async fn fetch_locks(&self) -> eyre::Result> {
+ tracing::debug!("Fetching locks from dooris API");
+ let url = self.server_url.join("api/locks/")?;
+ self.client
+ .get(url)
+ .header(reqwest::header::AUTHORIZATION, self.authorization.clone())
+ .send()
+ .await?
+ .error_for_status()
+ .context("Could not get list of locks from the dooris API")?
+ .json()
+ .await
+ .context("Could not parse list-locks response from dooris API")
+ }
+
+ /// Put the lock identified by `lock_id` into the given desired state.
+ ///
+ /// Returns as soon as the operation has been handed of to the backend and does not wait for physical lock movements.
+ pub async fn operate_lock(
+ &self,
+ lock_id: &str,
+ desired_state: DesiredLockState,
+ ) -> eyre::Result<()> {
+ tracing::debug!("Calling API to put {lock_id:?} into state {desired_state:?}");
+ let url = self.server_url.join(&format!("api/locks/{lock_id}"))?;
+ self.client
+ .patch(url)
+ .header(reqwest::header::AUTHORIZATION, self.authorization.clone())
+ .json(&LockOperationRequest { desired_state })
+ .send()
+ .await?
+ .error_for_status()
+ .context("Could not operate lock")?;
+ Ok(())
+ }
+}
diff --git a/ssh-cli/src/cli.rs b/ssh-cli/src/cli.rs
new file mode 100644
index 0000000..6a5b17b
--- /dev/null
+++ b/ssh-cli/src/cli.rs
@@ -0,0 +1,104 @@
+use eyre::{OptionExt, eyre};
+use std::env::VarError;
+
+/// CLI parameters given to this program
+#[derive(Debug, Eq, PartialEq)]
+pub enum CliArgs {
+ /// The user requested a help message
+ Help,
+ /// The user requested to lock a specific lock
+ Lock {
+ /// The name of the lock that should be locked
+ lock_name: String,
+ /// Whether the CLI should wait until the lock operation is not just started but until it is completely finished
+ wait_until_done: bool,
+ },
+ /// The user requested to unlock a specific lock
+ Unlock {
+ /// The name of the lock that should be unlocked
+ lock_name: String,
+ /// Whether the CLI should wait until the unlock operation is not just started but until it is completely finished
+ wait_until_done: bool,
+ },
+ /// The user wants to see a list of currently connected locks as well as their status
+ ShowLocks,
+}
+
+/// Get passed arguments from SSH_ORIGINAL_COMMAND environment variable but falling back to direct CLI args
+fn get_arg_string() -> eyre::Result {
+ match std::env::var("SSH_ORIGINAL_COMMAND") {
+ Ok(original_command) => {
+ tracing::trace!(
+ SSH_ORIGINAL_COMMAND = ?original_command,
+ "Taking CLI arguments from SSH_ORIGINAL_COMMAND environment variable"
+ );
+ Ok(original_command)
+ }
+ Err(VarError::NotUnicode(_)) => {
+ Err(eyre!("Your command is not very unicoded. Refusing >~<"))
+ }
+ Err(VarError::NotPresent) => {
+ let args = std::env::args().skip(1).collect::>().join(" ");
+ tracing::trace!(
+ argv = ?args,
+ "Taking CLI arguments from argv"
+ );
+ Ok(args)
+ }
+ }
+}
+
+impl CliArgs {
+ pub fn parse() -> eyre::Result {
+ let raw_args = get_arg_string()?;
+ let raw_args_list = raw_args.split_whitespace().collect::>();
+
+ if raw_args_list.len() == 0
+ || raw_args_list[0] == "help"
+ || raw_args_list[0] == "-h"
+ || raw_args_list[0] == "--help"
+ {
+ Ok(CliArgs::Help)
+ } else if raw_args_list[0] == "unlock" {
+ let lock_name = raw_args_list.get(1).ok_or_eyre(
+ "Unlock cli needs a lock name as parameter. See \"help\" for usage information.",
+ )?;
+ let wait_until_done = match *raw_args_list.get(2).unwrap_or(&"wait") {
+ "wait" => true,
+ "nowait" => false,
+ _ => {
+ return Err(eyre!(
+ "Third unlock parameter needs to be either \"wait\" or \"nowait\". See \"help\" for usage information."
+ ));
+ }
+ };
+ Ok(CliArgs::Unlock {
+ lock_name: lock_name.to_string(),
+ wait_until_done,
+ })
+ } else if raw_args_list[0] == "lock" {
+ let lock_name = raw_args_list.get(1).ok_or_eyre(
+ "Lock cli needs a lock name as parameter. See \"help\" for usage information.",
+ )?;
+ let wait_until_done = match *raw_args_list.get(2).unwrap_or(&"wait") {
+ "wait" => true,
+ "nowait" => false,
+ _ => {
+ return Err(eyre!(
+ "Third lock parameter needs to be either \"wait\" or \"nowait\". See \"help\" for usage information."
+ ));
+ }
+ };
+ Ok(CliArgs::Lock {
+ lock_name: lock_name.to_string(),
+ wait_until_done,
+ })
+ } else if raw_args_list[0] == "show-locks" {
+ Ok(CliArgs::ShowLocks)
+ } else {
+ Err(eyre!(
+ "Unknown commandline {raw_args:?}. See \"help\" for usage information."
+ ))
+ }
+ }
+}
diff --git a/ssh-cli/src/main.rs b/ssh-cli/src/main.rs
index 9c8ed62..b4690ee 100644
--- a/ssh-cli/src/main.rs
+++ b/ssh-cli/src/main.rs
@@ -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::>();
+ 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::>();
- 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 {
- 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 [wait|nowait] | unlock [wait|nowait]"
+ );
+ println!();
+ println!("Commands:");
+ println!();
+ println!(" help -> The help command prints this message. Hello 👋");
+ println!("");
+ println!(" lock [wait|nowait] -> Locks the door identified with .");
+ 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 [wait|nowait] -> Unlocks the door identified with .");
+ 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::>().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 {
- 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 {
- 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 {
- 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> {
- 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 {
+ 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)
}