348 lines
11 KiB
Rust
348 lines
11 KiB
Rust
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,
|
|
}
|
|
|
|
/// 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())),
|
|
}
|
|
}
|
|
}
|