1use reqwest::{multipart, Client, Method, StatusCode};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::path::Path;
5use std::time::Duration;
6use tokio::fs;
7use tokio::time::sleep;
8
9#[derive(Debug, Deserialize, Serialize)]
11pub struct PagedList {
12 pub results: Vec<Value>,
13 pub count: i64,
14 pub next: Option<String>,
15 pub previous: Option<String>,
16}
17
18#[derive(Debug)]
20pub struct BappApiClient {
21 pub host: String,
22 pub tenant: Option<String>,
23 pub app: String,
24 auth_header: Option<String>,
25 client: Client,
26}
27
28impl BappApiClient {
29 pub fn new() -> Self {
31 Self {
32 host: "https://panel.bapp.ro/api".to_string(),
33 tenant: None,
34 app: "account".to_string(),
35 auth_header: None,
36 client: Client::new(),
37 }
38 }
39
40 pub fn with_host(mut self, host: &str) -> Self {
42 self.host = host.trim_end_matches('/').to_string();
43 self
44 }
45
46 pub fn with_bearer(mut self, token: &str) -> Self {
48 self.auth_header = Some(format!("Bearer {}", token));
49 self
50 }
51
52 pub fn with_token(mut self, token: &str) -> Self {
54 self.auth_header = Some(format!("Token {}", token));
55 self
56 }
57
58 pub fn with_tenant(mut self, tenant: &str) -> Self {
60 self.tenant = Some(tenant.to_string());
61 self
62 }
63
64 pub fn with_app(mut self, app: &str) -> Self {
66 self.app = app.to_string();
67 self
68 }
69
70 fn build_request(
71 &self,
72 method: Method,
73 path: &str,
74 params: Option<&[(&str, &str)]>,
75 extra_headers: Option<&[(&str, &str)]>,
76 ) -> reqwest::RequestBuilder {
77 let url = format!("{}{}", self.host, path);
78 let mut req = self.client.request(method, &url);
79 if let Some(p) = params {
80 req = req.query(p);
81 }
82 if let Some(auth) = &self.auth_header {
83 req = req.header("Authorization", auth);
84 }
85 if let Some(t) = &self.tenant {
86 req = req.header("x-tenant-id", t);
87 }
88 req = req.header("x-app-slug", &self.app);
89 if let Some(extra) = extra_headers {
90 for (k, v) in extra {
91 req = req.header(*k, *v);
92 }
93 }
94 req
95 }
96
97 async fn send(req: reqwest::RequestBuilder) -> Result<Option<Value>, reqwest::Error> {
98 let resp = req.send().await?.error_for_status()?;
99 if resp.status() == StatusCode::NO_CONTENT {
100 return Ok(None);
101 }
102 let data = resp.json::<Value>().await?;
103 Ok(Some(data))
104 }
105
106 async fn request(
107 &self,
108 method: Method,
109 path: &str,
110 params: Option<&[(&str, &str)]>,
111 body: Option<&Value>,
112 extra_headers: Option<&[(&str, &str)]>,
113 ) -> Result<Option<Value>, reqwest::Error> {
114 let mut req = self.build_request(method, path, params, extra_headers);
115 if let Some(b) = body {
116 req = req.json(b);
117 }
118 Self::send(req).await
119 }
120
121 pub async fn request_multipart(
124 &self,
125 method: Method,
126 path: &str,
127 fields: &[(&str, &str)],
128 files: &[(&str, &str)],
129 ) -> Result<Option<Value>, reqwest::Error> {
130 let req = self.build_request(method, path, None, None);
131 let mut form = multipart::Form::new();
132 for (k, v) in fields {
133 form = form.text(k.to_string(), v.to_string());
134 }
135 for (field, file_path) in files {
136 let path = Path::new(file_path);
137 let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
138 let bytes = fs::read(path).await.expect("failed to read file");
139 let part = multipart::Part::bytes(bytes).file_name(filename);
140 form = form.part(field.to_string(), part);
141 }
142 Self::send(req.multipart(form)).await
143 }
144
145 pub async fn me(&self) -> Result<Option<Value>, reqwest::Error> {
149 self.request(
150 Method::GET,
151 "/tasks/bapp_framework.me",
152 None, None,
153 Some(&[("x-app-slug", "")]),
154 ).await
155 }
156
157 pub async fn get_app(&self, app_slug: &str) -> Result<Option<Value>, reqwest::Error> {
161 self.request(
162 Method::GET,
163 "/tasks/bapp_framework.getapp",
164 None, None,
165 Some(&[("x-app-slug", app_slug)]),
166 ).await
167 }
168
169 pub async fn list_introspect(&self, content_type: &str) -> Result<Option<Value>, reqwest::Error> {
173 self.request(
174 Method::GET,
175 "/tasks/bapp_framework.listintrospect",
176 Some(&[("ct", content_type)]),
177 None, None,
178 ).await
179 }
180
181 pub async fn detail_introspect(
183 &self, content_type: &str, pk: Option<&str>,
184 ) -> Result<Option<Value>, reqwest::Error> {
185 let mut params = vec![("ct", content_type)];
186 if let Some(pk) = pk {
187 params.push(("pk", pk));
188 }
189 self.request(
190 Method::GET,
191 "/tasks/bapp_framework.detailintrospect",
192 Some(¶ms), None, None,
193 ).await
194 }
195
196 pub async fn list(
200 &self, content_type: &str, filters: Option<&[(&str, &str)]>,
201 ) -> Result<PagedList, Box<dyn std::error::Error>> {
202 let path = format!("/content-type/{}/", content_type);
203 let req = self.build_request(Method::GET, &path, filters, None);
204 let resp = req.send().await?.error_for_status()?;
205 let paged: PagedList = resp.json().await?;
206 Ok(paged)
207 }
208
209 pub async fn get(
211 &self, content_type: &str, id: &str,
212 ) -> Result<Option<Value>, reqwest::Error> {
213 let path = format!("/content-type/{}/{}/", content_type, id);
214 self.request(Method::GET, &path, None, None, None).await
215 }
216
217 pub async fn create(
219 &self, content_type: &str, data: Option<&Value>,
220 ) -> Result<Option<Value>, reqwest::Error> {
221 let path = format!("/content-type/{}/", content_type);
222 self.request(Method::POST, &path, None, data, None).await
223 }
224
225 pub async fn update(
227 &self, content_type: &str, id: &str, data: Option<&Value>,
228 ) -> Result<Option<Value>, reqwest::Error> {
229 let path = format!("/content-type/{}/{}/", content_type, id);
230 self.request(Method::PUT, &path, None, data, None).await
231 }
232
233 pub async fn patch(
235 &self, content_type: &str, id: &str, data: Option<&Value>,
236 ) -> Result<Option<Value>, reqwest::Error> {
237 let path = format!("/content-type/{}/{}/", content_type, id);
238 self.request(Method::PATCH, &path, None, data, None).await
239 }
240
241 pub async fn delete(
243 &self, content_type: &str, id: &str,
244 ) -> Result<Option<Value>, reqwest::Error> {
245 let path = format!("/content-type/{}/{}/", content_type, id);
246 self.request(Method::DELETE, &path, None, None, None).await
247 }
248
249 pub async fn list_tasks(&self) -> Result<Option<Value>, reqwest::Error> {
253 self.request(Method::GET, "/tasks", None, None, None).await
254 }
255
256 pub async fn detail_task(&self, code: &str) -> Result<Option<Value>, reqwest::Error> {
258 let path = format!("/tasks/{}", code);
259 self.request(Method::OPTIONS, &path, None, None, None).await
260 }
261
262 pub async fn run_task(
264 &self, code: &str, payload: Option<&Value>,
265 ) -> Result<Option<Value>, reqwest::Error> {
266 let path = format!("/tasks/{}", code);
267 let method = if payload.is_some() { Method::POST } else { Method::GET };
268 self.request(method, &path, None, payload, None).await
269 }
270
271 pub async fn run_task_async(
274 &self,
275 code: &str,
276 payload: Option<&Value>,
277 poll_interval: Option<Duration>,
278 timeout: Option<Duration>,
279 ) -> Result<Value, Box<dyn std::error::Error>> {
280 let poll = poll_interval.unwrap_or(Duration::from_secs(1));
281 let tout = timeout.unwrap_or(Duration::from_secs(300));
282
283 let result = self.run_task(code, payload).await?;
284 let task_id = result
285 .as_ref()
286 .and_then(|v| v.get("id"))
287 .and_then(|v| v.as_str())
288 .map(|s| s.to_string());
289
290 let task_id = match task_id {
291 Some(id) => id,
292 None => return Ok(result.unwrap_or(Value::Null)),
293 };
294
295 let deadline = tokio::time::Instant::now() + tout;
296 loop {
297 sleep(poll).await;
298 if tokio::time::Instant::now() > deadline {
299 return Err(format!("Task {} ({}) did not finish within {:?}", code, task_id, tout).into());
300 }
301 let page = self.list("bapp_framework.taskdata", Some(&[("id", &task_id)])).await?;
302 if page.results.is_empty() {
303 continue;
304 }
305 let data = &page.results[0];
306 if data.get("failed").and_then(|v| v.as_bool()).unwrap_or(false) {
307 let msg = data.get("message").and_then(|v| v.as_str()).unwrap_or("");
308 return Err(format!("Task {} failed: {}", code, msg).into());
309 }
310 if data.get("finished").and_then(|v| v.as_bool()).unwrap_or(false) {
311 return Ok(data.clone());
312 }
313 }
314 }
315}
316
317impl Default for BappApiClient {
318 fn default() -> Self {
319 Self::new()
320 }
321}