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(()) } }