use crate::model::{ apps::{ AppDataQuery, AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery, ThirdPartyApp, }, ApiReturn, Error, Result, }; use reqwest::{ multipart::{Form, Part}, Client as HttpClient, }; pub use reqwest::Method; use serde::{de::DeserializeOwned, Deserialize, Serialize}; macro_rules! api_return_ok { ($ret:ty, $res:ident) => { match $res.json::>().await { Ok(x) => { if x.ok { Ok(x.payload) } else { Err(Error::MiscError(x.message)) } } Err(e) => Err(Error::MiscError(e.to_string())), } }; } /// A simplified app data query which matches what the API endpoint actually requires. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SimplifiedQuery { pub query: AppDataSelectQuery, pub mode: AppDataSelectMode, } /// The data client is used to access an app's data storage capabilities. #[derive(Debug, Clone)] pub struct DataClient { /// The HTTP client associated with this client. pub http: HttpClient, /// The app's API key. You can retrieve this from the web dashboard. pub api_key: String, /// The origin of the Tetratto server. When creating with [`DataClient::new`], /// you can provide `None` to use `https://tetratto.com`. pub host: String, } impl DataClient { /// Create a new [`DataClient`]. pub fn new(host: Option, api_key: String) -> Self { Self { http: HttpClient::new(), api_key, host: host.unwrap_or("https://tetratto.com".to_string()), } } /// Get the current app using the provided API key. /// /// # Usage /// ```rust /// let client = DataClient::new("https://tetratto.com".to_string(), "...".to_string()); /// let app = client.get_app().await.expect("failed to get app"); /// ``` pub async fn get_app(&self) -> Result { match self .http .get(format!("{}/api/v1/app_data/app", self.host)) .header("Atto-Secret-Key", &self.api_key) .send() .await { Ok(x) => api_return_ok!(ThirdPartyApp, x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Check if the given IP is IP banned from the Tetratto host. You will only know /// if the IP is banned or not, meaning you will not be shown the reason if it /// is banned. pub async fn check_ip(&self, ip: &str) -> Result { match self .http .get(format!("{}/api/v1/bans/{}", self.host, ip)) .header("Atto-Secret-Key", &self.api_key) .send() .await { Ok(x) => api_return_ok!(bool, x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Query the app's data. pub async fn query(&self, query: &SimplifiedQuery) -> Result { match self .http .post(format!("{}/api/v1/app_data/query", self.host)) .header("Atto-Secret-Key", &self.api_key) .json(&query) .send() .await { Ok(x) => api_return_ok!(AppDataQueryResult, x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Insert a key, value pair into the app's data. pub async fn insert(&self, key: String, value: String) -> Result { match self .http .post(format!("{}/api/v1/app_data", self.host)) .header("Atto-Secret-Key", &self.api_key) .json(&serde_json::Value::Object({ let mut map = serde_json::Map::new(); map.insert("key".to_string(), serde_json::Value::String(key)); map.insert("value".to_string(), serde_json::Value::String(value)); map })) .send() .await { Ok(x) => api_return_ok!(String, x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Update a record's value given its ID and the new value. pub async fn update(&self, id: usize, value: String) -> Result<()> { match self .http .post(format!("{}/api/v1/app_data/{id}/value", self.host)) .header("Atto-Secret-Key", &self.api_key) .json(&serde_json::Value::Object({ let mut map = serde_json::Map::new(); map.insert("value".to_string(), serde_json::Value::String(value)); map })) .send() .await { Ok(x) => api_return_ok!((), x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Update a record's key given its ID and the new key. pub async fn rename(&self, id: usize, key: String) -> Result<()> { match self .http .post(format!("{}/api/v1/app_data/{id}/key", self.host)) .header("Atto-Secret-Key", &self.api_key) .json(&serde_json::Value::Object({ let mut map = serde_json::Map::new(); map.insert("key".to_string(), serde_json::Value::String(key)); map })) .send() .await { Ok(x) => api_return_ok!((), x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Delete a row from the app's data by its `id`. pub async fn remove(&self, id: usize) -> Result<()> { match self .http .delete(format!("{}/api/v1/app_data/{id}", self.host)) .header("Atto-Secret-Key", &self.api_key) .send() .await { Ok(x) => api_return_ok!((), x), Err(e) => Err(Error::MiscError(e.to_string())), } } /// Delete row(s) from the app's data by a query. pub async fn remove_query(&self, query: &AppDataQuery) -> Result<()> { match self .http .delete(format!("{}/api/v1/app_data/query", self.host)) .header("Atto-Secret-Key", &self.api_key) .json(&query) .send() .await { Ok(x) => api_return_ok!((), x), Err(e) => Err(Error::MiscError(e.to_string())), } } } /// The state of the [`ApiClient`]. #[derive(Debug, Clone, Default)] pub struct ApiClientState { /// The token you received from an app grant request. pub user_token: String, /// The verifier you received from an app grant request. pub user_verifier: String, /// The ID of the user this client is connecting to. pub user_id: usize, /// The ID of the app that is being used for user grants. /// /// You can get this from the web dashboard. pub app_id: usize, } /// The API client is used to manage authentication flow and send requests on behalf of a user. /// /// This client assumes you already have the required information for the given user. /// If you don't, try using the JS SDK to extract this information. #[derive(Debug, Clone)] pub struct ApiClient { /// The HTTP client associated with this client. pub http: HttpClient, /// The general state of the client. Will be updated whenever you refresh the user's token. pub state: ApiClientState, /// The origin of the Tetratto server. When creating with [`ApiClient::new`], /// you can provide `None` to use `https://tetratto.com`. pub host: String, } impl ApiClient { /// Create a new [`ApiClient`]. pub fn new(host: Option, state: ApiClientState) -> Self { Self { http: HttpClient::new(), state, host: host.unwrap_or("https://tetratto.com".to_string()), } } /// Refresh the client's user_token. pub async fn refresh_token(&mut self) -> Result { match self .http .post(format!( "{}/api/v1/auth/user/{}/grants/{}/refresh", self.host, self.state.user_id, self.state.app_id )) .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) .json(&serde_json::Value::Object({ let mut map = serde_json::Map::new(); map.insert( "verifier".to_string(), serde_json::Value::String(self.state.user_verifier.to_owned()), ); map })) .send() .await { Ok(x) => { let ret = api_return_ok!(String, x)?; self.state.user_token = ret.clone(); Ok(ret) } Err(e) => Err(Error::MiscError(e.to_string())), } } /// Send a simple JSON request to the given endpoint. pub async fn request( &self, route: String, method: Method, body: Option<&B>, ) -> Result> where T: Serialize + DeserializeOwned, B: Serialize + ?Sized, { if let Some(body) = body { match self .http .request(method, format!("{}/api/v1/auth/{route}", self.host)) .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) .json(&body) .send() .await { Ok(x) => api_return_ok!(ApiReturn, x), Err(e) => Err(Error::MiscError(e.to_string())), } } else { match self .http .request(method, format!("{}/api/v1/auth/{route}", self.host)) .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) .send() .await { Ok(x) => api_return_ok!(ApiReturn, x), Err(e) => Err(Error::MiscError(e.to_string())), } } } /// Send a JSON request with attachments to the given endpoint. /// /// This type of request is only required for routes which use JsonMultipart, /// such as `POST /api/v1/posts` (`create_post`). /// /// Method is locked to `POST` for this type of request. pub async fn request_attachments( &self, route: String, attachments: Vec>, body: &B, ) -> Result> where T: Serialize + DeserializeOwned, B: Serialize + ?Sized, { let mut multipart_body = Form::new(); // add attachments for v in attachments.clone() { // the file name doesn't matter multipart_body = multipart_body.part(String::new(), Part::bytes(v)); } drop(attachments); // add json multipart_body = multipart_body.part( String::new(), Part::text(serde_json::to_string(body).unwrap()), ); // ... match self .http .post(format!("{}/api/v1/auth/{route}", self.host)) .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) .multipart(multipart_body) .send() .await { Ok(x) => api_return_ok!(ApiReturn, x), Err(e) => Err(Error::MiscError(e.to_string())), } } }