Skip to main content

plaudit_core/
client.rs

1use crate::oauth::OAuth;
2use crate::types::{FileDetail, FilesPage};
3use crate::{Result, API_BASE};
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7pub struct Client {
8    oauth: OAuth,
9}
10
11impl Default for Client {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl Client {
18    pub fn new() -> Self {
19        Client {
20            oauth: OAuth::new(),
21        }
22    }
23
24    pub fn oauth(&self) -> &OAuth {
25        &self.oauth
26    }
27
28    fn request<T: DeserializeOwned>(&self, method: &str, path: &str) -> Result<T> {
29        let token = self
30            .oauth
31            .get_access_token()?
32            .ok_or("Not authenticated. Run `plaud login` first.")?;
33        let url = format!("{API_BASE}{path}");
34        let req = if method == "POST" {
35            ureq::post(&url)
36        } else {
37            ureq::get(&url)
38        };
39        match req
40            .set("Authorization", &format!("Bearer {token}"))
41            .set("Accept", "application/json")
42            .call()
43        {
44            Ok(r) => Ok(r.into_json::<T>()?),
45            Err(ureq::Error::Status(code, r)) => {
46                let body = r.into_string().unwrap_or_default();
47                if code == 422 {
48                    if let Ok(v) = serde_json::from_str::<Value>(&body) {
49                        if let Some(detail) = v.get("detail").and_then(|d| d.as_array()) {
50                            let msgs: Vec<String> = detail
51                                .iter()
52                                .map(|d| {
53                                    let loc = d
54                                        .get("loc")
55                                        .and_then(|l| l.as_array())
56                                        .and_then(|a| a.last())
57                                        .and_then(|x| x.as_str())
58                                        .unwrap_or("");
59                                    let m = d.get("msg").and_then(|x| x.as_str()).unwrap_or("");
60                                    format!("{loc}: {m}")
61                                })
62                                .collect();
63                            return Err(msgs.join("; ").into());
64                        }
65                    }
66                }
67                Err(format!("API error: {code}").into())
68            }
69            Err(e) => Err(e.into()),
70        }
71    }
72
73    pub fn get_current_user(&self) -> Result<Value> {
74        self.request("GET", "/open/third-party/users/current")
75    }
76
77    /// Best-effort revoke; ignores body and errors (matches official CLI).
78    pub fn revoke_current_user(&self) {
79        let _ = self.request::<Value>("POST", "/open/third-party/users/current/revoke");
80    }
81
82    pub fn list_files(&self, page: u32, page_size: u32) -> Result<FilesPage> {
83        self.request(
84            "GET",
85            &format!("/open/third-party/files/?page={page}&page_size={page_size}"),
86        )
87    }
88
89    pub fn get_file(&self, id: &str) -> Result<FileDetail> {
90        self.request("GET", &format!("/open/third-party/files/{id}"))
91    }
92}