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#[derive(Debug)]
18pub enum NadeoCredentials {
19 DedicatedServer { u: String, p: String },
21 Ubisoft { e: String, p: String },
23}
24
25impl NadeoCredentials {
26 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 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 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
193pub static AUTH_UBI_URL: &str = "https://public-ubiservices.ubi.com/v3/profiles/sessions";
195pub static AUTH_UBI_URL2: &str =
197 "https://prod.trackmania.core.nadeo.online/v2/authentication/token/ubiservices";
198pub static AUTH_DEDI_URL: &str =
200 "https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic";
201pub 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#[derive(Debug)]
213pub struct UserAgentDetails {
214 pub app_name: String,
215 pub contact_email: String,
216 pub version: String,
217}
218
219#[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#[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#[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 self.ensure_tokens_valid().await;
285 &self.client
286 }
287
288 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 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
316pub 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 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 req_times.occupied_len() < self.max_concurrent_requests {
384 *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 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 nb_queued == 0 {
441 tokio::time::sleep(std::time::Duration::from_millis(29)).await;
442 continue;
443 }
444 if nb_queued < 50 {
446 tokio::time::sleep(std::time::Duration::from_millis(19)).await;
447 }
448 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 }
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
524pub 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
537pub 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 pub fn get_refresh_authz_header(&self) -> String {
608 format!("nadeo_v1 t={}", self.refresh_token)
609 }
610
611 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 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 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 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 let body: Value = req.json().await.unwrap();
708 self.access_token = body["accessToken"].as_str().unwrap().to_string();
712 if body["refreshToken"].is_null() {
713 warn!(
715 "Refresh token not returned for audience {:?}",
716 self.audience
717 );
718 } else {
719 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}