tetratto_core/
sdk.rs

1use crate::model::{
2    apps::{
3        AppDataQuery, AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery, ThirdPartyApp,
4    },
5    ApiReturn, Error, Result,
6};
7use reqwest::{
8    multipart::{Form, Part},
9    Client as HttpClient,
10};
11pub use reqwest::Method;
12use serde::{de::DeserializeOwned, Deserialize, Serialize};
13
14macro_rules! api_return_ok {
15    ($ret:ty, $res:ident) => {
16        match $res.json::<ApiReturn<$ret>>().await {
17            Ok(x) => {
18                if x.ok {
19                    Ok(x.payload)
20                } else {
21                    Err(Error::MiscError(x.message))
22                }
23            }
24            Err(e) => Err(Error::MiscError(e.to_string())),
25        }
26    };
27}
28
29/// A simplified app data query which matches what the API endpoint actually requires.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SimplifiedQuery {
32    pub query: AppDataSelectQuery,
33    pub mode: AppDataSelectMode,
34    pub cache: bool,
35}
36
37/// The data client is used to access an app's data storage capabilities.
38#[derive(Debug, Clone)]
39pub struct DataClient {
40    /// The HTTP client associated with this client.
41    pub http: HttpClient,
42    /// The app's API key. You can retrieve this from the web dashboard.
43    pub api_key: String,
44    /// The origin of the Tetratto server. When creating with [`DataClient::new`],
45    /// you can provide `None` to use `https://tetratto.com`.
46    pub host: String,
47}
48
49impl DataClient {
50    /// Create a new [`DataClient`].
51    pub fn new(host: Option<String>, api_key: String) -> Self {
52        Self {
53            http: HttpClient::new(),
54            api_key,
55            host: host.unwrap_or("https://tetratto.com".to_string()),
56        }
57    }
58
59    /// Get the current app using the provided API key.
60    ///
61    /// # Usage
62    /// ```rust
63    /// let client = DataClient::new("https://tetratto.com".to_string(), "...".to_string());
64    /// let app = client.get_app().await.expect("failed to get app");
65    /// ```
66    pub async fn get_app(&self) -> Result<ThirdPartyApp> {
67        match self
68            .http
69            .get(format!("{}/api/v1/app_data/app", self.host))
70            .header("Atto-Secret-Key", &self.api_key)
71            .send()
72            .await
73        {
74            Ok(x) => api_return_ok!(ThirdPartyApp, x),
75            Err(e) => Err(Error::MiscError(e.to_string())),
76        }
77    }
78
79    /// Check if the given IP is IP banned from the Tetratto host. You will only know
80    /// if the IP is banned or not, meaning you will not be shown the reason if it
81    /// is banned.
82    pub async fn check_ip(&self, ip: &str) -> Result<bool> {
83        match self
84            .http
85            .get(format!("{}/api/v1/bans/{}", self.host, ip))
86            .header("Atto-Secret-Key", &self.api_key)
87            .send()
88            .await
89        {
90            Ok(x) => api_return_ok!(bool, x),
91            Err(e) => Err(Error::MiscError(e.to_string())),
92        }
93    }
94
95    /// Query the app's data.
96    pub async fn query(&self, query: &SimplifiedQuery) -> Result<AppDataQueryResult> {
97        match self
98            .http
99            .post(format!("{}/api/v1/app_data/query", self.host))
100            .header("Atto-Secret-Key", &self.api_key)
101            .json(&query)
102            .send()
103            .await
104        {
105            Ok(x) => api_return_ok!(AppDataQueryResult, x),
106            Err(e) => Err(Error::MiscError(e.to_string())),
107        }
108    }
109
110    /// Insert a key, value pair into the app's data.
111    pub async fn insert(&self, key: String, value: String) -> Result<String> {
112        match self
113            .http
114            .post(format!("{}/api/v1/app_data", self.host))
115            .header("Atto-Secret-Key", &self.api_key)
116            .json(&serde_json::Value::Object({
117                let mut map = serde_json::Map::new();
118                map.insert("key".to_string(), serde_json::Value::String(key));
119                map.insert("value".to_string(), serde_json::Value::String(value));
120                map
121            }))
122            .send()
123            .await
124        {
125            Ok(x) => api_return_ok!(String, x),
126            Err(e) => Err(Error::MiscError(e.to_string())),
127        }
128    }
129
130    /// Update a record's value given its ID and the new value.
131    pub async fn update(&self, id: usize, value: String) -> Result<()> {
132        match self
133            .http
134            .post(format!("{}/api/v1/app_data/{id}/value", self.host))
135            .header("Atto-Secret-Key", &self.api_key)
136            .json(&serde_json::Value::Object({
137                let mut map = serde_json::Map::new();
138                map.insert("value".to_string(), serde_json::Value::String(value));
139                map
140            }))
141            .send()
142            .await
143        {
144            Ok(x) => api_return_ok!((), x),
145            Err(e) => Err(Error::MiscError(e.to_string())),
146        }
147    }
148
149    /// Update a record's key given its ID and the new key.
150    pub async fn rename(&self, id: usize, key: String) -> Result<()> {
151        match self
152            .http
153            .post(format!("{}/api/v1/app_data/{id}/key", self.host))
154            .header("Atto-Secret-Key", &self.api_key)
155            .json(&serde_json::Value::Object({
156                let mut map = serde_json::Map::new();
157                map.insert("key".to_string(), serde_json::Value::String(key));
158                map
159            }))
160            .send()
161            .await
162        {
163            Ok(x) => api_return_ok!((), x),
164            Err(e) => Err(Error::MiscError(e.to_string())),
165        }
166    }
167
168    /// Delete a row from the app's data by its `id`.
169    pub async fn remove(&self, id: usize) -> Result<()> {
170        match self
171            .http
172            .delete(format!("{}/api/v1/app_data/{id}", self.host))
173            .header("Atto-Secret-Key", &self.api_key)
174            .send()
175            .await
176        {
177            Ok(x) => api_return_ok!((), x),
178            Err(e) => Err(Error::MiscError(e.to_string())),
179        }
180    }
181
182    /// Delete row(s) from the app's data by a query.
183    pub async fn remove_query(&self, query: &AppDataQuery) -> Result<()> {
184        match self
185            .http
186            .delete(format!("{}/api/v1/app_data/query", self.host))
187            .header("Atto-Secret-Key", &self.api_key)
188            .json(&query)
189            .send()
190            .await
191        {
192            Ok(x) => api_return_ok!((), x),
193            Err(e) => Err(Error::MiscError(e.to_string())),
194        }
195    }
196}
197
198/// The state of the [`ApiClient`].
199#[derive(Debug, Clone, Default)]
200pub struct ApiClientState {
201    /// The token you received from an app grant request.
202    pub user_token: String,
203    /// The verifier you received from an app grant request.
204    pub user_verifier: String,
205    /// The ID of the user this client is connecting to.
206    pub user_id: usize,
207    /// The ID of the app that is being used for user grants.
208    ///
209    /// You can get this from the web dashboard.
210    pub app_id: usize,
211}
212
213/// The API client is used to manage authentication flow and send requests on behalf of a user.
214///
215/// This client assumes you already have the required information for the given user.
216/// If you don't, try using the JS SDK to extract this information.
217#[derive(Debug, Clone)]
218pub struct ApiClient {
219    /// The HTTP client associated with this client.
220    pub http: HttpClient,
221    /// The general state of the client. Will be updated whenever you refresh the user's token.
222    pub state: ApiClientState,
223    /// The origin of the Tetratto server. When creating with [`ApiClient::new`],
224    /// you can provide `None` to use `https://tetratto.com`.
225    pub host: String,
226}
227
228impl ApiClient {
229    /// Create a new [`ApiClient`].
230    pub fn new(host: Option<String>, state: ApiClientState) -> Self {
231        Self {
232            http: HttpClient::new(),
233            state,
234            host: host.unwrap_or("https://tetratto.com".to_string()),
235        }
236    }
237
238    /// Refresh the client's user_token.
239    pub async fn refresh_token(&mut self) -> Result<String> {
240        match self
241            .http
242            .post(format!(
243                "{}/api/v1/auth/user/{}/grants/{}/refresh",
244                self.host, self.state.user_id, self.state.app_id
245            ))
246            .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
247            .json(&serde_json::Value::Object({
248                let mut map = serde_json::Map::new();
249                map.insert(
250                    "verifier".to_string(),
251                    serde_json::Value::String(self.state.user_verifier.to_owned()),
252                );
253                map
254            }))
255            .send()
256            .await
257        {
258            Ok(x) => {
259                let ret = api_return_ok!(String, x)?;
260                self.state.user_token = ret.clone();
261                Ok(ret)
262            }
263            Err(e) => Err(Error::MiscError(e.to_string())),
264        }
265    }
266
267    /// Send a simple JSON request to the given endpoint.
268    pub async fn request<T, B>(
269        &self,
270        route: String,
271        method: Method,
272        body: Option<&B>,
273    ) -> Result<ApiReturn<T>>
274    where
275        T: Serialize + DeserializeOwned,
276        B: Serialize + ?Sized,
277    {
278        if let Some(body) = body {
279            match self
280                .http
281                .request(method, format!("{}/api/v1/auth/{route}", self.host))
282                .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
283                .json(&body)
284                .send()
285                .await
286            {
287                Ok(x) => api_return_ok!(ApiReturn<T>, x),
288                Err(e) => Err(Error::MiscError(e.to_string())),
289            }
290        } else {
291            match self
292                .http
293                .request(method, format!("{}/api/v1/auth/{route}", self.host))
294                .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
295                .send()
296                .await
297            {
298                Ok(x) => api_return_ok!(ApiReturn<T>, x),
299                Err(e) => Err(Error::MiscError(e.to_string())),
300            }
301        }
302    }
303
304    /// Send a JSON request with attachments to the given endpoint.
305    ///
306    /// This type of request is only required for routes which use JsonMultipart,
307    /// such as `POST /api/v1/posts` (`create_post`).
308    ///
309    /// Method is locked to `POST` for this type of request.
310    pub async fn request_attachments<T, B>(
311        &self,
312        route: String,
313        attachments: Vec<Vec<u8>>,
314        body: &B,
315    ) -> Result<ApiReturn<T>>
316    where
317        T: Serialize + DeserializeOwned,
318        B: Serialize + ?Sized,
319    {
320        let mut multipart_body = Form::new();
321
322        // add attachments
323        for v in attachments.clone() {
324            // the file name doesn't matter
325            multipart_body = multipart_body.part(String::new(), Part::bytes(v));
326        }
327
328        drop(attachments);
329
330        // add json
331        multipart_body = multipart_body.part(
332            String::new(),
333            Part::text(serde_json::to_string(body).unwrap()),
334        );
335
336        // ...
337        match self
338            .http
339            .post(format!("{}/api/v1/auth/{route}", self.host))
340            .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
341            .multipart(multipart_body)
342            .send()
343            .await
344        {
345            Ok(x) => api_return_ok!(ApiReturn<T>, x),
346            Err(e) => Err(Error::MiscError(e.to_string())),
347        }
348    }
349}