lib_mal/
builder.rs

1use reqwest::Client;
2use std::fs;
3use std::path::PathBuf;
4use std::time::SystemTime;
5
6use crate::client::{decrypt_tokens, encrypt_token, TokenResponse, Tokens};
7use crate::{MALClient, MALError};
8
9///# Example
10///```
11///  use lib_mal::ClientBuilder;
12///  fn example() {
13///     let client = ClientBuilder::new().secret("[YOUR_CLIENT_ID]".to_string()).access_token("exampleExAmPlE".to_string()).build_no_refresh();
14///  }
15///```
16pub struct ClientBuilder {
17    client_secret: Option<String>,
18    dirs: Option<PathBuf>,
19    access_token: Option<String>,
20    caching: bool,
21}
22
23#[allow(clippy::new_without_default)]
24impl ClientBuilder {
25    ///Creates a new ClientBuilder. All fields are set to None by default.
26    pub fn new() -> Self {
27        ClientBuilder {
28            client_secret: None,
29            dirs: None,
30            access_token: None,
31            caching: false,
32        }
33    }
34
35    /// Sets the client_secret
36    /// # Example
37    ///
38    ///```
39    /// # use lib_mal::ClientBuilder;
40    /// # fn test() {
41    ///     let client =
42    ///     ClientBuilder::new().secret("[YOUR_CLIENT_ID]".to_string()).build_no_refresh();
43    ///
44    ///     let another_client = ClientBuilder::new().secret(None).build_no_refresh();
45    /// # }
46    ///
47    ///```
48    pub fn secret(mut self, secret: impl Into<Option<String>>) -> Self {
49        self.client_secret = secret.into();
50        self
51    }
52
53    /// Sets the directory the client will use to cache the tokens
54    /// # Example
55    ///
56    /// ```
57    /// # use lib_mal::ClientBuilder;
58    /// use std::path::PathBuf;
59    /// # fn test() {
60    ///     let client = ClientBuilder::new().cache_dir(PathBuf::new()).build_no_refresh();
61    /// # }
62    /// ```
63    pub fn cache_dir(mut self, path: impl Into<Option<PathBuf>>) -> Self {
64        self.dirs = path.into();
65        self
66    }
67
68    /// Sets the access token for the client
69    /// # Example
70    ///
71    /// ```
72    /// # use lib_mal::ClientBuilder;
73    /// # fn test() {
74    ///     let client =
75    ///     ClientBuilder::new().access_token("exampleToKeN".to_string()).build_no_refresh();
76    /// # }
77    /// ```
78    pub fn access_token(mut self, token: impl Into<Option<String>>) -> Self {
79        self.access_token = token.into();
80        self
81    }
82
83    /// Sets wether or not the client should cache the tokens
84    /// # Example
85    ///
86    /// ```
87    /// # use lib_mal::ClientBuilder;
88    /// # fn test() {
89    ///     let client = ClientBuilder::new().caching(false).build_no_refresh();
90    /// # }
91    ///
92    /// ```
93    pub fn caching(mut self, caching: bool) -> Self {
94        self.caching = caching;
95        self
96    }
97
98    /// Builds a `MALClient` without attempting to refresh the access token
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// use lib_mal::ClientBuilder;
104    /// use std::path::PathBuf;
105    /// fn example() {
106    ///     let client =
107    ///     ClientBuilder::new().secret("[YOUR_CLIENT_ID]".to_string()).caching(true).cache_dir(PathBuf::new()).build_no_refresh();
108    /// }
109    pub fn build_no_refresh(self) -> MALClient {
110        MALClient::new(
111            self.client_secret.unwrap_or_default(),
112            self.dirs.unwrap_or_default(),
113            self.access_token.unwrap_or_default(),
114            Client::new(),
115            self.caching,
116            false,
117        )
118    }
119
120    /// Builds a `MALClient` after attempting to refresh the access token from cache
121    ///
122    /// # Example
123    ///
124    /// ```
125    /// use lib_mal::ClientBuilder;
126    /// use lib_mal::MALError;
127    /// use std::path::PathBuf;
128    /// async fn example() -> Result<(), MALError> {
129    ///     let client =
130    ///     ClientBuilder::new().secret("[YOUR_CLIENT_ID]".to_string()).caching(true).cache_dir(PathBuf::new()).build_with_refresh().await?;
131    ///
132    ///     Ok(())
133    /// }
134    pub async fn build_with_refresh(self) -> Result<MALClient, MALError> {
135        let client = reqwest::Client::new();
136        let mut will_cache = self.caching;
137        let mut n_a = false;
138
139        let dir = if let Some(d) = self.dirs {
140            d
141        } else {
142            will_cache = false;
143            PathBuf::new()
144        };
145
146        let mut token = String::new();
147        if will_cache && dir.join("tokens").exists() {
148            if let Ok(tokens) = fs::read(dir.join("tokens")) {
149                let mut tok: Tokens = decrypt_tokens(&tokens).unwrap();
150                if let Ok(n) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
151                    if n.as_secs() - tok.today >= tok.expires_in as u64 {
152                        let params = [
153                            ("grant_type", "refresh_token"),
154                            ("refesh_token", &tok.refresh_token),
155                        ];
156                        let res = client
157                            .post("https://myanimelist.net/v1/oauth2/token")
158                            .form(&params)
159                            .send()
160                            .await;
161                        if let Err(e) = res {
162                            return Err(MALError::new(
163                                "Unable to refresh token",
164                                e.to_string().as_str(),
165                                None,
166                            ));
167                        }
168                        let new_toks = serde_json::from_str::<TokenResponse>(
169                            &res.unwrap().text().await.unwrap(),
170                        );
171                        if let Err(e) = new_toks {
172                            return Err(MALError::new(
173                                "Unable to parse token reponse",
174                                e.to_string().as_str(),
175                                None,
176                            ));
177                        }
178                        let new_toks = new_toks.unwrap();
179                        token = new_toks.access_token.clone();
180                        tok = Tokens {
181                            access_token: new_toks.access_token,
182                            refresh_token: new_toks.refresh_token,
183                            expires_in: new_toks.expires_in,
184                            today: SystemTime::now()
185                                .duration_since(SystemTime::UNIX_EPOCH)
186                                .unwrap()
187                                .as_secs(),
188                        };
189
190                        if let Err(e) = fs::write(dir.join("tokens"), encrypt_token(tok)) {
191                            return Err(MALError::new(
192                                "Unable to write tokens to cache",
193                                e.to_string().as_str(),
194                                None,
195                            ));
196                        }
197                    } else {
198                        token = tok.access_token;
199                    }
200                }
201            }
202        } else {
203            will_cache = self.caching;
204            n_a = true;
205        }
206
207        Ok(MALClient::new(
208            self.client_secret.unwrap_or_default(),
209            dir,
210            token,
211            client,
212            will_cache,
213            n_a,
214        ))
215    }
216}