tetratto-core 16.0.1

The core behind Tetratto
Documentation
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::<ApiReturn<$ret>>().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,
    pub cache: bool,
}

/// 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<String>, 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<ThirdPartyApp> {
        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<bool> {
        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<AppDataQueryResult> {
        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<String> {
        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<String>, 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<String> {
        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<T, B>(
        &self,
        route: String,
        method: Method,
        body: Option<&B>,
    ) -> Result<ApiReturn<T>>
    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<T>, 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<T>, 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<T, B>(
        &self,
        route: String,
        attachments: Vec<Vec<u8>>,
        body: &B,
    ) -> Result<ApiReturn<T>>
    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<T>, x),
            Err(e) => Err(Error::MiscError(e.to_string())),
        }
    }
}