use reqwest::{Client, Url}; use serde::Deserialize; use thiserror::Error; use tracing::{event, instrument, Level}; #[derive(Debug, Error)] pub enum Error { #[error("invalid URL")] InvalidUrl(#[from] url::ParseError), #[error("error retrieving API response")] ResponseBody(#[source] reqwest::Error), #[error("invalid JSON in response")] InvalidJson(#[source] serde_json::Error), #[error("network request failed")] Network(#[source] reqwest::Error), } /// Handler for the IT-Syndikat API. #[derive(Clone, Debug)] pub struct ItsApi { spaceapi_url: Url, spaceping_url: Url, spaceping_token: String, client: Client, } fn response_get_lastchange(value: &mut serde_json::Value) -> Option<&mut serde_json::Value> { value .as_object_mut()? .get_mut("state")? .as_object_mut()? .get_mut("lastchange") } fn response_get_icon( value: &mut serde_json::Value, ) -> Option<&mut serde_json::Map> { value .as_object_mut()? .get_mut("state")? .as_object_mut()? .get_mut("icon")? .as_object_mut() } fn patch_response(value: &mut serde_json::Value) { // https://github.com/home-assistant/core/pull/83871 if let Some(lastchange) = response_get_lastchange(value) { if let Some(f) = lastchange.as_f64() { *lastchange = (f as u64).into(); } } // https://github.com/home-assistant/core/pull/108596 if let Some(icon) = response_get_icon(value) { if let Some(closed) = icon.remove("close") { icon.insert("closed".to_owned(), closed); } } } impl ItsApi { /// Constructs a new IT-Syndikat API handler. #[instrument] pub fn new(spaceapi_url: Url, spaceping_url: Url, spaceping_token: String) -> Self { Self { spaceapi_url, spaceping_url, spaceping_token, client: Client::new(), } } /// Request the SpaceAPI status. /// /// # Errors /// /// This function returns an error if the network request fails, or if the /// returned data can not be parsed as a valid SpaceAPI response. #[instrument] pub async fn status(&self) -> Result { event!(Level::DEBUG, "requesting spaceapi status"); let response = self .client .get(self.spaceapi_url.clone()) .send() .await .map_err(Error::Network)? .bytes() .await .map_err(Error::ResponseBody)?; let mut response = serde_json::from_slice(&response).map_err(Error::InvalidJson)?; patch_response(&mut response); let status = spaceapi::Status::deserialize(response).map_err(Error::InvalidJson)?; event!(Level::DEBUG, ?status); Ok(status) } /// Makes the box in the space beep. /// /// # Errors /// /// This function returns an error if the network request fails. #[instrument] pub async fn ping(&self) -> Result<(), Error> { event!(Level::INFO, "sending spaceping"); self.client .post(self.spaceping_url.clone()) .form(&[("apikey", &self.spaceping_token)]) .send() .await .map_err(Error::Network)? .error_for_status() .map_err(Error::ResponseBody)?; Ok(()) } }