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 fn get_document_views(record: &Value) -> Vec<Value> {
257 let mut views = Vec::new();
258
259 if let Some(public_views) = record.get("public_view").and_then(|v| v.as_array()) {
260 for entry in public_views {
261 views.push(serde_json::json!({
262 "label": entry.get("label").and_then(|v| v.as_str()).unwrap_or(""),
263 "token": entry.get("view_token").and_then(|v| v.as_str()).unwrap_or(""),
264 "type": "public_view",
265 "variations": entry.get("variations").cloned().unwrap_or(Value::Null),
266 "default_variation": entry.get("default_variation").cloned().unwrap_or(Value::Null),
267 }));
268 }
269 }
270
271 if let Some(view_tokens) = record.get("view_token").and_then(|v| v.as_array()) {
272 for entry in view_tokens {
273 views.push(serde_json::json!({
274 "label": entry.get("label").and_then(|v| v.as_str()).unwrap_or(""),
275 "token": entry.get("view_token").and_then(|v| v.as_str()).unwrap_or(""),
276 "type": "view_token",
277 "variations": null,
278 "default_variation": null,
279 }));
280 }
281 }
282
283 views
284 }
285
286 pub fn get_document_url(
295 &self,
296 record: &Value,
297 output: &str,
298 label: Option<&str>,
299 variation: Option<&str>,
300 download: bool,
301 ) -> Option<String> {
302 let views = Self::get_document_views(record);
303 if views.is_empty() {
304 return None;
305 }
306
307 let view = if let Some(label) = label {
308 views.iter()
309 .find(|v| v.get("label").and_then(|l| l.as_str()) == Some(label))
310 .unwrap_or(&views[0])
311 } else {
312 &views[0]
313 };
314
315 let token = view.get("token").and_then(|v| v.as_str()).unwrap_or("");
316 if token.is_empty() {
317 return None;
318 }
319
320 let view_type = view.get("type").and_then(|v| v.as_str()).unwrap_or("");
321
322 if view_type == "public_view" {
323 let mut url = format!("{}/render/{}?output={}", self.host, token, output);
324 let effective_variation = variation
325 .map(|s| s.to_string())
326 .or_else(|| {
327 view.get("default_variation")
328 .and_then(|v| v.as_str())
329 .map(|s| s.to_string())
330 });
331 if let Some(v) = effective_variation {
332 url.push_str(&format!("&variation={}", v));
333 }
334 if download {
335 url.push_str("&download=true");
336 }
337 return Some(url);
338 }
339
340 let action = match (output, download) {
342 ("pdf", true) => "pdf.download",
343 ("pdf", false) => "pdf.view",
344 ("context", _) => "pdf.context",
345 _ => "pdf.preview",
346 };
347 Some(format!("{}/documents/{}?token={}", self.host, action, token))
348 }
349
350 pub async fn get_document_content(
355 &self,
356 record: &Value,
357 output: &str,
358 label: Option<&str>,
359 variation: Option<&str>,
360 download: bool,
361 ) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
362 let url = match self.get_document_url(record, output, label, variation, download) {
363 Some(u) => u,
364 None => return Ok(None),
365 };
366 let resp = self.client.get(&url).send().await?.error_for_status()?;
367 let bytes = resp.bytes().await?;
368 Ok(Some(bytes.to_vec()))
369 }
370
371 pub async fn list_tasks(&self) -> Result<Option<Value>, reqwest::Error> {
375 self.request(Method::GET, "/tasks", None, None, None).await
376 }
377
378 pub async fn detail_task(&self, code: &str) -> Result<Option<Value>, reqwest::Error> {
380 let path = format!("/tasks/{}", code);
381 self.request(Method::OPTIONS, &path, None, None, None).await
382 }
383
384 pub async fn run_task(
386 &self, code: &str, payload: Option<&Value>,
387 ) -> Result<Option<Value>, reqwest::Error> {
388 let path = format!("/tasks/{}", code);
389 let method = if payload.is_some() { Method::POST } else { Method::GET };
390 self.request(method, &path, None, payload, None).await
391 }
392
393 pub async fn run_task_async(
396 &self,
397 code: &str,
398 payload: Option<&Value>,
399 poll_interval: Option<Duration>,
400 timeout: Option<Duration>,
401 ) -> Result<Value, Box<dyn std::error::Error>> {
402 let poll = poll_interval.unwrap_or(Duration::from_secs(1));
403 let tout = timeout.unwrap_or(Duration::from_secs(300));
404
405 let result = self.run_task(code, payload).await?;
406 let task_id = result
407 .as_ref()
408 .and_then(|v| v.get("id"))
409 .and_then(|v| v.as_str())
410 .map(|s| s.to_string());
411
412 let task_id = match task_id {
413 Some(id) => id,
414 None => return Ok(result.unwrap_or(Value::Null)),
415 };
416
417 let deadline = tokio::time::Instant::now() + tout;
418 loop {
419 sleep(poll).await;
420 if tokio::time::Instant::now() > deadline {
421 return Err(format!("Task {} ({}) did not finish within {:?}", code, task_id, tout).into());
422 }
423 let page = self.list("bapp_framework.taskdata", Some(&[("id", &task_id)])).await?;
424 if page.results.is_empty() {
425 continue;
426 }
427 let data = &page.results[0];
428 if data.get("failed").and_then(|v| v.as_bool()).unwrap_or(false) {
429 let msg = data.get("message").and_then(|v| v.as_str()).unwrap_or("");
430 return Err(format!("Task {} failed: {}", code, msg).into());
431 }
432 if data.get("finished").and_then(|v| v.as_bool()).unwrap_or(false) {
433 return Ok(data.clone());
434 }
435 }
436 }
437}
438
439impl Default for BappApiClient {
440 fn default() -> Self {
441 Self::new()
442 }
443}