nadeo_api_rs/
auth.rs

1use std::sync::{Arc, OnceLock};
2
3use base64::prelude::*;
4use chrono::{DateTime, TimeZone, Utc};
5use log::{info, warn};
6use reqwest::{
7    header::{AUTHORIZATION, CONTENT_TYPE},
8    redirect::Policy,
9};
10use ringbuf::{traits::*, HeapRb};
11use serde::Deserialize;
12use serde_json::{json, Value};
13use tokio::sync::{RwLock, Semaphore, SemaphorePermit, TryLockError};
14
15// Handle nadeo authentication
16/// Credentials for nadeo services -- create via [::dedicated_server](NadeoCredentials::dedicated_server) or [::ubisoft](NadeoCredentials::ubisoft)
17#[derive(Debug)]
18pub enum NadeoCredentials {
19    /// username and pw
20    DedicatedServer { u: String, p: String },
21    /// email and pw
22    Ubisoft { e: String, p: String },
23}
24
25impl NadeoCredentials {
26    /// Create credentials from a dedicated server login
27    pub fn dedicated_server(username: &str, password: &str) -> Self {
28        NadeoCredentials::DedicatedServer {
29            u: username.to_string(),
30            p: password.to_string(),
31        }
32    }
33
34    /// Create credentials from a ubisoft login
35    pub fn ubisoft(email: &str, password: &str) -> Self {
36        NadeoCredentials::Ubisoft {
37            e: email.to_string(),
38            p: password.to_string(),
39        }
40    }
41}
42
43#[derive(Debug)]
44pub enum LoginError {
45    Misc(String),
46    Req(reqwest::Error),
47}
48
49impl From<reqwest::Error> for LoginError {
50    fn from(e: reqwest::Error) -> Self {
51        LoginError::Req(e)
52    }
53}
54
55impl From<&str> for LoginError {
56    fn from(s: &str) -> Self {
57        LoginError::Misc(s.to_string())
58    }
59}
60
61impl From<String> for LoginError {
62    fn from(s: String) -> Self {
63        LoginError::Misc(s)
64    }
65}
66
67impl std::error::Error for LoginError {}
68
69impl std::fmt::Display for LoginError {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            LoginError::Misc(s) => write!(f, "LoginError: {}", s),
73            LoginError::Req(e) => write!(f, "LoginError: {}", e),
74        }
75    }
76}
77
78impl NadeoCredentials {
79    pub fn get_basic_auth_header(&self) -> String {
80        let auth_string = match self {
81            NadeoCredentials::DedicatedServer { u, p } => format!("{}:{}", u, p),
82            NadeoCredentials::Ubisoft { e, p } => format!("{}:{}", e, p),
83        };
84        format!("Basic {}", BASE64_STANDARD.encode(auth_string))
85    }
86
87    pub fn is_ubi(&self) -> bool {
88        matches!(self, NadeoCredentials::Ubisoft { .. })
89    }
90
91    pub fn is_dedi(&self) -> bool {
92        matches!(self, NadeoCredentials::DedicatedServer { .. })
93    }
94
95    pub async fn run_login(&self, client: &reqwest::Client) -> Result<NadeoTokens, LoginError> {
96        let req_for_audience_token;
97        match self {
98            NadeoCredentials::Ubisoft { e, p } => {
99                let res = client
100                    .post(AUTH_UBI_URL)
101                    .basic_auth(e, Some(p))
102                    .header("Ubi-AppId", "86263886-327a-4328-ac69-527f0d20a237")
103                    .header(CONTENT_TYPE, "application/json")
104                    .send()
105                    .await?;
106                let body: Value = res.json().await?;
107                let ticket = body["ticket"].as_str().ok_or("No ticket in response")?;
108                let auth2 = format!("ubi_v1 t={}", ticket);
109                req_for_audience_token = client
110                    .post(AUTH_UBI_URL2)
111                    .header(AUTHORIZATION, auth2)
112                    .header(CONTENT_TYPE, "application/json");
113            }
114            NadeoCredentials::DedicatedServer { u, p } => {
115                req_for_audience_token = client
116                    .post(AUTH_DEDI_URL)
117                    .header(CONTENT_TYPE, "application/json")
118                    .basic_auth(u, Some(p));
119            }
120        };
121        let req_core_token = req_for_audience_token
122            .try_clone()
123            .unwrap()
124            .json(&json!({"audience": "NadeoServices"}))
125            .send();
126        let req_live_token = req_for_audience_token
127            .try_clone()
128            .unwrap()
129            .json(&json!({"audience": "NadeoLiveServices"}))
130            .send();
131        let (core_token, live_token) = tokio::try_join!(req_core_token, req_live_token)?;
132        let core_token_resp: Value = core_token.json().await?;
133        let live_token_resp: Value = live_token.json().await?;
134        let core_token = NadeoToken::new(NadeoAudience::Core, core_token_resp)?;
135        let live_token = NadeoToken::new(NadeoAudience::Live, live_token_resp)?;
136        Ok(NadeoTokens::new(core_token, live_token))
137    }
138}
139
140#[derive(Debug)]
141pub struct OAuthCredentials {
142    pub client_id: String,
143    pub client_secret: String,
144}
145
146impl OAuthCredentials {
147    pub fn new(client_id: &str, client_secret: &str) -> Self {
148        Self {
149            client_id: client_id.to_string(),
150            client_secret: client_secret.to_string(),
151        }
152    }
153
154    pub async fn run_login(&self, client: &reqwest::Client) -> Result<OAuthToken, reqwest::Error> {
155        let res = client
156            .post(AUTH_OAUTH_URL)
157            .form(&[
158                ("grant_type", "client_credentials"),
159                ("client_id", &self.client_id),
160                ("client_secret", &self.client_secret),
161            ])
162            .send()
163            .await?;
164        let j: OAuthToken = res.json().await?;
165        let _ = j
166            .expires_at
167            .set(Utc::now() + chrono::Duration::seconds(j.expires_in))
168            .expect("Failed to set expires_at");
169        Ok(j)
170    }
171}
172
173#[derive(Debug, Deserialize, Clone)]
174pub struct OAuthToken {
175    pub token_type: String,
176    pub expires_in: i64,
177    pub access_token: String,
178    #[serde(skip)]
179    pub expires_at: OnceLock<DateTime<Utc>>,
180}
181
182impl OAuthToken {
183    /// Returns `<bearer> <access_token>`
184    pub fn get_authz_header(&self) -> String {
185        format!("{} {}", self.token_type, self.access_token)
186    }
187
188    pub fn should_refresh(&self) -> bool {
189        *self.expires_at.get().unwrap() - chrono::TimeDelta::seconds(60) < Utc::now()
190    }
191}
192
193// first url to post to creds
194pub static AUTH_UBI_URL: &str = "https://public-ubiservices.ubi.com/v3/profiles/sessions";
195// second url to post audience to
196pub static AUTH_UBI_URL2: &str =
197    "https://prod.trackmania.core.nadeo.online/v2/authentication/token/ubiservices";
198// dedicated server url to post audience to
199pub static AUTH_DEDI_URL: &str =
200    "https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic";
201// oauth machine-to-machine url to post id,sec to
202pub static AUTH_OAUTH_URL: &str = "https://api.trackmania.com/api/access_token";
203
204pub static AUTH_REFRESH_URL: &str =
205    "https://prod.trackmania.core.nadeo.online/v2/authentication/token/refresh";
206
207// pub struct UbiAuth {
208//     token: String,
209// }
210
211/// User agent details for nadeo services; format: `app_name/version (contact_email)`
212#[derive(Debug)]
213pub struct UserAgentDetails {
214    pub app_name: String,
215    pub contact_email: String,
216    pub version: String,
217}
218
219/// Create a UserAgentDetails struct with the app name and version autodetected from the cargo manifest.
220///
221/// Usage:
222/// ```
223/// use nadeo_api_rs::{user_agent_auto, auth::UserAgentDetails};
224/// let ua = user_agent_auto!("my@email");
225/// ```
226#[macro_export]
227macro_rules! user_agent_auto {
228    ($email:expr) => {
229        UserAgentDetails::new(env!("CARGO_CRATE_NAME"), $email, env!("CARGO_PKG_VERSION"))
230    };
231}
232/// Create a UserAgentDetails struct with the version autodetected from the cargo manifest.
233///
234/// Usage:
235/// ```
236/// use nadeo_api_rs::{user_agent_auto_ver, auth::UserAgentDetails};
237/// let ua = user_agent_auto_ver!("myapp", "my@email");
238/// ```
239#[macro_export]
240macro_rules! user_agent_auto_ver {
241    ($appname:expr, $email:expr) => {
242        UserAgentDetails::new($appname, $email, env!("CARGO_PKG_VERSION"))
243    };
244}
245
246impl UserAgentDetails {
247    pub fn new(app_name: &str, contact_email: &str, version: &str) -> Self {
248        Self {
249            app_name: app_name.to_string(),
250            contact_email: contact_email.to_string(),
251            version: version.to_string(),
252        }
253    }
254
255    pub fn get_user_agent_string(&self) -> String {
256        format!(
257            "{}/{} ({})",
258            self.app_name, self.version, self.contact_email
259        )
260    }
261}
262
263/// Main client for nadeo services
264#[derive()]
265pub struct NadeoClient {
266    credentials: NadeoCredentials,
267    core_token: RwLock<NadeoToken>,
268    live_token: RwLock<NadeoToken>,
269    pub user_agent: UserAgentDetails,
270    pub max_concurrent_requests: usize,
271    req_semaphore: Arc<Semaphore>,
272    client: reqwest::Client,
273    req_times: RwLock<HeapRb<chrono::DateTime<chrono::Utc>>>,
274    nb_reqs: RwLock<usize>,
275    last_avg_req_per_sec: RwLock<f64>,
276    oauth_credentials: OnceLock<OAuthCredentials>,
277    oauth_token: RwLock<Option<OAuthToken>>,
278    pub(crate) batcher_lb_pos_by_time: BatcherLbPosByTime,
279}
280
281impl NadeoApiClient for NadeoClient {
282    async fn get_client(&self) -> &reqwest::Client {
283        // todo: check if token is expired / refreshable
284        self.ensure_tokens_valid().await;
285        &self.client
286    }
287
288    /// Prefer: aget_auth_token
289    /// Get the auth token for the given audience (used to construct the header)
290    fn get_auth_token(&self, audience: NadeoAudience) -> Result<String, TryLockError> {
291        Ok(match audience {
292            Core => self.core_token.try_read()?.access_token.clone(),
293            Live => self.live_token.try_read()?.access_token.clone(),
294        })
295    }
296
297    /// Get the auth token for the given audience (used to construct the header)
298    async fn aget_auth_token(&self, audience: NadeoAudience) -> String {
299        match audience {
300            Core => self.core_token.read().await.access_token.clone(),
301            Live => self.live_token.read().await.access_token.clone(),
302        }
303    }
304
305    async fn rate_limit(&self) -> SemaphorePermit {
306        let permit = self
307            .req_semaphore
308            .acquire()
309            .await
310            .expect("Failed to acquire semaphore");
311        self.keep_long_running_rate_limit().await;
312        permit
313    }
314}
315
316// proper defaults: 1.0, 1500
317// pub const MAX_REQ_PER_SEC: f64 = 7.0;
318pub const MAX_REQ_PER_SEC: f64 = 1.0;
319pub const HIT_MAX_REQ_PER_SEC_WAIT: u64 = 500;
320
321impl NadeoClient {
322    pub fn get_authz_header_for_auth(&self) -> String {
323        self.credentials.get_basic_auth_header()
324    }
325
326    pub async fn create(
327        credentials: NadeoCredentials,
328        user_agent: UserAgentDetails,
329        max_concurrent_requests: usize,
330    ) -> Result<Self, NadeoError> {
331        let req_semaphore = Arc::new(Semaphore::new(max_concurrent_requests));
332        let client = reqwest::Client::builder()
333            .user_agent(user_agent.get_user_agent_string())
334            .redirect(Policy::limited(5))
335            .build()?;
336
337        // run auth
338        let tokens = credentials.run_login(&client).await?;
339
340        let req_times = RwLock::new(HeapRb::new(500));
341
342        Ok(Self {
343            credentials,
344            core_token: RwLock::new(tokens.core),
345            live_token: RwLock::new(tokens.live),
346            user_agent,
347            max_concurrent_requests,
348            req_semaphore,
349            client,
350            req_times,
351            nb_reqs: RwLock::new(0),
352            last_avg_req_per_sec: RwLock::new(0.0),
353            oauth_credentials: OnceLock::new(),
354            oauth_token: RwLock::new(None),
355            batcher_lb_pos_by_time: BatcherLbPosByTime::new(),
356        })
357    }
358
359    pub fn with_oauth(self, oauth: OAuthCredentials) -> Option<Self> {
360        self.oauth_credentials.set(oauth).ok()?;
361        Some(self)
362    }
363
364    pub async fn ensure_tokens_valid(&self) {
365        let mut core_token = self.core_token.write().await;
366        if core_token.is_access_expired() || core_token.can_refresh() {
367            info!("Core token expired, refreshing");
368            core_token.run_refresh(&self.client).await;
369        }
370
371        let mut live_token = self.live_token.write().await;
372        if live_token.is_access_expired() || live_token.can_refresh() {
373            info!("Live token expired, refreshing");
374            live_token.run_refresh(&self.client).await;
375        }
376    }
377
378    async fn keep_long_running_rate_limit(&self) {
379        *self.nb_reqs.write().await += 1;
380        let mut req_times = self.req_times.write().await;
381        let now = now_dt();
382        // if we have made less than 500 requests, just push the current time
383        if req_times.occupied_len() < self.max_concurrent_requests {
384            // update avg req per sec
385            *self.last_avg_req_per_sec.write().await = match req_times.try_peek() {
386                Some(oldest) => {
387                    req_times.occupied_len() as f64
388                        / (now - *oldest).num_milliseconds().max(1) as f64
389                        * 1000.0
390                }
391                None => 0.0,
392            };
393            req_times.try_push(now).unwrap();
394            return;
395        }
396        let oldest = req_times.try_peek().unwrap();
397        let diff = now - *oldest;
398        let req_per_sec =
399            req_times.capacity().get() as f64 / diff.num_milliseconds().max(1) as f64 * 1000.0;
400        *self.last_avg_req_per_sec.write().await = req_per_sec;
401        if req_per_sec > MAX_REQ_PER_SEC {
402            // we have an exclusive lock on req_times, so we can sleep and we won't miss any requests
403            tokio::time::sleep(std::time::Duration::from_millis(HIT_MAX_REQ_PER_SEC_WAIT)).await;
404        }
405        req_times.push_overwrite(now_dt());
406    }
407
408    pub async fn calc_avg_req_per_sec(&self) -> f64 {
409        let req_times = self.req_times.read().await;
410        if req_times.is_empty() {
411            return 0.0;
412        }
413        let now = now_dt();
414        let diff = now - *req_times.try_peek().unwrap();
415        req_times.occupied_len() as f64 / diff.num_milliseconds() as f64 * 1000.0
416    }
417
418    pub async fn get_cached_avg_req_per_sec(&self) -> f64 {
419        *self.last_avg_req_per_sec.read().await
420    }
421
422    pub async fn get_nb_reqs(&self) -> usize {
423        *self.nb_reqs.read().await
424    }
425
426    pub fn get_batcher(&self) -> &BatcherLbPosByTime {
427        &self.batcher_lb_pos_by_time
428    }
429
430    pub(crate) async fn check_start_batcher_lb_pos_by_time_loop(&'static self) {
431        if self.batcher_lb_pos_by_time.has_loop_started()
432            || self.batcher_lb_pos_by_time.set_loop_started().is_err()
433        {
434            return;
435        }
436        tokio::spawn(async move {
437            loop {
438                let nb_queued = self.batcher_lb_pos_by_time.nb_queued().await;
439                // if nothing queued, sleep and repeat
440                if nb_queued == 0 {
441                    tokio::time::sleep(std::time::Duration::from_millis(29)).await;
442                    continue;
443                }
444                // if less than the limit (50), sleep a little to let more come in
445                if nb_queued < 50 {
446                    tokio::time::sleep(std::time::Duration::from_millis(19)).await;
447                }
448                // clear out queue
449                while self.batcher_lb_pos_by_time.nb_queued().await > 0 {
450                    match self.batcher_lb_pos_by_time.run_batch(self).await {
451                        Ok(_) => (),
452                        Err(e) => {
453                            warn!("Error in batcher_lb_pos_by_time: {:?}", e);
454                        }
455                    }
456                }
457            }
458        });
459    }
460}
461
462impl OAuthApiClient for NadeoClient {
463    async fn get_oauth_token(&self) -> Result<OAuthToken, String> {
464        let oauth = self
465            .oauth_credentials
466            .get()
467            .ok_or("No oauth credentials".to_string())?;
468        let cached_token: Option<OAuthToken> = self.oauth_token.read().await.as_ref().cloned();
469        match cached_token {
470            Some(token) => {
471                if !token.should_refresh() {
472                    return Ok(token);
473                }
474                let new_token = oauth
475                    .run_login(&self.client)
476                    .await
477                    .map_err(|e| e.to_string())?;
478                let _ = self.oauth_token.write().await.replace(new_token.clone());
479                Ok(new_token)
480            }
481            None => {
482                let token = oauth
483                    .run_login(&self.client)
484                    .await
485                    .map_err(|e| e.to_string())?;
486                self.oauth_token.write().await.replace(token.clone());
487                Ok(token)
488            }
489        }
490    }
491}
492
493#[derive(Debug)]
494pub enum NadeoAudience {
495    Core,
496    Live,
497    // Meet, // uses Live
498}
499pub use NadeoAudience::*;
500
501use crate::{
502    client::NadeoApiClient,
503    live::{BatcherLbPosByTime, NadeoError},
504    oauth::OAuthApiClient,
505};
506
507impl NadeoAudience {
508    pub fn get_audience_string(&self) -> &str {
509        match self {
510            Core => "NadeoServices",
511            Live => "NadeoLiveServices",
512        }
513    }
514
515    pub fn from_audience_string(aud: &str) -> Option<NadeoAudience> {
516        match aud {
517            "NadeoServices" => Some(Core),
518            "NadeoLiveServices" => Some(Live),
519            _ => None,
520        }
521    }
522}
523
524// Returns the token body (middle chunk, aka "payload") as a JSON object
525pub fn get_token_body(token: &String) -> Result<Value, String> {
526    token
527        .split(".")
528        .nth(1)
529        .map_or(Err("Token does not have 3 parts".to_string()), |part| {
530            let decoded = BASE64_URL_SAFE_NO_PAD
531                .decode(part.as_bytes())
532                .map_err(|e| e.to_string())?;
533            serde_json::from_slice(&decoded).map_err(|e| e.to_string())
534        })
535}
536
537// Returns e.g., "NadeoLiveServices"
538pub fn get_token_body_audience(body: &Value) -> Result<&str, String> {
539    body["aud"]
540        .as_str()
541        .ok_or("token.aud is not a string".to_string())
542}
543
544pub fn get_token_body_expiry(body: &Value) -> Result<i64, String> {
545    body["exp"]
546        .as_i64()
547        .ok_or("token.exp is not a i64".to_string())
548        .map(|exp| exp as i64)
549}
550
551pub fn get_token_body_refresh_after(body: &Value) -> Result<i64, String> {
552    body["rat"]
553        .as_i64()
554        .ok_or("token.rat is not a i64".to_string())
555        .map(|exp| exp as i64)
556}
557
558pub fn i64_to_datetime(i: i64) -> Result<chrono::DateTime<chrono::Utc>, String> {
559    match chrono::Utc.timestamp_opt(i, 0) {
560        chrono::offset::LocalResult::Single(dt) => Ok(dt),
561        chrono::offset::LocalResult::Ambiguous(dt, _) => Ok(dt),
562        _ => Err("Failed to convert i64 to DateTime".to_string()),
563    }
564}
565
566pub fn now_dt() -> chrono::DateTime<chrono::Utc> {
567    chrono::Utc::now()
568}
569
570#[derive(Debug)]
571pub struct NadeoTokens {
572    core: NadeoToken,
573    live: NadeoToken,
574}
575
576impl NadeoTokens {
577    pub fn new(core: NadeoToken, live: NadeoToken) -> Self {
578        Self { core, live }
579    }
580}
581
582#[derive(Debug)]
583pub struct NadeoToken {
584    audience: NadeoAudience,
585    access_token: String,
586    refresh_token: String,
587}
588
589impl NadeoToken {
590    pub fn new(audience: NadeoAudience, body: Value) -> Result<Self, String> {
591        let access_token = body["accessToken"]
592            .as_str()
593            .ok_or(format!("No accessToken in response; body: {}", body))?
594            .to_string();
595        let refresh_token = body["refreshToken"]
596            .as_str()
597            .ok_or("No refreshToken in response")?
598            .to_string();
599        Ok(Self {
600            audience,
601            access_token,
602            refresh_token,
603        })
604    }
605
606    /// Returns `nadeo_v1 t=refresh_token` (used to call AUTH_REFRESH_URL)
607    pub fn get_refresh_authz_header(&self) -> String {
608        format!("nadeo_v1 t={}", self.refresh_token)
609    }
610
611    /// Prefer get_*_authz_header instead. Returns `nadeo_v1 t=access_token`
612    pub fn get_access_authz_header(&self) -> String {
613        format!("nadeo_v1 t={}", self.access_token)
614    }
615
616    pub fn get_access_token_body(&self) -> Result<Value, String> {
617        get_token_body(&self.access_token)
618    }
619
620    pub fn get_refresh_token_body(&self) -> Result<Value, String> {
621        get_token_body(&self.refresh_token)
622    }
623
624    pub fn get_access_expiry_i64(&self) -> Result<i64, String> {
625        get_token_body_expiry(&self.get_access_token_body()?)
626    }
627
628    pub fn get_refresh_expiry_i64(&self) -> Result<i64, String> {
629        get_token_body_expiry(&self.get_refresh_token_body()?)
630    }
631
632    pub fn get_access_expiry(&self) -> Result<chrono::DateTime<chrono::Utc>, String> {
633        i64_to_datetime(self.get_access_expiry_i64()?)
634    }
635
636    pub fn get_refresh_expiry(&self) -> Result<chrono::DateTime<chrono::Utc>, String> {
637        i64_to_datetime(self.get_refresh_expiry_i64()?)
638    }
639
640    pub fn get_access_refresh_after(&self) -> Result<chrono::DateTime<chrono::Utc>, String> {
641        i64_to_datetime(get_token_body_refresh_after(
642            &self.get_access_token_body()?,
643        )?)
644    }
645
646    pub fn get_refresh_refresh_after(&self) -> Result<chrono::DateTime<chrono::Utc>, String> {
647        i64_to_datetime(get_token_body_refresh_after(
648            &self.get_refresh_token_body()?,
649        )?)
650    }
651
652    pub fn can_refresh(&self) -> bool {
653        self.get_access_refresh_after()
654            .map_or(false, |after| after < now_dt())
655    }
656
657    pub fn is_access_expired(&self) -> bool {
658        self.get_access_expiry().map_or(true, |exp| exp < now_dt())
659    }
660
661    pub fn is_refresh_expired(&self) -> bool {
662        self.get_refresh_expiry().map_or(true, |exp| exp < now_dt())
663    }
664
665    /// Returns authz header if audience is core
666    pub fn get_core_authz_header(&self) -> Option<String> {
667        match self.audience {
668            NadeoAudience::Core => Some(self.get_access_authz_header()),
669            _ => None,
670        }
671    }
672
673    /// Returns authz header if audience is live
674    pub fn get_live_authz_header(&self) -> Option<String> {
675        match self.audience {
676            NadeoAudience::Live => Some(self.get_access_authz_header()),
677            _ => None,
678        }
679    }
680
681    // /// Returns authz header if audience is meet
682    // pub fn get_meet_authz_header(&self) -> Option<String> {
683    //     match self.audience {
684    //         NadeoAudience::Meet => Some(self.get_access_authz_header()),
685    //         _ => None,
686    //     }
687    // }
688
689    async fn run_refresh(&mut self, client: &reqwest::Client) {
690        let req = client
691            .post(AUTH_REFRESH_URL)
692            .header(AUTHORIZATION, self.get_refresh_authz_header())
693            .send()
694            .await
695            .unwrap();
696
697        #[cfg(test)]
698        {
699            println!("----------------------------");
700            println!("{:?} Token before refresh:", self.audience);
701            print_token_stuff(&self);
702            println!("----------------------------");
703        }
704
705        // #[cfg(test)]
706        // dbg!(&req);
707        let body: Value = req.json().await.unwrap();
708        // #[cfg(test)]
709        // dbg!(&body);
710
711        self.access_token = body["accessToken"].as_str().unwrap().to_string();
712        if body["refreshToken"].is_null() {
713            // refresh token not returned, keep old one
714            warn!(
715                "Refresh token not returned for audience {:?}",
716                self.audience
717            );
718        } else {
719            // todo, test
720            self.refresh_token = body["refreshToken"].as_str().unwrap().to_string();
721            warn!("Updated refresh token for audience {:?}", self.audience);
722        }
723
724        #[cfg(test)]
725        {
726            println!("----------------------------");
727            println!("{:?} Token after refresh:", self.audience);
728            print_token_stuff(&self);
729            println!("----------------------------");
730        }
731    }
732}
733
734#[cfg(test)]
735fn print_token_stuff(token: &NadeoToken) {
736    println!("Audience: {:?}", token.audience);
737    println!("Access token: {}", token.access_token);
738    println!("Refresh token: {}", token.refresh_token);
739    println!("Access expiry: {:?}", token.get_access_expiry());
740    println!("Refresh expiry: {:?}", token.get_refresh_expiry());
741    println!(
742        "Access refresh after: {:?}",
743        token.get_access_refresh_after()
744    );
745    println!(
746        "Refresh refresh after: {:?}",
747        token.get_refresh_refresh_after()
748    );
749}
750
751#[cfg(test)]
752mod tests {
753
754    use crate::test_helpers::*;
755
756    use super::*;
757
758    #[ignore]
759    #[tokio::test]
760    async fn test_login() -> Result<(), NadeoError> {
761        let cred = get_test_creds();
762        let client = reqwest::Client::new();
763        let res = cred.run_login(&client).await?;
764        println!("{:?}", res);
765        Ok(())
766    }
767
768    #[ignore]
769    #[tokio::test]
770    async fn test_ubi_login() -> Result<(), NadeoError> {
771        let cred = get_test_ubi_creds();
772        let client = reqwest::Client::new();
773        let res = cred.run_login(&client).await?;
774        println!("{:?}", res);
775        Ok(())
776    }
777
778    #[tokio::test]
779    async fn test_get_file_size() -> Result<(), NadeoError> {
780        let cred = get_test_creds();
781        let nc = NadeoClient::create(cred, user_agent_auto!("email"), 2).await?;
782        let size = nc
783            .get_file_size(
784                "https://core.trackmania.nadeo.live/maps/0c90c62a-f3ea-491c-ab86-1245ff575667/file",
785            )
786            .await?;
787        println!("Size: {}", size);
788        assert!(size > 0);
789        Ok(())
790    }
791
792    #[ignore]
793    #[tokio::test]
794    async fn test_print_token_times() -> Result<(), NadeoError> {
795        let cred = get_test_creds();
796        let client = reqwest::Client::new();
797        let tokens = cred.run_login(&client).await?;
798        println!("----------------------------");
799        print_token_stuff(&tokens.core);
800        println!("----------------------------");
801        print_token_stuff(&tokens.live);
802        println!("----------------------------");
803        Ok(())
804    }
805
806    fn print_token_stuff(token: &NadeoToken) {
807        println!("Audience: {:?}", token.audience);
808        println!("Access token: {}", token.access_token);
809        println!("Refresh token: {}", token.refresh_token);
810        println!("Access expiry: {:?}", token.get_access_expiry());
811        println!("Refresh expiry: {:?}", token.get_refresh_expiry());
812        println!(
813            "Access refresh after: {:?}",
814            token.get_access_refresh_after()
815        );
816        println!(
817            "Refresh refresh after: {:?}",
818            token.get_refresh_refresh_after()
819        );
820    }
821}