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 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 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 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 self.accounts.insert(account.credential.clone(), account);
98 }
99
100 self.ready.store(true, Ordering::Relaxed);
101
102 Ok(())
103 }
104
105 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 #[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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 reqwest::StatusCode::BAD_REQUEST => Err(APIError::BadParameters),
609 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 reqwest::StatusCode::NOT_FOUND => Err(APIError::NotFound),
629 reqwest::StatusCode::TOO_MANY_REQUESTS => {
631 Err(APIError::RequestThrottled)
632 }
633 reqwest::StatusCode::INTERNAL_SERVER_ERROR => {
635 Err(APIError::UnknownError)
636 }
637 reqwest::StatusCode::SERVICE_UNAVAILABLE => {
639 Err(APIError::InMaintenance)
640 }
641 _ => {
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 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 key_index == size_of_keys - 1 {
669 key_index = 0;
671 if account_index == (accounts.len() - 1) {
673 account_index = 0;
675 } else {
676 account_index += 1;
678 }
679 } else {
680 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}