coc_rs/
api.rs

1use std::sync::{
2    atomic::{AtomicBool, AtomicUsize, Ordering},
3    Arc,
4};
5
6use async_recursion::async_recursion;
7use dashmap::DashMap;
8use parking_lot::Mutex;
9use reqwest::{RequestBuilder, Url};
10use serde::{de::DeserializeOwned, Deserialize, Serialize};
11
12#[cfg(feature = "cos")]
13use reqwest::header::{HeaderMap, HeaderValue};
14
15use crate::{
16    credentials::{Credential, Credentials},
17    dev::{self, CLIENT},
18    error::APIError,
19    models::{
20        clan, clan_capital, clan_search, gold_pass, labels, leagues, location, paging, player,
21        rankings, season, war, war_log,
22    },
23    util::LogicLong,
24};
25
26#[derive(Clone, Debug, Default)]
27pub struct Client {
28    ready: Arc<AtomicBool>,
29    pub(crate) accounts: Arc<DashMap<Credential, dev::APIAccount>>,
30
31    account_index: Arc<AtomicUsize>,
32    key_index: Arc<AtomicUsize>,
33
34    ip_address: Arc<Mutex<String>>,
35
36    #[cfg(feature = "cos")]
37    pub(crate) is_cos_logged_in: Arc<AtomicBool>,
38}
39
40impl Client {
41    const BASE_URL: &'static str = "https://api.clashofclans.com/v1";
42
43    /// Returns a [`Client`]
44    ///
45    /// # Errors
46    ///
47    /// This function will return an error if the credentials are invalid
48    pub async fn new(credentials: Credentials) -> anyhow::Result<Self> {
49        let client = Self {
50            ready: Arc::new(AtomicBool::new(false)),
51
52            accounts: Arc::new(DashMap::new()),
53
54            account_index: Arc::new(AtomicUsize::new(0)),
55            key_index: Arc::new(AtomicUsize::new(0)),
56
57            ip_address: Arc::new(Mutex::new(String::new())),
58
59            #[cfg(feature = "cos")]
60            is_cos_logged_in: Arc::new(AtomicBool::new(false)),
61        };
62
63        client.init(credentials).await?;
64        client.ready.store(true, Ordering::Relaxed);
65        Ok(client)
66    }
67
68    /// Called when the client is created to initialize every credential.
69    async fn init(&self, credentials: Credentials) -> anyhow::Result<()> {
70        let tasks = credentials.0.into_iter().map(dev::APIAccount::login);
71
72        let accounts =
73            futures::future::join_all(tasks).await.into_iter().collect::<Result<Vec<_>, _>>()?;
74
75        *self.ip_address.lock() = accounts[0].1.clone();
76
77        for (account, _) in accounts {
78            self.accounts.insert(account.credential.clone(), account);
79        }
80
81        Ok(())
82    }
83
84    /// Called when an IP address change is detected
85    pub(crate) async fn reinit(&self) -> anyhow::Result<()> {
86        #[cfg(feature = "tracing")]
87        tracing::debug!("reinitializing client");
88
89        self.ready.store(false, Ordering::Relaxed);
90
91        let accounts = self.accounts.iter().map(|account| account.clone()).collect::<Vec<_>>();
92
93        for mut account in accounts {
94            account.re_login().await?;
95
96            // update the account in the DashMap
97            self.accounts.insert(account.credential.clone(), account);
98        }
99
100        self.ready.store(true, Ordering::Relaxed);
101
102        Ok(())
103    }
104
105    /// Here you can create a client yourself and load them here later (for example .env parsing)
106    ///
107    /// # Errors
108    ///
109    /// This function will return an error if the credentials are invalid
110    ///
111    /// # Example
112    /// ```no_run
113    /// use coc_rs::{api::Client, credentials::Credentials};
114    ///
115    /// #[tokio::main]
116    /// async fn main() -> anyhow::Result<()> {
117    ///     let client = Client::new(None);
118    ///     let credentials = Credentials::builder()
119    ///         .add_credential("email", "password")
120    ///         .add_credential("email2", "password2")
121    ///         .build();
122    ///     client.load(credentials).await?;
123    ///
124    ///     Ok(())
125    /// }
126    /// ```
127    pub async fn load(&self, credentials: Credentials) -> anyhow::Result<()> {
128        #[cfg(feature = "tracing")]
129        tracing::trace!(credentials = ?credentials, "Loading credentials");
130
131        self.ready.store(false, Ordering::Relaxed);
132        self.init(credentials).await?;
133        self.ready.store(true, Ordering::Relaxed);
134        Ok(())
135    }
136
137    /// This is purely for diagnostics, it's not used anywhere else.
138    ///
139    /// # Example
140    /// ```no_run
141    /// use coc_rs::Client;
142    ///
143    /// let credentials = Credentials::builder()
144    ///     .add_credential("email", "password")
145    ///     .add_credential("email2", "password2")
146    ///     .build();
147    /// let client = Client::new(credentials);
148    /// client.debug_keys().await;
149    /// ```
150    #[cfg(feature = "tracing")]
151    pub fn debug_keys(&self) {
152        self.accounts.iter().for_each(|account| {
153            account.keys.keys.iter().for_each(|key| {
154                tracing::debug!(key = %key.key, key.id=%key.id, key.name=%key.name);
155            });
156        });
157    }
158
159    //         ╭──────────────────────────────────────────────────────────╮
160    //         │                       HTTP Methods                       │
161    //         ╰──────────────────────────────────────────────────────────╯
162
163    pub(crate) fn get<U: reqwest::IntoUrl>(
164        &self,
165        url: U,
166    ) -> Result<reqwest::RequestBuilder, APIError> {
167        if !self.ready.load(Ordering::Relaxed) {
168            return Err(APIError::ClientNotReady);
169        }
170        Ok(CLIENT.get(url).bearer_auth(self.get_next_key()))
171    }
172
173    pub(crate) fn post<U: reqwest::IntoUrl, T: Into<reqwest::Body>>(
174        &self,
175        url: U,
176        body: T,
177    ) -> Result<reqwest::RequestBuilder, APIError> {
178        if !self.ready.load(Ordering::Relaxed) {
179            return Err(APIError::ClientNotReady);
180        }
181        Ok(CLIENT.post(url).bearer_auth(self.get_next_key()).body(body))
182    }
183
184    /// To allow usage without a client being ready
185    #[cfg(feature = "cos")]
186    pub(crate) fn cos_get<U: reqwest::IntoUrl>(
187        &self,
188        url: U,
189    ) -> Result<reqwest::RequestBuilder, APIError> {
190        let mut headers = HeaderMap::new();
191        headers.insert("authority", HeaderValue::from_str("api.clashofstats.com")?);
192        headers.insert("method", HeaderValue::from_str("GET")?);
193        headers.insert("scheme", HeaderValue::from_str("https")?);
194        headers.insert(
195            "accept",
196            HeaderValue::from_str("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")?,
197        );
198        headers.insert(
199            "accept-language",
200            HeaderValue::from_str("en-US,en;q=0.9,zh-CN;q=0.8,z;q=0.7")?,
201        );
202        headers.insert(
203            "sec-ch-ua",
204            HeaderValue::from_str(
205                "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"103\", \"Chromium\";v=\"103\"",
206            )?,
207        );
208        headers.insert("sec-ch-ua-platform", HeaderValue::from_str("\"Windows\"")?);
209        headers.insert("upgrade-insecure-requests", HeaderValue::from_str("1")?);
210        headers.insert(
211            "user-agent",
212            HeaderValue::from_str("Mozilla/5.0 (X11; Windows x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36")?,
213        );
214
215        Ok(CLIENT.get(url).headers(headers))
216    }
217
218    /// To allow usage without a client being ready
219    #[cfg(feature = "cos")]
220    pub(crate) fn cos_post<U: reqwest::IntoUrl, T: Into<reqwest::Body>>(
221        &self,
222        url: U,
223        body: T,
224    ) -> Result<reqwest::RequestBuilder, APIError> {
225        let mut headers = HeaderMap::new();
226        headers.insert("authority", HeaderValue::from_str("api.clashofstats.com")?);
227        headers.insert("method", HeaderValue::from_str("POST")?);
228        headers.insert("scheme", HeaderValue::from_str("https")?);
229        headers.insert("accept", HeaderValue::from_str("application/json, text/plain, */*")?);
230        headers.insert("accept-encoding", HeaderValue::from_str("gzip, deflate, br")?);
231        headers.insert(
232            "accept-language",
233            HeaderValue::from_str("en-US,en;q=0.9,zh-CN;q=0.8,z;q=0.7")?,
234        );
235        headers.insert("content-type", HeaderValue::from_str("application/json;charset=UTF-8")?);
236        headers.insert(
237            "sec-ch-ua",
238            HeaderValue::from_str(
239                "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"103\", \"Chromium\";v=\"103\"",
240            )?,
241        );
242        headers.insert("sec-ch-ua-platform", HeaderValue::from_str("\"Windows\"")?);
243        headers.insert("upgrade-insecure-requests", HeaderValue::from_str("1")?);
244        headers.insert(
245            "user-agent",
246            HeaderValue::from_str("Mozilla/5.0 (X11; Windows x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36")?,
247        );
248
249        Ok(CLIENT.post(url).body(body).headers(headers))
250    }
251
252    //         ╭──────────────────────────────────────────────────────────╮
253    //         │                       Clan Methods                       │
254    //         ╰──────────────────────────────────────────────────────────╯
255
256    /// # Errors
257    ///
258    /// This function will return an error if the request fails
259    pub async fn get_clan_warlog(
260        &self,
261        clan_tag: &str,
262    ) -> Result<APIResponse<war_log::WarLog>, APIError> {
263        #[cfg(feature = "tracing")]
264        tracing::trace!("get_clan_warlog({})", clan_tag);
265        let clan_tag = clan_tag.parse::<LogicLong>()?.to_string();
266        let url = format!("{}/clans/{}/warlog", Self::BASE_URL, urlencoding::encode(&clan_tag));
267        self.parse_json(self.get(url), false).await
268    }
269
270    /// # Errors
271    ///
272    /// This function will return an error if the request fails
273    pub async fn get_clans(
274        &self,
275        options: clan_search::ClanSearchOptions,
276    ) -> Result<APIResponse<clan::Clan>, APIError> {
277        #[cfg(feature = "tracing")]
278        tracing::trace!("get_clans({})", options);
279        let url = Url::parse_with_params(&format!("{}/clans", Self::BASE_URL), options.items)?;
280        self.parse_json(self.get(url.to_string()), false).await
281    }
282
283    /// # Errors
284    ///
285    /// This function will return an error if the request fails
286    pub async fn get_current_war(&self, clan_tag: &str) -> Result<war::War, APIError> {
287        #[cfg(feature = "tracing")]
288        tracing::trace!("get_current_war({})", clan_tag);
289        let clan_tag = clan_tag.parse::<LogicLong>()?.to_string();
290        let url = format!("{}/clans/{}/currentwar", Self::BASE_URL, urlencoding::encode(&clan_tag));
291        self.parse_json(self.get(url), false).await
292    }
293
294    /// # Errors
295    ///
296    /// This function will return an error if the request fails
297    pub async fn get_clan(&self, clan_tag: &str) -> Result<clan::Clan, APIError> {
298        #[cfg(feature = "tracing")]
299        tracing::trace!("get_clan({})", clan_tag);
300        let clan_tag = clan_tag.parse::<LogicLong>()?.to_string();
301        let url = format!("{}/clans/{}", Self::BASE_URL, urlencoding::encode(&clan_tag));
302        self.parse_json(self.get(url), false).await
303    }
304
305    /// # Errors
306    ///
307    /// This function will return an error if the request fails
308    pub async fn get_clan_members(
309        &self,
310        clan_tag: &str,
311    ) -> Result<APIResponse<clan::ClanMember>, APIError> {
312        #[cfg(feature = "tracing")]
313        tracing::trace!("get_clan_members({})", clan_tag);
314        let clan_tag = clan_tag.parse::<LogicLong>()?.to_string();
315        let url = format!("{}/clans/{}/members", Self::BASE_URL, urlencoding::encode(&clan_tag));
316        self.parse_json(self.get(url), false).await
317    }
318
319    /// # Errors
320    ///
321    /// This function will return an error if the request fails
322    pub async fn get_clan_capital_raid_seasons(
323        &self,
324        clan_tag: &str,
325    ) -> Result<APIResponse<clan_capital::ClanCapitalRaidSeason>, APIError> {
326        #[cfg(feature = "tracing")]
327        tracing::trace!("get_clan_capital_raid_seasons({})", clan_tag);
328        let clan_tag = clan_tag.parse::<LogicLong>()?.to_string();
329        let url = format!(
330            "{}/clans/{}/capitalraidseasons",
331            Self::BASE_URL,
332            urlencoding::encode(&clan_tag)
333        );
334        self.parse_json(self.get(url), false).await
335    }
336
337    //         ╭──────────────────────────────────────────────────────────╮
338    //         │                      Player Methods                      │
339    //         ╰──────────────────────────────────────────────────────────╯
340
341    /// # Errors
342    ///
343    /// This function will return an error if the request fails
344    pub async fn get_player(&self, player_tag: &str) -> Result<player::Player, APIError> {
345        #[cfg(feature = "tracing")]
346        tracing::trace!("get_player({})", player_tag);
347        let player_tag = player_tag.parse::<LogicLong>()?.to_string();
348        let url = format!("{}/players/{}", Self::BASE_URL, urlencoding::encode(&player_tag));
349        self.parse_json(self.get(url), false).await
350    }
351
352    /// # Errors
353    ///
354    /// This function will return an error if the request fails
355    pub async fn verify_player_token(
356        &self,
357        player_tag: &str,
358        token: &str,
359    ) -> Result<player::PlayerToken, APIError> {
360        #[cfg(feature = "tracing")]
361        tracing::trace!("verify_player_token({}, {})", player_tag, token);
362        let player_tag = player_tag.parse::<LogicLong>()?.to_string();
363        let url =
364            format!("{}/players/{}/verifytoken", Self::BASE_URL, urlencoding::encode(&player_tag));
365        let token = format!("{{\"token\":\"{token}\"}}");
366        self.parse_json(self.post(url, token), false).await
367    }
368
369    //         ╭──────────────────────────────────────────────────────────╮
370    //         │                      League Methods                      │
371    //         ╰──────────────────────────────────────────────────────────╯
372
373    /// # Errors
374    ///
375    /// This function will return an error if the request fails
376    pub async fn get_leagues(&self) -> Result<APIResponse<leagues::League>, APIError> {
377        #[cfg(feature = "tracing")]
378        tracing::trace!("get_leagues()");
379        let url = format!("{}/leagues", Self::BASE_URL);
380        self.parse_json(self.get(url), false).await
381    }
382
383    /// # Errors
384    ///
385    /// This function will return an error if the request fails
386    pub async fn get_league_season_rankings(
387        &self,
388        league_id: leagues::LeagueKind,
389        season_id: season::Season,
390        paging: paging::Paging,
391    ) -> Result<APIResponse<rankings::PlayerRanking>, APIError> {
392        #[cfg(feature = "tracing")]
393        tracing::trace!("get_league_season_rankings({}, {}, {})", league_id, season_id, paging);
394        if league_id != leagues::LeagueKind::LegendLeague {
395            return Err(APIError::InvalidParameters(
396                "This league does not have seasons, only League::LegendLeague has seasons"
397                    .to_string(),
398            ));
399        }
400        let mut url =
401            format!("{}/leagues/{}/seasons/{season_id}", Self::BASE_URL, league_id as i32);
402        if paging.is_some() {
403            url = Url::parse_with_params(&url, paging.to_vec())?.to_string();
404        }
405        self.parse_json(self.get(url), false).await
406    }
407
408    /// # Errors
409    ///
410    /// This function will return an error if the request fails
411    pub async fn get_league(
412        &self,
413        league_id: leagues::LeagueKind,
414    ) -> Result<leagues::League, APIError> {
415        #[cfg(feature = "tracing")]
416        tracing::trace!("get_league({})", league_id);
417        let url = format!("{}/leagues/{}", Self::BASE_URL, league_id as i32);
418        self.parse_json(self.get(url), false).await
419    }
420
421    /// # Errors
422    ///
423    /// This function will return an error if the request fails
424    pub async fn get_league_seasons(
425        &self,
426        league_id: leagues::LeagueKind,
427    ) -> Result<APIResponse<season::Season>, APIError> {
428        #[cfg(feature = "tracing")]
429        tracing::trace!("get_league_seasons({})", league_id);
430        if league_id != leagues::LeagueKind::LegendLeague {
431            return Err(APIError::InvalidParameters(
432                "This league does not have seasons, only League::LegendLeague has seasons"
433                    .to_string(),
434            ));
435        }
436        let url = format!("{}/leagues/{}/seasons", Self::BASE_URL, league_id as i32);
437        self.parse_json(self.get(url), false).await
438    }
439
440    /// # Errors
441    ///
442    /// This function will return an error if the request fails
443    pub async fn get_war_league(
444        &self,
445        war_league: leagues::WarLeagueKind,
446    ) -> Result<leagues::WarLeague, APIError> {
447        #[cfg(feature = "tracing")]
448        tracing::trace!("get_war_league({})", war_league);
449        let url = format!("{}/warleagues/{}", Self::BASE_URL, war_league as i32);
450        self.parse_json(self.get(url), false).await
451    }
452
453    /// # Errors
454    ///
455    /// This function will return an error if the request fails
456    pub async fn get_war_leagues(&self) -> Result<APIResponse<leagues::WarLeague>, APIError> {
457        #[cfg(feature = "tracing")]
458        tracing::trace!("get_war_leagues()");
459        let url = format!("{}/warleagues", Self::BASE_URL);
460        self.parse_json(self.get(url), false).await
461    }
462
463    //         ╭──────────────────────────────────────────────────────────╮
464    //         │                     Location Methods                     │
465    //         ╰──────────────────────────────────────────────────────────╯
466
467    /// # Errors
468    ///
469    /// This function will return an error if the request fails
470    pub async fn get_clan_rankings(
471        &self,
472        location: location::Local,
473    ) -> Result<APIResponse<rankings::ClanRanking>, APIError> {
474        #[cfg(feature = "tracing")]
475        tracing::trace!("get_clan_rankings({})", location);
476        let url = format!("{}/locations/{}/rankings/clans", Self::BASE_URL, location as i32);
477        self.parse_json(self.get(url), false).await
478    }
479
480    /// # Errors
481    ///
482    /// This function will return an error if the request fails
483    pub async fn get_player_rankings(
484        &self,
485        location: location::Local,
486    ) -> Result<APIResponse<rankings::PlayerRanking>, APIError> {
487        #[cfg(feature = "tracing")]
488        tracing::trace!("get_player_rankings({})", location);
489        let url = format!("{}/locations/{}/rankings/players", Self::BASE_URL, location as i32);
490        self.parse_json(self.get(url), false).await
491    }
492
493    /// # Errors
494    ///
495    /// This function will return an error if the request fails
496    pub async fn get_versus_clan_rankings(
497        &self,
498        location: location::Local,
499    ) -> Result<APIResponse<rankings::ClanRanking>, APIError> {
500        #[cfg(feature = "tracing")]
501        tracing::trace!("get_versus_clan_rankings({})", location);
502        let url = format!("{}/locations/{}/rankings/clans-versus", Self::BASE_URL, location as i32);
503        self.parse_json(self.get(url), false).await
504    }
505
506    /// # Errors
507    ///
508    /// This function will return an error if the request fails
509    pub async fn get_versus_player_rankings(
510        &self,
511        location: location::Local,
512    ) -> Result<APIResponse<rankings::PlayerVersusRanking>, APIError> {
513        #[cfg(feature = "tracing")]
514        tracing::trace!("get_versus_player_rankings({})", location);
515        let url =
516            format!("{}/locations/{}/rankings/players-versus", Self::BASE_URL, location as i32);
517        self.parse_json(self.get(url), false).await
518    }
519
520    /// # Errors
521    ///
522    /// This function will return an error if the request fails
523    pub async fn get_locations(&self) -> Result<APIResponse<location::Location>, APIError> {
524        #[cfg(feature = "tracing")]
525        tracing::trace!("get_locations()");
526        let url = format!("{}/locations", Self::BASE_URL);
527        self.parse_json(self.get(url), false).await
528    }
529
530    /// # Errors
531    ///
532    /// This function will return an error if the request fails
533    pub async fn get_location(
534        &self,
535        location: location::Local,
536    ) -> Result<location::Location, APIError> {
537        #[cfg(feature = "tracing")]
538        tracing::trace!("get_location({})", location);
539        let url = format!("{}/locations/{}", Self::BASE_URL, location as i32);
540        self.parse_json(self.get(url), false).await
541    }
542
543    //         ╭──────────────────────────────────────────────────────────╮
544    //         │                     Gold Pass Method                     │
545    //         ╰──────────────────────────────────────────────────────────╯
546
547    /// # Errors
548    ///
549    /// This function will return an error if the request fails
550    pub async fn get_goldpass(&self) -> Result<gold_pass::GoldPass, APIError> {
551        #[cfg(feature = "tracing")]
552        tracing::trace!("get_goldpass()");
553        let url = format!("{}/goldpass/seasons/current", Self::BASE_URL);
554        self.parse_json(self.get(url), false).await
555    }
556
557    //         ╭──────────────────────────────────────────────────────────╮
558    //         │                      Label Methods                       │
559    //         ╰──────────────────────────────────────────────────────────╯
560
561    /// # Errors
562    ///
563    /// This function will return an error if the request fails.
564    pub async fn get_player_labels(&self) -> Result<APIResponse<labels::PlayerLabel>, APIError> {
565        #[cfg(feature = "tracing")]
566        tracing::trace!("get_player_labels()");
567        let url = format!("{}/labels/players", Self::BASE_URL);
568        self.parse_json(self.get(url), false).await
569    }
570
571    /// # Errors
572    ///
573    /// This function will return an error if the request fails.
574    pub async fn get_clan_labels(&self) -> Result<APIResponse<labels::ClanLabel>, APIError> {
575        #[cfg(feature = "tracing")]
576        tracing::trace!("get_clan_labels()");
577        let url = format!("{}/labels/clans", Self::BASE_URL);
578        self.parse_json(self.get(url), false).await
579    }
580
581    /// Runs the future that implements `Send` and parses the reqwest response into an
582    /// `APIResponse`.
583    ///
584    /// # Panics
585    ///
586    /// Panics if the JSON parsing fails for some odd reason. This is a bug and should be reported.
587    ///
588    /// # Errors
589    ///
590    /// This function will return an error if the request fails.
591    #[async_recursion]
592    pub(crate) async fn parse_json<T: DeserializeOwned>(
593        &self,
594        rb: Result<RequestBuilder, APIError>,
595        is_retry_and_not_cos: bool,
596    ) -> Result<T, APIError> {
597        match rb {
598            Ok(rb) => {
599                let cloned_rb = rb.try_clone();
600                match rb.send().await {
601                    Ok(resp) => {
602                        match resp.status() {
603                            reqwest::StatusCode::OK => {
604                                let text = resp.text().await?;
605                                Ok(serde_json::from_str(&text).unwrap_or_else(|e| panic!("Failure parsing json (please file a bug on the GitHub): {text}\nError: {e}")))
606                            }
607                            // 400
608                            reqwest::StatusCode::BAD_REQUEST => Err(APIError::BadParameters),
609                            // 403 - likely means the IP address has changed, let's reinit the
610                            // client then and try this again
611                            reqwest::StatusCode::FORBIDDEN => {
612                                if is_retry_and_not_cos {
613                                    #[cfg(feature = "tracing")]
614                                    tracing::debug!("403 Forbidden, but already retried, try checking your credentials?");
615                                    Err(APIError::AccessDenied)
616                                } else {
617                                    if let Err(e) = self.reinit().await {
618                                        return Err(APIError::LoginFailed(e.to_string()));
619                                    }
620                                    if let Some(rb) = cloned_rb {
621                                        self.parse_json(Ok(rb), true).await
622                                    } else {
623                                        Err(APIError::AccessDenied)
624                                    }
625                                }
626                            }
627                            // 404
628                            reqwest::StatusCode::NOT_FOUND => Err(APIError::NotFound),
629                            // 429
630                            reqwest::StatusCode::TOO_MANY_REQUESTS => {
631                                Err(APIError::RequestThrottled)
632                            }
633                            // 500
634                            reqwest::StatusCode::INTERNAL_SERVER_ERROR => {
635                                Err(APIError::UnknownError)
636                            }
637                            // 503
638                            reqwest::StatusCode::SERVICE_UNAVAILABLE => {
639                                Err(APIError::InMaintenance)
640                            }
641                            // edge cases
642                            _ => {
643                                let status = resp.status();
644                                #[cfg(feature = "tracing")]
645                                tracing::debug!("Unknown status code: {}", status);
646                                Err(APIError::BadResponse(resp.text().await?, status))
647                            }
648                        }
649                    }
650                    Err(e) => Err(APIError::RequestFailed(e)),
651                }
652            }
653            Err(e) => Err(e),
654        }
655    }
656
657    fn get_next_key(&self) -> String {
658        // increment key_token_index, unless it would be larger than the account's token size (10),
659        // then reset to 0 and increment key_account_index
660
661        let mut account_index = self.account_index.load(Ordering::Relaxed);
662        let mut key_index = self.key_index.load(Ordering::Relaxed);
663
664        let accounts = self.accounts.iter().collect::<Vec<_>>();
665        let size_of_keys = accounts[account_index].keys.len().min(10);
666
667        // if we're at the end of this account's keys..
668        if key_index == size_of_keys - 1 {
669            // reset token index anyways
670            key_index = 0;
671            // ..and at the end of the accounts
672            if account_index == (accounts.len() - 1) {
673                // then we've reached end of accounts, go back to first account
674                account_index = 0;
675            } else {
676                // otherwise, just increment account index
677                account_index += 1;
678            }
679        } else {
680            // otherwise, just increment token index
681            key_index += 1;
682        }
683
684        let token = accounts
685            .get(account_index)
686            .unwrap_or_else(|| {
687                #[cfg(feature = "tracing")]
688                tracing::warn!("No account found at index {account_index}");
689                panic!("No account found at index {account_index}")
690            })
691            .keys
692            .keys
693            .get(key_index)
694            .unwrap_or_else(|| {
695                #[cfg(feature = "tracing")]
696                tracing::warn!("No key found at index {key_index}");
697                panic!("No key found at index {key_index}");
698            })
699            .clone();
700
701        self.account_index.store(account_index, Ordering::Relaxed);
702        self.key_index.store(key_index, Ordering::Relaxed);
703
704        token.key
705    }
706}
707
708#[derive(Debug, Serialize, Deserialize)]
709#[serde(rename_all = "camelCase")]
710pub struct APIResponse<T> {
711    pub items: Vec<T>,
712    pub paging: paging::Paging,
713}