nadeo_api_rs/
live.rs

1// API calls for the Live API
2
3use ahash::{HashMap, HashMapExt};
4use log::warn;
5use serde::de::{self, MapAccess, SeqAccess, Visitor};
6use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::json;
8use std::{fmt, marker::PhantomData, num::NonZero, sync::OnceLock, vec};
9use tokio::sync::{oneshot, RwLock};
10
11use crate::auth::LoginError;
12use crate::{auth::NadeoClient, client::*};
13
14/// Returns query params for length and offset
15fn query_lo(length: u32, offset: u32) -> Vec<(&'static str, String)> {
16    vec![
17        ("length", length.to_string()),
18        ("offset", offset.to_string()),
19    ]
20}
21
22#[derive(Debug)]
23pub enum NadeoError {
24    ArgsErr(String),
25    ReqwestError(reqwest::Error),
26    SerdeJsonWithURL(serde_json::Error, String),
27    SerdeJson(serde_json::Error),
28    Login(LoginError),
29}
30
31impl From<reqwest::Error> for NadeoError {
32    fn from(e: reqwest::Error) -> Self {
33        NadeoError::ReqwestError(e)
34    }
35}
36
37impl From<serde_json::Error> for NadeoError {
38    fn from(e: serde_json::Error) -> Self {
39        NadeoError::SerdeJson(e)
40    }
41}
42
43impl From<LoginError> for NadeoError {
44    fn from(e: LoginError) -> Self {
45        NadeoError::Login(e)
46    }
47}
48
49/// API calls for the Live API
50pub trait LiveApiClient: NadeoApiClient {
51    /// Get TOTDs / Royal maps
52    async fn get_monthly_campaign(
53        &self,
54        ty: MonthlyCampaignType,
55        length: u32,
56        offset: u32,
57    ) -> Result<MonthlyCampaign_List, NadeoError> {
58        let mut query: Vec<(&str, String)> = query_lo(length, offset);
59        if matches!(ty, MonthlyCampaignType::Royal) {
60            query.push(("royal", "true".to_string()));
61        }
62        let (rb, permit) = self.live_get("api/token/campaign/month").await;
63        let j: Value = rb.query(&query).send().await?.json().await?;
64        drop(permit);
65        Ok(serde_json::from_value(j)?)
66    }
67
68    ///
69    /// <https://webservices.openplanet.dev/live/leaderboards/top>
70    ///
71    /// calls `api/token/leaderboard/group/{groupUid}/map/{mapUid}/top`
72    async fn get_map_group_leaderboard(
73        &self,
74        group_uid: &str,
75        map_uid: &str,
76        only_world: bool,
77        length: u32,
78        offset: u32,
79    ) -> Result<MapGroupLeaderboard, NadeoError> {
80        let mut query = query_lo(length, offset);
81        query.push(("onlyWorld", only_world.to_string()));
82        let (rb, permit) = self
83            .live_get(&format!(
84                "api/token/leaderboard/group/{group_uid}/map/{map_uid}/top"
85            ))
86            .await;
87        let j: Value = rb.query(&query).send().await?.json().await?;
88        drop(permit);
89        Ok(serde_json::from_value(j)?)
90    }
91
92    /// Personal_Best LB
93    ///
94    /// <https://webservices.openplanet.dev/live/leaderboards/top>
95    ///
96    /// calls `api/token/leaderboard/group/Personal_Best/map/{mapUid}/top`
97    async fn get_map_leaderboard(
98        &self,
99        map_uid: &str,
100        only_world: bool,
101        length: u32,
102        offset: u32,
103    ) -> Result<MapGroupLeaderboard, NadeoError> {
104        self.get_map_group_leaderboard("Personal_Best", map_uid, only_world, length, offset)
105            .await
106    }
107
108    /// <https://webservices.openplanet.dev/live/leaderboards/position>
109    ///
110    /// Note: different groups are supported by the API but not this method.
111    ///
112    /// Warning: duplicate uids with different scores is not supported by the API.
113    ///
114    /// calls `api/token/leaderboard/group/map?scores[{mapUid}]={score}`
115    async fn get_lb_positions_by_time(
116        &self,
117        uid_scores: &[(&str, NonZero<u32>)],
118    ) -> Result<Vec<RecordsByTime>, NadeoError> {
119        self.get_lb_positions_by_time_group(uid_scores, "Personal_Best")
120            .await
121    }
122
123    /// <https://webservices.openplanet.dev/live/leaderboards/position>
124    ///
125    /// When using a different groupUid, make sure you're only referencing currently open leaderboards. Maps with closed leaderboards will not be included in the response.
126    ///
127    /// Warning: duplicate uids with different scores is not supported by the API.
128    ///
129    /// calls `api/token/leaderboard/group/map?scores[{mapUid}]={score}`
130    async fn get_lb_positions_by_time_group(
131        &self,
132        uid_scores: &[(&str, NonZero<u32>)],
133        group_uid: &str,
134    ) -> Result<Vec<RecordsByTime>, NadeoError> {
135        let mut query = vec![];
136        for (uid, score) in uid_scores.iter() {
137            query.push((format!("scores[{}]", uid), score.get().to_string()));
138        }
139
140        let mut body_maps = vec![];
141        for (uid, _) in uid_scores.iter() {
142            body_maps.push(json!({
143                "mapUid": uid,
144                "groupUid": group_uid,
145            }));
146        }
147        let body = json!({ "maps": body_maps });
148
149        let (rb, permit) = self
150            .live_post("api/token/leaderboard/group/map", &body)
151            .await;
152        let j: Value = rb.query(&query).send().await?.json().await?;
153        drop(permit);
154        Ok(serde_json::from_value(j)?)
155    }
156
157    /// <https://webservices.openplanet.dev/live/leaderboards/position>
158    ///
159    /// Uses `get_lb_positions_by_time` with an async batching system
160    /// to get the position on the leaderboard for a given score.
161    /// It is highly efficient in terms of API calls provided the calls are initiated within a short (20ms) time frame.
162    async fn get_lb_position_by_time_batched(
163        &'static self,
164        map_uid: &str,
165        score: NonZero<u32>,
166    ) -> Result<ScoreToPos, oneshot::error::RecvError> {
167        if score.get() >= 2147483648 {
168            warn!("score >= 2147483648: {}", map_uid);
169        }
170        let ret_chan = self
171            .push_rec_position_req(map_uid, score.get() as i64)
172            .await;
173        Ok(ret_chan.await?)
174    }
175
176    /// Internal method supporting `get_lb_position_by_time_batched`
177    async fn push_rec_position_req(
178        &'static self,
179        map_uid: &str,
180        score: i64,
181    ) -> oneshot::Receiver<ScoreToPos>;
182
183    /// <https://webservices.openplanet.dev/live/leaderboards/surround>
184    ///
185    /// calls `api/token/leaderboard/group/{groupUid}/map/{mapUid}/surround/{lower}/{upper}?score={score}&onlyWorld={onlyWorld}`
186    async fn get_group_surround(
187        &self,
188        group_uid: &str,
189        map_uid: &str,
190        lower: i32,
191        upper: i32,
192        score: u32,
193        only_world: bool,
194    ) -> Result<RecordsSurround, NadeoError> {
195        let (rb, permit) = self
196            .live_get(&format!(
197                "api/token/leaderboard/group/{group_uid}/map/{map_uid}/surround/{lower}/{upper}"
198            ))
199            .await;
200        let j: Value = rb
201            .query(&[
202                ("score", score.to_string()),
203                ("onlyWorld", only_world.to_string()),
204            ])
205            .send()
206            .await?
207            .json()
208            .await?;
209        drop(permit);
210        Ok(serde_json::from_value(j)?)
211    }
212
213    /// Surround on the Personal_Best group
214    ///
215    /// <https://webservices.openplanet.dev/live/leaderboards/surround>
216    ///
217    /// calls `api/token/leaderboard/group/Personal_Best/map/{mapUid}/surround/{lower}/{upper}?score={score}`
218    async fn get_pb_surround(
219        &self,
220        map_uid: &str,
221        lower: i32,
222        upper: i32,
223        score: u32,
224        only_world: bool,
225    ) -> Result<RecordsSurround, NadeoError> {
226        self.get_group_surround("Personal_Best", map_uid, lower, upper, score, only_world)
227            .await
228    }
229
230    /// <https://webservices.openplanet.dev/live/maps/info>
231    ///
232    /// calls `api/token/map/{mapUid}`
233    ///
234    /// Returns `None` if the map isn't uploaded
235    async fn get_map_info(&self, map_uid: &str) -> Result<Option<MapInfo>, NadeoError> {
236        let (rb, permit) = self.live_get(&format!("api/token/map/{map_uid}")).await;
237        let resp = rb.send().await?;
238        if resp.status().as_u16() == 404 {
239            drop(permit);
240            return Ok(None);
241        }
242        let j: Value = resp.json().await?;
243        drop(permit);
244        Ok(serde_json::from_value(j)?)
245    }
246
247    /// <https://webservices.openplanet.dev/live/maps/info-multiple>
248    ///
249    /// calls `api/token/map/get-multiple?mapUidList={mapUidList}`
250    async fn get_map_info_multiple(&self, map_uids: &[&str]) -> Result<MapInfos, NadeoError> {
251        if map_uids.len() > 100 {
252            return Err(NadeoError::ArgsErr("map_uids length must be <= 100".into()));
253        }
254        let url = "api/token/map/get-multiple?mapUidList=".to_string() + &map_uids.join(",");
255        let j = self.run_live_get(&url).await?;
256        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
257    }
258
259    /// <https://webservices.openplanet.dev/live/clubs/activities>
260    ///
261    /// calls `/api/token/club/{clubId}/activity?length=3&offset=0&active=true`
262    async fn get_club_activities(
263        &self,
264        club_id: i32,
265        length: u32,
266        offset: u32,
267        active: bool,
268    ) -> Result<ClubActivityList, NadeoError> {
269        let mut query = query_lo(length, offset);
270        query.push(("active", active.to_string()));
271        let url = format!("api/token/club/{}/activity", club_id);
272        let (rb, permit) = self.live_get(&url).await;
273        let j: Value = rb.query(&query).send().await?.json().await?;
274        drop(permit);
275        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
276    }
277
278    /// <https://webservices.openplanet.dev/live/clubs/club>
279    ///
280    /// calls `/api/token/club/{clubId}`
281    async fn get_club_info(&self, club_id: i32) -> Result<ClubInfo, NadeoError> {
282        let url = format!("api/token/club/{}", club_id);
283        let (rb, permit) = self.live_get(&url).await;
284        let j: Value = rb.send().await?.json().await?;
285        drop(permit);
286        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
287    }
288
289    /// <https://webservices.openplanet.dev/live/clubs/campaign-by-id>
290    ///
291    /// calls `/api/token/club/{clubId}/campaign/{campaignId}`
292    async fn get_club_campaign_by_id(
293        &self,
294        club_id: i32,
295        campaign_id: i32,
296    ) -> Result<ClubCampaignById, NadeoError> {
297        let url = format!("api/token/club/{}/campaign/{}", club_id, campaign_id);
298        let (rb, permit) = self.live_get(&url).await;
299        let j: Value = rb.send().await?.json().await?;
300        eprintln!("{:?}", j);
301        drop(permit);
302        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
303    }
304
305    /// <https://webservices.openplanet.dev/live/clubs/campaigns>
306    ///
307    /// calls `api/token/club/campaign?length={length}&offset={offset}&name={name}`
308    async fn get_club_campaigns(
309        &self,
310        // club_id: i32,
311        length: u32,
312        offset: u32,
313        name: Option<&str>,
314    ) -> Result<ClubCampaignList, NadeoError> {
315        let mut query = query_lo(length, offset);
316        if let Some(name) = name {
317            query.push(("name", name.to_string()));
318        }
319        let url = format!("api/token/club/campaign");
320        let (rb, permit) = self.live_get(&url).await;
321        let j: Value = rb.query(&query).send().await?.json().await?;
322        drop(permit);
323        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
324    }
325
326    /// <https://webservices.openplanet.dev/live/clubs/rooms>
327    ///
328    /// calls `api/token/club/room?length={length}&offset={offset}&name={name}`
329    async fn get_club_rooms(
330        &self,
331        length: u32,
332        offset: u32,
333        name: Option<&str>,
334    ) -> Result<ClubRoomList, NadeoError> {
335        let mut query = query_lo(length, offset);
336        if let Some(name) = name {
337            query.push(("name", name.to_string()));
338        }
339        let url = format!("api/token/club/room");
340        let (rb, permit) = self.live_get(&url).await;
341        let j: Value = rb.query(&query).send().await?.json().await?;
342        drop(permit);
343        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
344    }
345
346    /// <https://webservices.openplanet.dev/live/clubs/room-by-id>
347    ///
348    /// calls `/api/token/club/{clubId}/room/{roomId}`
349    async fn get_club_room_by_id(
350        &self,
351        club_id: i32,
352        activity_id: i32,
353    ) -> Result<ClubRoom, NadeoError> {
354        let url = format!("api/token/club/{}/room/{}", club_id, activity_id);
355        let (rb, permit) = self.live_get(&url).await;
356        let j: Value = rb.send().await?.json().await?;
357        drop(permit);
358        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
359    }
360
361    async fn edit_club_room_by_id(
362        &self,
363        club_id: i32,
364        activity_id: i32,
365        body: &ClubRoom_Room_ForEdit,
366    ) -> Result<ClubRoom, NadeoError> {
367        let url = format!("api/token/club/{}/room/{}/edit", club_id, activity_id);
368        let body =
369            serde_json::to_value(body).map_err(|e| NadeoError::SerdeJsonWithURL(e, url.clone()))?;
370        let (rb, permit) = self.live_post(&url, &body).await;
371        let j: Value = rb.send().await?.json().await?;
372        drop(permit);
373        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
374    }
375
376    async fn create_club_room(
377        &self,
378        club_id: i32,
379        body: &ClubRoom_Room_ForEdit,
380    ) -> Result<ClubRoom, NadeoError> {
381        let url = format!("api/token/club/{}/room/create", club_id);
382        let body =
383            serde_json::to_value(body).map_err(|e| NadeoError::SerdeJsonWithURL(e, url.clone()))?;
384        let (rb, permit) = self.live_post(&url, &body).await;
385        let j: Value = rb.send().await?.json().await?;
386        drop(permit);
387        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
388    }
389
390    /// <https://webservices.openplanet.dev/live/clubs/activities>
391    ///
392    /// calls `/api/token/club/{clubId}/activity?length={length}&offset={offset}&active={active}`
393    ///
394    /// `active` can only be None if the account is an admin of the club; must be Some(true) if not a member.
395    async fn get_club_activity_list(
396        &self,
397        club_id: i32,
398        length: u32,
399        offset: u32,
400        active: Option<bool>,
401    ) -> Result<ActivityList, NadeoError> {
402        let mut query = query_lo(length, offset);
403        if let Some(active) = active {
404            query.push(("active", active.to_string()));
405        }
406        let url = format!("api/token/club/{}/activity", club_id);
407        let (rb, permit) = self.live_get(&url).await;
408        let j: Value = rb.query(&query).send().await?.json().await?;
409        drop(permit);
410        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
411    }
412
413    async fn edit_club_activity(
414        &self,
415        club_id: i32,
416        activity_id: i32,
417        public: Option<bool>,
418        active: Option<bool>,
419    ) -> Result<Value, NadeoError> {
420        let url = format!("api/token/club/{}/activity/{}/edit", club_id, activity_id);
421        let mut body = serde_json::Value::Object(Default::default());
422        if let Some(public) = public {
423            body["public"] = serde_json::Value::Bool(public);
424        }
425        if let Some(active) = active {
426            body["active"] = serde_json::Value::Bool(active);
427        }
428        let (rb, permit) = self.live_post(&url, &body).await;
429        let j: Value = rb.send().await?.json().await?;
430        drop(permit);
431        Ok(j)
432    }
433
434    /// <https://webservices.openplanet.dev/live/clubs/clubs>
435    ///
436    /// calls `/api/token/club?length={length}&offset={offset}&name={name}`
437    async fn get_clubs(
438        &self,
439        length: u32,
440        offset: u32,
441        name: Option<&str>,
442    ) -> Result<ClubList, NadeoError> {
443        let mut query = query_lo(length, offset);
444        if let Some(name) = name {
445            query.push(("name", name.to_string()));
446        }
447        let url = format!("api/token/club");
448        let (rb, permit) = self.live_get(&url).await;
449        let j: Value = rb.query(&query).send().await?.json().await?;
450        drop(permit);
451        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
452    }
453
454    async fn get_clubs_mine(&self, length: u32, offset: u32) -> Result<ClubList, NadeoError> {
455        let query = query_lo(length, offset);
456        let url = format!("api/token/club/mine");
457        let (rb, permit) = self.live_get(&url).await;
458        let j: Value = rb.query(&query).send().await?.json().await?;
459        drop(permit);
460        Ok(serde_json::from_value(j).map_err(|e| NadeoError::SerdeJsonWithURL(e, url))?)
461    }
462}
463
464impl LiveApiClient for NadeoClient {
465    async fn push_rec_position_req(
466        &'static self,
467        map_uid: &str,
468        score: i64,
469    ) -> oneshot::Receiver<ScoreToPos> {
470        let (tx, rx) = oneshot::channel();
471        self.batcher_lb_pos_by_time.push(map_uid, score, tx).await;
472        self.check_start_batcher_lb_pos_by_time_loop().await;
473        rx
474    }
475}
476
477// MARK: BatcherLbPosByTime
478
479/// High performance batcher for getting leaderboard positions by time.
480/// Should be used via [LiveApiClient::get_lb_position_by_time_batched].
481pub struct BatcherLbPosByTime {
482    queued: RwLock<BatcherLbPosByTimeQueue>,
483    loop_started: OnceLock<bool>,
484}
485
486impl BatcherLbPosByTime {
487    pub fn new() -> Self {
488        Self {
489            queued: RwLock::new(BatcherLbPosByTimeQueue::new()),
490            loop_started: OnceLock::new(),
491        }
492    }
493
494    pub async fn push(&self, map_uid: &str, score: i64, ret_chan: oneshot::Sender<ScoreToPos>) {
495        let mut q = self.queued.write().await;
496        q.push(map_uid, score, ret_chan);
497    }
498
499    pub fn has_loop_started(&self) -> bool {
500        self.loop_started.get().is_some()
501    }
502
503    pub fn set_loop_started(&self) -> Result<(), bool> {
504        self.loop_started.set(true)
505    }
506
507    pub async fn is_empty(&self) -> bool {
508        self.queued.read().await.nb_queued == 0
509    }
510
511    pub async fn nb_queued(&self) -> usize {
512        self.queued.read().await.nb_queued
513    }
514
515    pub async fn nb_in_progress(&self) -> usize {
516        self.queued.read().await.nb_in_progress
517    }
518
519    pub async fn get_batch_size_avg(&self) -> (f64, usize) {
520        let q = self.queued.read().await;
521        (q.avg_batch_size, q.nb_batches)
522    }
523
524    pub async fn run_batch<T: LiveApiClient>(&self, api: &T) -> Result<Vec<String>, NadeoError> {
525        // L1 start: queued -> in progress
526        let mut q = self.queued.write().await;
527        let batch = q.take_up_to(50);
528        // let uids = batch
529        //     .iter()
530        //     .map(|(uid, _, _)| uid.clone())
531        //     .collect::<HashSet<String>>();
532
533        let batch_size = batch.len();
534        q.nb_in_progress += batch_size;
535        drop(q);
536        // L1 end
537
538        // Do the batch
539        let uid_scores: Vec<(&str, _)> = batch
540            .iter()
541            .map(|(uid, score, _)| (uid.as_str(), NonZero::new(*score as u32).unwrap()))
542            .collect();
543        let resp = api.get_lb_positions_by_time(&uid_scores).await?;
544        if resp.len() != batch_size {
545            warn!(
546                "[BatcherLbPosByTime] resp.len() != batch_size: {} != {}",
547                resp.len(),
548                batch_size
549            );
550        }
551        // let b_lookup = batch.into_iter().map(|(uid, time, sender)| (uid, (time, sender))).collect::<HashMap<String, (i32, oneshot::Sender<Option<i32>>)>>();
552        let r_lookup = resp
553            .into_iter()
554            .filter_map(|r| {
555                let pos = r.get_global_pos()?;
556                Some((r.mapUid, pos))
557            })
558            .collect::<HashMap<String, i32>>();
559
560        let mut uids = vec![];
561        for (uid, score, sender) in batch {
562            let pos = r_lookup.get(&uid).copied();
563            let _ = sender.send(ScoreToPos { score, pos });
564            uids.push(uid);
565        }
566
567        // L2 start: in progress -> done
568        let mut q = self.queued.write().await;
569        q.nb_in_progress -= batch_size;
570        drop(q);
571        // L2 end
572        Ok(uids)
573    }
574}
575
576#[derive(Debug, Clone)]
577pub struct ScoreToPos {
578    pub score: i64,
579    pub pos: Option<i32>,
580}
581impl ScoreToPos {
582    pub fn get_s_p(&self) -> (i64, Option<i32>) {
583        (self.score, self.pos)
584    }
585}
586
587pub struct BatcherLbPosByTimeQueue {
588    pub queue: HashMap<String, Vec<(i64, oneshot::Sender<ScoreToPos>)>>,
589    pub nb_queued: usize,
590    pub nb_in_progress: usize,
591    pub avg_batch_size: f64,
592    pub nb_batches: usize,
593}
594
595impl BatcherLbPosByTimeQueue {
596    pub fn new() -> Self {
597        Self {
598            queue: HashMap::new(),
599            nb_queued: 0,
600            nb_in_progress: 0,
601            avg_batch_size: 0.0,
602            nb_batches: 0,
603        }
604    }
605
606    pub fn push(&mut self, map_uid: &str, score: i64, ret_chan: oneshot::Sender<ScoreToPos>) {
607        self.nb_queued += 1;
608        self.queue
609            .entry(map_uid.to_string())
610            .or_insert_with(Vec::new)
611            .push((score, ret_chan));
612    }
613
614    pub fn take_up_to(&mut self, limit: usize) -> Vec<(String, i64, oneshot::Sender<ScoreToPos>)> {
615        let mut ret = vec![];
616        let mut to_rem = vec![];
617        for (map_uid, v) in self.queue.iter_mut() {
618            match v.pop() {
619                Some((score, ret_chan)) => {
620                    self.nb_queued -= 1;
621                    ret.push((map_uid.clone(), score, ret_chan));
622                }
623                None => {}
624            }
625            if v.is_empty() {
626                to_rem.push(map_uid.clone());
627            }
628            if ret.len() >= limit {
629                break;
630            }
631        }
632        for k in to_rem {
633            let v = self.queue.remove(&k);
634            if let Some(v) = v {
635                self.nb_queued -= v.len();
636                if v.len() > 0 {
637                    panic!("v.len() > 0: {}: {:?}", k, v);
638                }
639            }
640        }
641        self.update_avg_batch_size(ret.len());
642        ret
643    }
644
645    fn update_avg_batch_size(&mut self, batch_size: usize) {
646        self.nb_batches += 1;
647        self.avg_batch_size = (self.avg_batch_size * (self.nb_batches - 1) as f64
648            + batch_size as f64)
649            / self.nb_batches as f64;
650    }
651}
652
653// MARK: Resp Types
654
655#[derive(Debug, Clone, Serialize, Deserialize)]
656#[allow(non_camel_case_types, non_snake_case)]
657pub struct MapInfos {
658    pub mapList: Vec<MapInfo>,
659    pub itemCount: i32,
660}
661
662/// get_map_info response types
663#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
664#[allow(non_camel_case_types, non_snake_case)]
665pub struct MapInfo {
666    pub uid: String,
667    pub mapId: String,
668    pub name: String,
669    pub author: String,
670    pub submitter: String,
671    pub authorTime: i64,
672    pub goldTime: i64,
673    pub silverTime: i64,
674    pub bronzeTime: i64,
675    pub nbLaps: i32,
676    pub valid: bool,
677    pub downloadUrl: String,
678    pub thumbnailUrl: String,
679    pub uploadTimestamp: i64,
680    pub updateTimestamp: i64,
681    pub fileSize: Option<i32>,
682    pub public: bool,
683    pub favorite: bool,
684    pub playable: bool,
685    /// usually blank or same as mapType
686    pub mapStyle: String,
687    /// Typically "TrackMania\\TM_Race"
688    pub mapType: String,
689    pub collectionName: String,
690}
691
692/// get_group_surround response types
693#[derive(Debug, Clone, Serialize, Deserialize)]
694#[allow(non_camel_case_types, non_snake_case)]
695pub struct RecordsSurround {
696    pub groupUid: String,
697    pub mapUid: String,
698    pub tops: Vec<RecordsSurround_Top>,
699}
700
701#[derive(Debug, Clone, Serialize, Deserialize)]
702#[allow(non_camel_case_types, non_snake_case)]
703pub struct RecordsSurround_Top {
704    pub zoneId: String,
705    pub zoneName: String,
706    pub top: Vec<RecordsSurround_TopEntry>,
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
710#[allow(non_camel_case_types, non_snake_case)]
711pub struct RecordsSurround_TopEntry {
712    pub accountId: String,
713    pub zoneId: String,
714    pub zoneName: String,
715    pub position: i32,
716    pub score: u32,
717    pub timestamp: Option<i64>,
718}
719
720/// get_lb_positions_by_time response types
721#[derive(Debug, Clone, Serialize, Deserialize)]
722#[allow(non_camel_case_types, non_snake_case)]
723pub struct RecordsByTime {
724    pub groupUid: String,
725    pub mapUid: String,
726    pub score: i64,
727    pub zones: Vec<RecordsByTime_Zone>,
728}
729
730impl RecordsByTime {
731    pub fn get_global_pos(&self) -> Option<i32> {
732        Some(self.zones.get(0)?.ranking.position)
733    }
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
737#[allow(non_camel_case_types, non_snake_case)]
738pub struct RecordsByTime_Zone {
739    pub zoneId: String,
740    pub zoneName: String,
741    pub ranking: RecordsByTime_ZoneRanking,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
745#[allow(non_camel_case_types, non_snake_case)]
746pub struct RecordsByTime_ZoneRanking {
747    pub position: i32,
748    pub length: i32,
749}
750
751/// Map Group Leaderboard response types
752#[derive(Debug, Clone, Serialize, Deserialize)]
753#[allow(non_camel_case_types, non_snake_case)]
754pub struct MapGroupLeaderboard {
755    pub groupUid: String,
756    pub mapUid: String,
757    /// If the map doesn't exist, this has `top` as an empty array for zone World
758    pub tops: Vec<MapGroupLeaderboard_Top>,
759}
760
761#[derive(Debug, Clone, Serialize, Deserialize)]
762#[allow(non_camel_case_types, non_snake_case)]
763pub struct MapGroupLeaderboard_Top {
764    pub zoneId: String,
765    pub zoneName: String,
766    pub top: Vec<MapGroupLeaderboard_TopEntry>,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
770#[allow(non_camel_case_types, non_snake_case)]
771pub struct MapGroupLeaderboard_TopEntry {
772    pub accountId: String,
773    pub zoneId: String,
774    pub zoneName: String,
775    pub position: i32,
776    /// Can be negative if map has secret records
777    pub score: i64,
778}
779
780/// TOTD/Royal response types
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[allow(non_camel_case_types, non_snake_case)]
783pub struct MonthlyCampaign_List {
784    pub monthList: Vec<MonthlyCampaign_Month>,
785    pub itemCount: i32,
786    pub nextRequestTimestamp: i64,
787    pub relativeNextRequest: i64,
788}
789
790#[derive(Debug, Clone, Serialize, Deserialize)]
791#[allow(non_camel_case_types, non_snake_case)]
792pub struct MonthlyCampaign_Month {
793    pub year: i32,
794    pub month: i32,
795    pub lastDay: i32,
796    pub days: Vec<MonthlyCampaign_Day>,
797    pub media: MonthlyCampaign_Media,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
801#[allow(non_camel_case_types, non_snake_case)]
802pub struct MonthlyCampaign_Day {
803    pub campaignId: i32,
804    pub mapUid: String,
805    pub day: i32,
806    pub monthDay: i32,
807    pub seasonUid: String,
808    pub leaderboardGroup: Option<String>,
809    pub startTimestamp: i64,
810    pub endTimestamp: i64,
811    pub relativeStart: i64,
812    pub relativeEnd: i64,
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize)]
816#[allow(non_camel_case_types, non_snake_case)]
817pub struct MonthlyCampaign_Media {
818    pub buttonBackgroundUrl: String,
819    pub buttonForegroundUrl: String,
820    pub decalUrl: String,
821    pub popUpBackgroundUrl: String,
822    pub popUpImageUrl: String,
823    pub liveButtonBackgroundUrl: String,
824    pub liveButtonForegroundUrl: String,
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize)]
828#[allow(non_camel_case_types, non_snake_case)]
829pub struct ClubActivityList {
830    pub activityList: Vec<ClubActivity>,
831    pub maxPage: i32,
832    pub itemCount: i32,
833}
834
835#[derive(Debug, Clone, Serialize, Deserialize)]
836#[allow(non_camel_case_types, non_snake_case)]
837pub struct ClubActivity {
838    pub id: i32,
839    pub name: String,
840    pub activityType: String,
841    pub activityId: i32,
842    pub targetActivityId: i32,
843    pub campaignId: i32,
844    pub position: i32,
845    pub public: bool,
846    pub active: bool,
847    pub externalId: i32,
848    pub featured: bool,
849    pub password: bool,
850    pub itemsCount: i32,
851    pub clubId: i32,
852    pub editionTimestamp: i64,
853    pub creatorAccountId: String,
854    pub latestEditorAccountId: String,
855    pub mediaUrl: String,
856    pub mediaUrlPngLarge: String,
857    pub mediaUrlPngMedium: String,
858    pub mediaUrlPngSmall: String,
859    pub mediaUrlDds: String,
860    pub mediaTheme: String,
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize)]
864#[allow(non_camel_case_types, non_snake_case)]
865pub struct ClubInfo {
866    pub id: i32,
867    pub name: String,
868    pub tag: String,
869    pub description: String,
870    pub authorAccountId: String,
871    pub latestEditorAccountId: String,
872    pub iconUrl: String,
873    pub iconUrlPngLarge: String,
874    pub iconUrlPngMedium: String,
875    pub iconUrlPngSmall: String,
876    pub iconUrlDds: String,
877    pub logoUrl: String,
878    pub decalUrl: String,
879    pub decalUrlPngLarge: String,
880    pub decalUrlPngMedium: String,
881    pub decalUrlPngSmall: String,
882    pub decalUrlDds: String,
883    pub screen16x9Url: String,
884    pub screen16x9UrlPngLarge: String,
885    pub screen16x9UrlPngMedium: String,
886    pub screen16x9UrlPngSmall: String,
887    pub screen16x9UrlDds: String,
888    pub screen64x41Url: String,
889    pub screen64x41UrlPngLarge: String,
890    pub screen64x41UrlPngMedium: String,
891    pub screen64x41UrlPngSmall: String,
892    pub screen64x41UrlDds: String,
893    pub decalSponsor4x1Url: String,
894    pub decalSponsor4x1UrlPngLarge: String,
895    pub decalSponsor4x1UrlPngMedium: String,
896    pub decalSponsor4x1UrlPngSmall: String,
897    pub decalSponsor4x1UrlDds: String,
898    pub screen8x1Url: String,
899    pub screen8x1UrlPngLarge: String,
900    pub screen8x1UrlPngMedium: String,
901    pub screen8x1UrlPngSmall: String,
902    pub screen8x1UrlDds: String,
903    pub screen16x1Url: String,
904    pub screen16x1UrlPngLarge: String,
905    pub screen16x1UrlPngMedium: String,
906    pub screen16x1UrlPngSmall: String,
907    pub screen16x1UrlDds: String,
908    pub verticalUrl: String,
909    pub verticalUrlPngLarge: String,
910    pub verticalUrlPngMedium: String,
911    pub verticalUrlPngSmall: String,
912    pub verticalUrlDds: String,
913    pub backgroundUrl: String,
914    pub backgroundUrlJpgLarge: String,
915    pub backgroundUrlJpgMedium: String,
916    pub backgroundUrlJpgSmall: String,
917    pub backgroundUrlDds: String,
918    pub creationTimestamp: i64,
919    pub popularityLevel: i32,
920    pub state: String,
921    pub featured: bool,
922    pub walletUid: String,
923    pub metadata: String,
924    pub editionTimestamp: i64,
925    pub iconTheme: String,
926    pub decalTheme: String,
927    pub screen16x9Theme: String,
928    pub screen64x41Theme: String,
929    pub screen8x1Theme: String,
930    pub screen16x1Theme: String,
931    pub verticalTheme: String,
932    pub backgroundTheme: String,
933    pub verified: bool,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937#[allow(non_camel_case_types, non_snake_case)]
938pub struct ClubCampaignById {
939    pub clubDecalUrl: String,
940    pub campaignId: i32,
941    pub activityId: i32,
942    pub campaign: ClubCampaign,
943    pub popularityLevel: i32,
944    pub publicationTimestamp: i64,
945    pub creationTimestamp: i64,
946    pub creatorAccountId: String,
947    pub latestEditorAccountId: String,
948    pub id: i32,
949    pub clubId: i32,
950    pub clubName: String,
951    pub name: String,
952    pub mapsCount: i32,
953    pub mediaUrl: String,
954    pub mediaUrlPngLarge: String,
955    pub mediaUrlPngMedium: String,
956    pub mediaUrlPngSmall: String,
957    pub mediaUrlDds: String,
958    pub mediaTheme: String,
959}
960
961#[derive(Debug, Clone, Serialize, Deserialize)]
962#[allow(non_camel_case_types, non_snake_case)]
963pub struct ClubCampaign {
964    pub id: i32,
965    pub seasonUid: String,
966    pub name: String,
967    pub color: String,
968    pub useCase: i32,
969    pub clubId: i32,
970    pub leaderboardGroupUid: String,
971    pub publicationTimestamp: i64,
972    pub startTimestamp: i64,
973    pub endTimestamp: i64,
974    pub rankingSentTimestamp: Option<i64>,
975    // pub year: Option<i32>,
976    // pub week: Option<i32>,
977    // pub day: Option<i32>,
978    // pub monthYear: Option<i32>,
979    // pub month: Option<i32>,
980    // pub monthDay: Option<i32>,
981    pub year: i32,
982    pub week: i32,
983    pub day: i32,
984    pub monthYear: i32,
985    pub month: i32,
986    pub monthDay: i32,
987    pub published: bool,
988    pub playlist: Vec<ClubCampaign_PlaylistEntry>,
989    pub latestSeasons: Vec<ClubCampaign_LatestSeason>,
990    pub categories: Vec<ClubCampaign_Category>,
991    pub media: ClubCampaign_Media,
992    pub editionTimestamp: i64,
993}
994
995#[derive(Debug, Clone, Serialize, Deserialize)]
996#[allow(non_camel_case_types, non_snake_case)]
997pub struct ClubCampaign_PlaylistEntry {
998    pub id: i32,
999    pub position: i32,
1000    pub mapUid: String,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[allow(non_camel_case_types, non_snake_case)]
1005pub struct ClubCampaign_LatestSeason {
1006    pub uid: String,
1007    pub name: String,
1008    pub startTimestamp: i64,
1009    pub endTimestamp: i64,
1010    pub relativeStart: i64,
1011    pub relativeEnd: i64,
1012    pub campaignId: i32,
1013    pub active: bool,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017#[allow(non_camel_case_types, non_snake_case)]
1018pub struct ClubCampaign_Category {
1019    pub position: i32,
1020    pub length: i32,
1021    pub name: String,
1022}
1023
1024#[derive(Debug, Clone, Serialize, Deserialize)]
1025#[allow(non_camel_case_types, non_snake_case)]
1026pub struct ClubCampaign_Media {
1027    pub buttonBackgroundUrl: String,
1028    pub buttonForegroundUrl: String,
1029    pub decalUrl: String,
1030    pub popUpBackgroundUrl: String,
1031    pub popUpImageUrl: String,
1032    pub liveButtonBackgroundUrl: String,
1033    pub liveButtonForegroundUrl: String,
1034}
1035
1036#[derive(Debug, Clone, Serialize, Deserialize)]
1037#[allow(non_camel_case_types, non_snake_case)]
1038pub struct ClubCampaignList {
1039    pub clubCampaignList: Vec<ClubCampaignById>,
1040    pub maxPage: i32,
1041    pub itemCount: i32,
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1045#[allow(non_camel_case_types, non_snake_case)]
1046pub struct ClubRoomList {
1047    pub clubRoomList: Vec<ClubRoom>,
1048    pub maxPage: i32,
1049    pub itemCount: i32,
1050}
1051
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1053#[allow(non_camel_case_types, non_snake_case)]
1054pub struct ClubRoom {
1055    pub id: i32,
1056    pub clubId: i32,
1057    pub clubName: String,
1058    pub nadeo: bool,
1059    pub roomId: Option<i32>,
1060    pub campaignId: Option<i32>,
1061    pub playerServerLogin: Option<String>,
1062    pub activityId: i32,
1063    pub name: String,
1064    pub room: ClubRoom_Room,
1065    pub popularityLevel: i32,
1066    pub creationTimestamp: i64,
1067    pub creatorAccountId: String,
1068    pub latestEditorAccountId: String,
1069    pub password: bool,
1070    pub mediaUrl: String,
1071    pub mediaUrlPngLarge: String,
1072    pub mediaUrlPngMedium: String,
1073    pub mediaUrlPngSmall: String,
1074    pub mediaUrlDds: String,
1075    pub mediaTheme: String,
1076    // pub maps: Vec<String>,
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize)]
1080#[allow(non_camel_case_types, non_snake_case)]
1081pub struct ClubRoom_Room {
1082    pub id: Option<i32>,
1083    pub name: String,
1084    pub region: Option<String>,
1085    pub serverAccountId: String,
1086    pub maxPlayers: i32,
1087    pub playerCount: i32,
1088    pub maps: Vec<String>,
1089    pub script: String,
1090    pub scalable: bool,
1091    #[serde(deserialize_with = "empty_list_or_map")]
1092    pub scriptSettings: HashMap<String, ClubRoom_ScriptSetting>,
1093    pub serverInfo: Option<String>,
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1097#[allow(non_camel_case_types, non_snake_case)]
1098pub struct ClubRoom_Room_ForEdit {
1099    pub name: String,
1100    pub region: String,
1101    pub maxPlayersPerServer: i32,
1102    pub maps: Vec<String>,
1103    pub script: String,
1104    pub scalable: bool,
1105    /// cannot be changed after creation
1106    pub password: bool,
1107    pub settings: Vec<ClubRoom_ScriptSetting>,
1108}
1109
1110fn empty_list_or_map<'de, T, D>(deserializer: D) -> Result<T, D::Error>
1111where
1112    T: Deserialize<'de> + Default,
1113    D: Deserializer<'de>,
1114{
1115    struct EmptyListOrStruct<T>(PhantomData<fn() -> T>);
1116    impl<'de, T> Visitor<'de> for EmptyListOrStruct<T>
1117    where
1118        T: Deserialize<'de> + Default,
1119    {
1120        type Value = T;
1121
1122        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1123            formatter.write_str("empty list or struct")
1124        }
1125
1126        fn visit_seq<A>(self, mut seq: A) -> Result<T, A::Error>
1127        where
1128            A: SeqAccess<'de>,
1129        {
1130            if seq.next_element::<()>()?.is_some() {
1131                Err(de::Error::invalid_length(1, &self))
1132            } else {
1133                Ok(Default::default())
1134            }
1135        }
1136
1137        fn visit_map<M>(self, map: M) -> Result<T, M::Error>
1138        where
1139            M: MapAccess<'de>,
1140        {
1141            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
1142        }
1143    }
1144
1145    deserializer.deserialize_any(EmptyListOrStruct(PhantomData))
1146}
1147
1148#[derive(Debug, Clone, Serialize, Deserialize)]
1149#[allow(non_camel_case_types, non_snake_case)]
1150pub struct ClubRoom_ScriptSetting {
1151    pub key: String,
1152    pub value: String,
1153    pub r#type: String,
1154}
1155
1156#[derive(Debug, Clone, Serialize, Deserialize)]
1157#[allow(non_camel_case_types, non_snake_case)]
1158pub struct ActivityList {
1159    pub activityList: Vec<Activity>,
1160    pub maxPage: i32,
1161    pub itemCount: i32,
1162}
1163
1164#[derive(Debug, Clone, Serialize, Deserialize)]
1165#[allow(non_camel_case_types, non_snake_case)]
1166pub struct Activity {
1167    pub id: i32,
1168    pub name: String,
1169    pub activityType: String,
1170    pub activityId: i32,
1171    pub targetActivityId: i32,
1172    pub campaignId: i32,
1173    pub position: i32,
1174    pub public: bool,
1175    pub active: bool,
1176    pub externalId: i32,
1177    pub featured: bool,
1178    pub password: bool,
1179    pub itemsCount: i32,
1180    pub clubId: i32,
1181    pub editionTimestamp: i64,
1182    pub creatorAccountId: String,
1183    pub latestEditorAccountId: String,
1184    pub mediaUrl: String,
1185    pub mediaUrlPngLarge: String,
1186    pub mediaUrlPngMedium: String,
1187    pub mediaUrlPngSmall: String,
1188    pub mediaUrlDds: String,
1189    pub mediaTheme: String,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize)]
1193#[allow(non_camel_case_types, non_snake_case)]
1194pub struct ClubList {
1195    pub clubList: Vec<ClubInfo>,
1196    pub maxPage: u32,
1197    pub clubCount: u32,
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202    use std::{future::Future, u32};
1203
1204    use crate::test_helpers::*;
1205    use auth::{NadeoClient, UserAgentDetails};
1206    use futures::stream::FuturesUnordered;
1207    use lazy_static::lazy_static;
1208    use oneshot::error::RecvError;
1209
1210    use super::*;
1211    use crate::*;
1212
1213    pub static MAP_UIDS: [&str; 5] = [
1214        "YewzuEnjmnh_ShMW1cX0puuZHcf",
1215        "HisPAAWhTMTjQPxhMJtMak7Daud",
1216        "PrometheusByXertroVFtArcher",
1217        "DeepDip2__The_Gentle_Breeze",
1218        "DeepDip2__The_Storm_Is_Here",
1219    ];
1220
1221    #[ignore]
1222    #[tokio::test]
1223    async fn test_monthly_campaign() {
1224        let creds = get_test_creds();
1225        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1226        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1227            .await
1228            .unwrap();
1229        let res = client
1230            .get_monthly_campaign(MonthlyCampaignType::Royal, 10, 0)
1231            .await
1232            .unwrap();
1233        println!("Monthly Campaign: {:?}", res);
1234    }
1235
1236    // test get_map_info_multiple -- works, ignore now
1237    #[ignore]
1238    #[tokio::test]
1239    async fn test_get_map_info_multiple() {
1240        let creds = get_test_creds();
1241        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1242        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1243            .await
1244            .unwrap();
1245        let uids = Vec::from(&MAP_UIDS[..]);
1246        let res = client.get_map_info_multiple(&uids).await.unwrap();
1247        println!("Map Info Multiple: {:?}", res);
1248        println!(
1249            "get_cached_avg_req_per_sec: {:?}",
1250            client.get_cached_avg_req_per_sec().await
1251        );
1252        for (mi, uid) in res.mapList.iter().zip(MAP_UIDS.iter()) {
1253            let mi2 = client.get_map_info(uid).await.unwrap().unwrap();
1254            assert_eq!(mi, &mi2);
1255            println!("Matches: {:?} -> {:?}", mi.uid, mi2);
1256            println!(
1257                "get_cached_avg_req_per_sec: {:?}",
1258                client.get_cached_avg_req_per_sec().await
1259            );
1260        }
1261        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
1262        let _mi2 = client.get_map_info(uids[0]).await.unwrap();
1263        println!(
1264            "get_cached_avg_req_per_sec: {:?}",
1265            client.get_cached_avg_req_per_sec().await
1266        );
1267    }
1268
1269    // test get_map_info for PrometheusByXertroVFtArcher
1270    #[ignore]
1271    #[tokio::test]
1272    async fn test_get_map_info() {
1273        let creds = get_test_creds();
1274        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1275        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1276            .await
1277            .unwrap();
1278        let res = client
1279            .get_map_info("PrometheusByXertroVFtArcher")
1280            .await
1281            .unwrap()
1282            .unwrap();
1283        println!("Map Info: {:?}", res);
1284        let res = client
1285            .get_map_info("XXXXetheusByXertroVFtArcher")
1286            .await
1287            .unwrap();
1288        assert_eq!(res, None);
1289        println!(
1290            "missing map info was None (good). get_cached_avg_req_per_sec: {:?}",
1291            client.get_cached_avg_req_per_sec().await
1292        );
1293    }
1294
1295    // test surround
1296    #[ignore]
1297    #[tokio::test]
1298    async fn test_surround() {
1299        let creds = get_test_creds();
1300        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1301        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1302            .await
1303            .unwrap();
1304        let res = client
1305            .get_pb_surround("PrometheusByXertroVFtArcher", 0, 0, u32::MAX, true)
1306            .await
1307            .unwrap();
1308        println!("Surround: {:?}", res);
1309    }
1310
1311    // test get_lb_positions_by_time
1312    #[ignore]
1313    #[tokio::test]
1314    async fn test_records_by_time() {
1315        let creds = get_test_creds();
1316        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1317        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1318            .await
1319            .unwrap();
1320        let res = client
1321            .get_lb_positions_by_time(&[
1322                ("PrometheusByXertroVFtArcher", NonZero::new(47391).unwrap()),
1323                (
1324                    "DeepDip2__The_Storm_Is_Here",
1325                    NonZero::new(60000 * 50).unwrap(),
1326                ),
1327            ])
1328            .await
1329            .unwrap();
1330        println!("Records by Time: {:?}", res);
1331    }
1332
1333    lazy_static! {
1334        static ref CLIENT: OnceLock<NadeoClient> = OnceLock::new();
1335    }
1336
1337    // test get_lb_positions_by_time batcher
1338    // #[ignore]
1339    #[tokio::test]
1340    async fn test_records_by_time_batched() {
1341        let creds = get_test_creds();
1342        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1343        let client: NadeoClient = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1344            .await
1345            .unwrap();
1346        if CLIENT.set(client).is_err() {
1347            panic!("CLIENT already set");
1348        }
1349        let client = CLIENT.get().unwrap();
1350        let to_get = vec![
1351            ("PrometheusByXertroVFtArcher", NonZero::new(1000).unwrap()),
1352            ("DeepDip2__The_Storm_Is_Here", NonZero::new(1).unwrap()),
1353            ("PrometheusByXertroVFtArcher", NonZero::new(47391).unwrap()),
1354            ("PrometheusByXertroVFtArcher", NonZero::new(48391).unwrap()),
1355            ("PrometheusByXertroVFtArcher", NonZero::new(48220).unwrap()),
1356            (
1357                "DeepDip2__The_Storm_Is_Here",
1358                NonZero::new(60000 * 50).unwrap(),
1359            ),
1360        ];
1361
1362        fn run_and_time<F: Future<Output = Result<ScoreToPos, RecvError>> + Send + 'static>(
1363            f: F,
1364        ) -> tokio::task::JoinHandle<ScoreToPos> {
1365            tokio::spawn(async move {
1366                let start = std::time::Instant::now();
1367                let r = f.await.unwrap();
1368                let end = std::time::Instant::now();
1369                println!("Time: {:?}; Res: {:?}", end - start, r);
1370                r
1371            })
1372        }
1373
1374        let reqs = to_get
1375            .into_iter()
1376            .map(|s| client.get_lb_position_by_time_batched(s.0, s.1))
1377            .map(run_and_time)
1378            .collect::<FuturesUnordered<_>>();
1379        let r = futures::future::join_all(reqs).await;
1380        println!("Results: {:?}", r);
1381    }
1382
1383    #[ignore]
1384    #[tokio::test]
1385    async fn test_long_running_refresh_token() {
1386        let creds = get_test_creds();
1387        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1388        let client = NadeoClient::create(creds, user_agent_auto!(&email), 1)
1389            .await
1390            .unwrap();
1391        let start = chrono::Utc::now();
1392        println!("Long running test -- starting at {:?}", start);
1393        let n = 200;
1394        for i in 0..n {
1395            println!("Running iteration {} / {}", i + 1, n);
1396            println!("\n\n--- token below? ---\n\n");
1397            let r = client
1398                .get_map_leaderboard("PrometheusByXertroVFtArcher", true, 10, i * 10)
1399                .await
1400                .unwrap();
1401            println!("\n\n--- end ---\n\n");
1402            println!(
1403                "Response: {}",
1404                r.tops[0]
1405                    .top
1406                    .iter()
1407                    .map(|t| format!("{}. {} -- {} ms", t.position, t.zoneName, t.score))
1408                    .collect::<Vec<String>>()
1409                    .join("\n")
1410            );
1411            if i == n - 1 {
1412                println!(
1413                    "Last iter. Duration so far: {:?}",
1414                    chrono::Utc::now() - start
1415                );
1416                break;
1417            }
1418            println!("Sleeping for 10 minutes @ {:?}", chrono::Utc::now());
1419            tokio::time::sleep(tokio::time::Duration::from_secs(60 * 10)).await;
1420            println!(
1421                "Next iter. Duration so far: {:?} @ {:?}",
1422                chrono::Utc::now() - start,
1423                chrono::Utc::now()
1424            );
1425        }
1426    }
1427
1428    const TEST_CLUB_ID: i32 = 46587;
1429    const TEST_CAMPAIGN_ID: i32 = 38997;
1430    const TEST_ROOM_ID: i32 = 287220;
1431
1432    #[ignore]
1433    #[tokio::test]
1434    async fn test_get_club_campaigns() {
1435        let creds = get_test_creds();
1436        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1437        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1438            .await
1439            .unwrap();
1440        let res = client.get_club_campaigns(100, 4000, None).await.unwrap();
1441        println!("Club Campaigns: {:?}", res);
1442    }
1443
1444    #[ignore]
1445    #[tokio::test]
1446    async fn test_get_club_campaign_by_id() {
1447        let creds = get_test_creds();
1448        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1449        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1450            .await
1451            .unwrap();
1452        let res = client
1453            .get_club_campaign_by_id(TEST_CLUB_ID, TEST_CAMPAIGN_ID)
1454            .await
1455            .unwrap();
1456        println!("Club Campaign: {:?}", res);
1457    }
1458
1459    #[ignore]
1460    #[tokio::test]
1461    async fn test_get_club_rooms() {
1462        let creds = get_test_creds();
1463        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1464        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1465            .await
1466            .unwrap();
1467        let res = client.get_club_rooms(10, 0, None).await.unwrap();
1468        println!("Club Rooms: {:?}", res);
1469    }
1470
1471    #[ignore]
1472    #[tokio::test]
1473    async fn test_get_club_room_by_id() {
1474        let creds = get_test_creds();
1475        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1476        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1477            .await
1478            .unwrap();
1479        let res = client
1480            .get_club_room_by_id(TEST_CLUB_ID, TEST_ROOM_ID)
1481            .await
1482            .unwrap();
1483        println!("Club Room: {:?}", res);
1484    }
1485
1486    #[ignore]
1487    #[tokio::test]
1488    async fn test_get_club_info() {
1489        let creds = get_test_creds();
1490        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1491        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1492            .await
1493            .unwrap();
1494        let res = client.get_club_info(TEST_CLUB_ID).await.unwrap();
1495        println!("Club Info: {:?}", res);
1496    }
1497
1498    #[ignore]
1499    #[tokio::test]
1500    async fn test_get_club_activity_list() {
1501        let creds = get_test_creds();
1502        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1503        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1504            .await
1505            .unwrap();
1506        let res = client
1507            .get_club_activity_list(TEST_CLUB_ID, 10, 0, Some(true))
1508            .await
1509            .unwrap();
1510        println!("Club Activity List: {:?}", res);
1511    }
1512
1513    #[ignore]
1514    #[tokio::test]
1515    async fn test_get_clubs() {
1516        let creds = get_test_creds();
1517        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1518        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1519            .await
1520            .unwrap();
1521        let res = client.get_clubs(10, 0, None).await.unwrap();
1522        println!("Clubs: {:?}", res);
1523    }
1524
1525    #[ignore]
1526    #[tokio::test]
1527    async fn test_get_clubs_mine() {
1528        let creds = get_test_ubi_creds();
1529        let email = std::env::var("NADEO_TEST_UA_EMAIL").unwrap();
1530        let client = NadeoClient::create(creds, user_agent_auto!(&email), 10)
1531            .await
1532            .unwrap();
1533        let res = client.get_clubs_mine(10, 0).await.unwrap();
1534        println!("Clubs: {:?}", res);
1535    }
1536}