pandora_api/json/
mod.rs

1/*!
2Support for the [JSON API v5 interface for Pandora](https://6xq.net/pandora-apidoc/json/).
3
4Unless noted otherwise JSON-encoded requests sent by within the HTTP POST body
5are encrypted using Blowfish ECB and converted to hexadecimal notation with
6lowercase letters.
7*/
8// SPDX-License-Identifier: MIT AND WTFPL
9
10pub mod accessory;
11pub mod ad;
12pub mod auth;
13pub mod bookmark;
14mod crypt;
15pub mod device;
16pub mod errors;
17pub mod music;
18pub mod station;
19pub mod test;
20pub mod track;
21pub mod user;
22
23use std::collections::HashMap;
24use std::fmt::Debug;
25
26use serde::{Deserialize, Serialize};
27use serde_json;
28
29use crate::errors::Error;
30use crate::json::auth::{PartnerLogin, PartnerLoginResponse};
31use crate::json::errors::{JsonError, JsonErrorKind};
32
33/// A builder to construct the properties of an http request to Pandora.
34#[derive(Debug, Clone)]
35pub struct PandoraSession {
36    client: reqwest::Client,
37    endpoint_url: url::Url,
38    tokens: SessionTokens,
39    json: serde_json::value::Value,
40    args: std::collections::BTreeMap<String, String>,
41    encrypted: bool,
42}
43
44impl PandoraSession {
45    /// Construct a new PandoraSession.
46    pub fn new<T: ToEncryptionTokens, E: ToEndpoint>(
47        client: Option<reqwest::Client>,
48        to_encryption_tokens: &T,
49        to_endpoint: &E,
50    ) -> Self {
51        Self {
52            client: client.unwrap_or_default(),
53            endpoint_url: to_endpoint.to_endpoint_url(),
54            tokens: SessionTokens::new(to_encryption_tokens),
55            json: serde_json::value::Value::Object(serde_json::map::Map::new()),
56            args: std::collections::BTreeMap::new(),
57            encrypted: false,
58        }
59    }
60
61    /// Create a new PandoraSession copying the endpoint and session values into the new
62    /// object.
63    pub fn copy_session(&self) -> Self {
64        Self {
65            client: self.client.clone(),
66            endpoint_url: self.endpoint_url.clone(),
67            tokens: self.tokens.clone(),
68            json: serde_json::value::Value::Object(serde_json::map::Map::new()),
69            args: std::collections::BTreeMap::new(),
70            encrypted: false,
71        }
72    }
73
74    /// Get a reference to the http client.
75    pub fn http_client(&self) -> &reqwest::Client {
76        &self.client
77    }
78
79    /// Set the Endpoint on this PandoraSession instance.
80    pub fn endpoint<E: ToEndpoint>(&mut self, to_endpoint: E) -> &mut Self {
81        self.endpoint_url = to_endpoint.to_endpoint_url();
82        self
83    }
84
85    /// Get a mutable reference to the endpoint url::Url to update or make calls on it.
86    pub fn endpoint_mut<E: ToEndpoint>(&mut self) -> &mut url::Url {
87        &mut self.endpoint_url
88    }
89
90    /// Update the session partner tokens from type implementing ToPartnerTokens.
91    pub fn update_partner_tokens<T: ToPartnerTokens>(&mut self, to_partner_tokens: &T) {
92        self.tokens.update_partner_tokens(to_partner_tokens);
93    }
94
95    /// Update the session partner tokens from type implementing ToPartnerTokens.
96    pub fn update_user_tokens<T: ToUserTokens>(&mut self, to_user_tokens: &T) {
97        self.tokens.update_user_tokens(to_user_tokens);
98    }
99
100    /// Get a reference to the session tokens to check the state or make calls
101    /// on it.
102    pub fn session_tokens(&self) -> &SessionTokens {
103        &self.tokens
104    }
105
106    /// Get a mutable reference to the session tokens to modify the state or
107    /// make calls on it.
108    pub fn session_tokens_mut(&mut self) -> &mut SessionTokens {
109        &mut self.tokens
110    }
111
112    /// Set the json object on this PandoraSession instance.
113    ///
114    /// When build() is called, the json object will be updated with
115    /// session keys from the session instance, if one was provided.
116    pub fn json(&mut self, json: serde_json::value::Value) -> &mut Self {
117        self.json = json;
118        self
119    }
120
121    /// Get a mutable reference to the json to update or make calls on it.
122    pub fn json_mut(&mut self) -> &mut serde_json::value::Value {
123        &mut self.json
124    }
125
126    /// Add query arguments to the http request.
127    pub fn arg(&mut self, key: &str, value: &str) -> &mut Self {
128        self.args.insert(key.to_string(), value.to_string());
129        self
130    }
131
132    /// Require that the request body be encrypted using the session instance, if any was set.  If
133    /// no session instance is set, this will silently transmit the data unencrypted.
134    pub fn encrypted(&mut self) -> &mut Self {
135        self.encrypted = true;
136        self
137    }
138
139    /// Merge necessary values from the session instance into the query arguments
140    fn add_session_tokens_to_args(&mut self) {
141        // auth_token arg should be set to user_token, if available, otherwise partner_token
142        if let Some(auth_token) = self
143            .tokens
144            .user_token
145            .clone()
146            .or_else(|| self.tokens.partner_token.clone())
147        {
148            self.arg("auth_token", &auth_token);
149        }
150        if let Some(partner_id) = self.tokens.partner_id.clone() {
151            self.arg("partner_id", &partner_id);
152        }
153        if let Some(user_id) = self.tokens.user_id.clone() {
154            self.arg("user_id", &user_id);
155        }
156    }
157
158    /// Merge necessary values from the session instance into the json body
159    fn add_session_tokens_to_json(&mut self) {
160        let json_obj = self
161            .json
162            .as_object_mut()
163            .expect("Programming Error accessing API request json for modification.");
164        if let Some(partner_auth_token) = self.tokens.partner_token.clone() {
165            json_obj.insert(
166                "partnerAuthToken".to_string(),
167                serde_json::Value::String(partner_auth_token),
168            );
169        }
170        if let Some(user_auth_token) = self.tokens.user_token.clone() {
171            json_obj.insert(
172                "userAuthToken".to_string(),
173                serde_json::Value::String(user_auth_token),
174            );
175        }
176
177        if let Some(sync_time) = self.tokens.sync_time {
178            json_obj.insert("syncTime".to_string(), serde_json::Value::from(sync_time));
179        }
180    }
181
182    /// Build a reqwest::Request, which can be inspected, modified, and executed with
183    /// reqwest::Client::execute().
184    pub fn build(&mut self) -> reqwest::RequestBuilder {
185        self.add_session_tokens_to_args();
186        let mut url: url::Url = self.endpoint_url.clone();
187        url.query_pairs_mut().extend_pairs(&self.args);
188
189        self.add_session_tokens_to_json();
190        let mut body: String = self.json.to_string();
191        if cfg!(test) {
192            log::debug!("Request body: {:?}", body);
193        }
194        if self.encrypted {
195            body = self.tokens.encrypt(&body);
196            if cfg!(test) {
197                log::debug!("Encrypted body: {:?}", body);
198            }
199        }
200
201        self.client.post(url).body(body)
202    }
203}
204
205/// A generic type to aid in converting the returned Json document from a
206/// Pandora API call into a custom struct T that deserializes the content of
207/// the API call result.
208#[derive(Debug, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct PandoraResponse<T> {
211    /// The reported status of the call
212    pub stat: PandoraStatus,
213    /// The resulting content of the API call
214    pub result: Option<T>,
215    /// A message explaining the returned code
216    pub message: Option<String>,
217    /// A numeric error code
218    pub code: Option<u32>,
219}
220
221impl<T: serde::de::DeserializeOwned> From<PandoraResponse<T>>
222    for std::result::Result<T, JsonError>
223{
224    fn from(pandora_resp: PandoraResponse<T>) -> Self {
225        match pandora_resp {
226            PandoraResponse {
227                stat: PandoraStatus::Ok,
228                result: Some(result),
229                ..
230            } => Ok(result),
231            PandoraResponse {
232                stat: PandoraStatus::Ok,
233                result: None,
234                ..
235            } => {
236                let default_value = serde_json::json!({});
237                let deser = serde_json::from_value(default_value);
238                deser.map_err(|_| JsonError::new(None, Some(String::from("Invalid JSON content."))))
239            }
240            PandoraResponse { code, message, .. } => Err(JsonError::new(code, message)),
241        }
242    }
243}
244
245/// The status string returned by the Pandora JSON API call.
246#[derive(Debug, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub enum PandoraStatus {
249    /// The API method call succeeded
250    Ok,
251    /// The API method call failed
252    Fail,
253}
254
255/// A trait for accessing information and capabilities specific to each
256/// Pandora JSON API call, including the method name, the json body content,
257/// and whether the body content should be encrypted before transmission.
258///
259/// It also includes two convenience methods for submitting the request.
260#[async_trait::async_trait]
261pub trait PandoraJsonApiRequest: serde::ser::Serialize {
262    /// The type that the json response will be deserialized to.
263    type Response: Debug + serde::de::DeserializeOwned;
264    /// The Error type to be returned by fallible calls on this trait.
265    type Error: Debug
266        + From<serde_json::error::Error>
267        + From<reqwest::Error>
268        + From<JsonError>
269        + Send;
270
271    /// Returns the name of the Pandora JSON API call in the form that it must
272    /// appear when making that call.
273    fn get_method(&self) -> String;
274
275    /// Returns the root json Value that should be serialized into the body of
276    /// the API call.
277    fn get_json(&self) -> std::result::Result<serde_json::value::Value, Self::Error> {
278        serde_json::to_value(self).map_err(Self::Error::from)
279    }
280
281    /// Whether the json body of the API call is expected to be encrypted before
282    /// transmission.
283    fn encrypt_request(&self) -> bool {
284        false
285    }
286
287    /// Generate an HTTP request that, when send() is called on it, will submit
288    /// the built request.
289    fn request(
290        &self,
291        session: &mut PandoraSession,
292    ) -> std::result::Result<reqwest::RequestBuilder, Self::Error> {
293        let mut tmp_session = session.clone();
294        tmp_session
295            .arg("method", &self.get_method())
296            .json(self.get_json()?);
297        if self.encrypt_request() {
298            tmp_session.encrypted();
299        }
300        Ok(tmp_session.build())
301    }
302
303    /// Build the request, submit it, and extract the response content from the
304    /// body json, and deserialize it into the Self::Response type.
305    async fn response(
306        &self,
307        session: &mut PandoraSession,
308    ) -> std::result::Result<Self::Response, Self::Error> {
309        let response = self
310            .request(session)?
311            .send()
312            .await
313            .map_err(Self::Error::from)?;
314
315        let response_obj: PandoraResponse<Self::Response> = if cfg!(test) {
316            // Debugging support - output full response text before attempting
317            // deserialization
318            let response_body = response.text().await?;
319            if cfg!(test) {
320                log::debug!("Full response: {:?}", response_body);
321            }
322            serde_json::from_slice(response_body.as_bytes())?
323        } else {
324            // Regular builds just grab the json directly.
325            response.json().await?
326        };
327
328        if cfg!(test) {
329            log::debug!("Json response: {:?}", response_obj);
330        }
331
332        let result: std::result::Result<Self::Response, JsonError> = response_obj.into();
333        // Detect errors that indicate that our session tokens aren't valid, and clear them
334        match result {
335            Err(JsonError {
336                kind: JsonErrorKind::InvalidAuthToken,
337                message,
338            }) => {
339                session.session_tokens_mut().clear_partner_tokens();
340                session.session_tokens_mut().clear_user_tokens();
341                Err(JsonError {
342                    kind: JsonErrorKind::InvalidAuthToken,
343                    message,
344                })
345            }
346            Err(JsonError {
347                kind: JsonErrorKind::InsufficientConnectivity,
348                message,
349            }) => {
350                session.session_tokens_mut().clear_partner_tokens();
351                session.session_tokens_mut().clear_user_tokens();
352                Err(JsonError {
353                    kind: JsonErrorKind::InsufficientConnectivity,
354                    message,
355                })
356            }
357            res => res,
358        }
359        .map_err(Self::Error::from)
360    }
361}
362
363/// Trait for getting the JSON API endpoint specific to the partner account
364/// being used for the service
365pub trait ToEndpoint: serde::ser::Serialize {
366    /// Generate a string describing the API endpoint to be used.
367    fn to_endpoint(&self) -> String;
368    /// Generate a url::Url for the API endpoint to be used.
369    fn to_endpoint_url(&self) -> url::Url {
370        url::Url::parse(&self.to_endpoint()).expect("Error parsing Pandora endpoint url.")
371    }
372}
373
374impl ToEndpoint for String {
375    /// Generate a string describing the API endpoint to be used.
376    fn to_endpoint(&self) -> String {
377        self.clone()
378    }
379}
380
381/// This trait is used to provide access to the tokens needed to initiate
382/// a partnerLogin.
383pub trait ToEncryptionTokens {
384    /// Returns the encryption key to be used for this session.
385    fn to_encrypt_key(&self) -> String;
386    /// Encrypt the provided data using the session encryption key.
387    fn encrypt(&self, data: &str) -> String {
388        crypt::encrypt(&self.to_encrypt_key(), data)
389    }
390    /// Returns the decryption key to be used for this session.
391    fn to_decrypt_key(&self) -> String;
392    /// Decrypt the provided data using the session decryption key.
393    fn decrypt(&self, hex_data: &str) -> Vec<u8> {
394        crypt::decrypt(&self.to_decrypt_key(), hex_data)
395    }
396}
397
398/// This trait is used to provide access to all the tokens needed to track
399/// the partner (application) session.
400pub trait ToPartnerTokens {
401    /// Return the partner id for the session, if one has been already been set.
402    ///
403    /// Returns None otherwise.
404    fn to_partner_id(&self) -> Option<String>;
405
406    /// Return the partner token for the session, if one has been already been set.
407    ///
408    /// Returns None otherwise.
409    fn to_partner_token(&self) -> Option<String>;
410
411    /// Return the session sync time as a u64, if one has been already been set.
412    ///
413    /// Returns None otherwise.
414    fn to_sync_time(&self) -> Option<String>;
415}
416
417/// This trait is used to provide access to all the tokens needed to track
418/// the user session.
419pub trait ToUserTokens {
420    /// Return the user id for the session, if one has been already been set.
421    ///
422    /// Returns None otherwise.
423    fn to_user_id(&self) -> Option<String>;
424
425    /// Return the user token for the session, if one has been already been set.
426    ///
427    /// Returns None otherwise.
428    fn to_user_token(&self) -> Option<String>;
429}
430
431/// Trait for providing access to a station token.
432pub trait ToStationToken: serde::ser::Serialize {
433    /// Return the station token as a String.
434    fn to_station_token(&self) -> String;
435}
436
437impl ToStationToken for String {
438    /// Allow for using strings with functions accepting ToStationToken.
439    fn to_station_token(&self) -> String {
440        self.clone()
441    }
442}
443
444impl ToStationToken for &str {
445    /// Allow for using string slices with functions accepting ToStationToken.
446    fn to_station_token(&self) -> String {
447        // Clippy says it's faster to dereference self first before calling
448        // to_string() when self is a &&str
449        (*self).to_string()
450    }
451}
452
453/// Trait for providing access to one or more ad tracking tokens.
454pub trait ToTrackingToken: serde::ser::Serialize {
455    /// Return the ad tracking tokens as a String.
456    fn to_ad_tracking_tokens(&self) -> String;
457}
458
459impl ToTrackingToken for String {
460    /// Allow for using strings with functions accepting ToTrackingToken.
461    fn to_ad_tracking_tokens(&self) -> String {
462        self.clone()
463    }
464}
465
466impl ToTrackingToken for &str {
467    /// Allow for using string slices with functions accepting ToTrackingToken.
468    fn to_ad_tracking_tokens(&self) -> String {
469        // Clippy says it's faster to dereference self first before calling
470        // to_string() when self is a &&str
471        (*self).to_string()
472    }
473}
474
475/// A convenience type that can produce valid PartnerLogin instances for a
476/// number of different endpoints and device types.
477#[derive(Debug, Clone, Serialize)]
478#[serde(rename_all = "camelCase")]
479pub struct Partner {
480    /// The partner login name (not the account-holder's username)
481    /// used to authenticate the application with the Pandora service.
482    pub username: String,
483    /// The partner login password (not the account-holder's username)
484    /// used to authenticate the application with the Pandora service.
485    pub password: String,
486    /// The partner device model name.
487    pub device_model: String,
488    /// The Pandora JSON API version
489    pub version: String,
490    /// The encryption key associated with this partner login
491    #[serde(skip)]
492    pub encrypt_password: String,
493    /// The decryption key associated with this partner login
494    #[serde(skip)]
495    pub decrypt_password: String,
496    /// The hostname for the endpoint used to communicate with the Pandora API.
497    /// This is a bare hostname, without scheme/protocol.  This value will later
498    /// be combined with a scheme and path to create a complete, valid URL.
499    #[serde(skip)]
500    pub endpoint_host: String,
501}
502
503impl Partner {
504    /// Generate a Partner instance using the android app credentials
505    pub fn new_android() -> Self {
506        Self {
507            username: "android".to_string(),
508            password: "AC7IBG09A3DTSYM4R41UJWL07VLN8JI7".to_string(),
509            device_model: "android-generic".to_string(),
510            version: "5".to_string(),
511            decrypt_password: "R=U!LH$O2B#".to_string(),
512            encrypt_password: "6#26FRL$ZWD".to_string(),
513            endpoint_host: "tuner.pandora.com".to_string(),
514        }
515    }
516
517    /// Generate a Partner instance using the iOS app credentials
518    pub fn new_ios() -> Self {
519        Self {
520            username: "iphone".to_string(),
521            password: "P2E4FC0EAD3*878N92B2CDp34I0B1@388137C".to_string(),
522            device_model: "IP01".to_string(),
523            version: "5".to_string(),
524            decrypt_password: "20zE1E47BE57$51".to_string(),
525            encrypt_password: "721^26xE22776".to_string(),
526            endpoint_host: "tuner.pandora.com".to_string(),
527        }
528    }
529
530    /// Generate a Partner instance using the Palm Pre credentials
531    pub fn new_palm() -> Self {
532        Self {
533            username: "palm".to_string(),
534            password: "IUC7IBG09A3JTSYM4N11UJWL07VLH8JP0".to_string(),
535            device_model: "pre".to_string(),
536            version: "5".to_string(),
537            decrypt_password: "E#U$MY$O2B=".to_string(),
538            encrypt_password: "%526CBL$ZU3".to_string(),
539            endpoint_host: "tuner.pandora.com".to_string(),
540        }
541    }
542
543    /// Generate a Partner instance using the Windows Mobile credentials
544    pub fn new_windows_mobile() -> Self {
545        Self {
546            username: "winmo".to_string(),
547            password: "ED227E10a628EB0E8Pm825Dw7114AC39".to_string(),
548            device_model: "VERIZON_MOTOQ9C".to_string(),
549            version: "5".to_string(),
550            decrypt_password: "7D671jt0C5E5d251".to_string(),
551            encrypt_password: "v93C8C2s12E0EBD".to_string(),
552            endpoint_host: "tuner.pandora.com".to_string(),
553        }
554    }
555
556    /// Generate a Partner instance using the Desktop AIR credentials
557    pub fn new_desktop_air() -> Self {
558        Self {
559            username: "pandora one".to_string(),
560            password: "TVCKIBGS9AO9TSYLNNFUML0743LH82D".to_string(),
561            device_model: "D01".to_string(),
562            version: "5".to_string(),
563            decrypt_password: "U#IO$RZPAB%VX2".to_string(),
564            encrypt_password: "2%3WCL*JU$MP]4".to_string(),
565            endpoint_host: "internal-tuner.pandora.com".to_string(),
566        }
567    }
568
569    /// Generate a Partner instance using the Vista widget credentials
570    pub fn new_vista_widget() -> Self {
571        Self {
572            username: "windowsgadget".to_string(),
573            password: "EVCCIBGS9AOJTSYMNNFUML07VLH8JYP0".to_string(),
574            device_model: "WG01".to_string(),
575            version: "5".to_string(),
576            decrypt_password: "E#IO$MYZOAB%FVR2".to_string(),
577            encrypt_password: "%22CML*ZU$8YXP[1".to_string(),
578            endpoint_host: "internal-tuner.pandora.com".to_string(),
579        }
580    }
581
582    /// Initialize a PandoraSession using the corresponding Partner
583    /// tokens and endpoint.
584    pub fn init_session(&self) -> PandoraSession {
585        PandoraSession::new(None, self, self)
586    }
587
588    /// Generate a PartnerLogin instance from this object that can be
589    /// used for initiating authentication with the service.
590    pub fn to_partner_login(&self) -> PartnerLogin {
591        PartnerLogin {
592            username: self.username.clone(),
593            password: self.password.clone(),
594            device_model: self.device_model.clone(),
595            version: self.version.clone(),
596            optional: HashMap::new(),
597        }
598    }
599
600    /// Convenience method for submitting the partner login request for this
601    /// partner.
602    pub async fn login(&self, session: &mut PandoraSession) -> Result<PartnerLoginResponse, Error> {
603        let response = self.to_partner_login().response(session).await?;
604        session.update_partner_tokens(&response);
605        Ok(response)
606    }
607}
608
609impl Default for Partner {
610    /// Create a default Partner instance using the android device type.
611    fn default() -> Self {
612        Self::new_android()
613    }
614}
615
616impl ToEncryptionTokens for Partner {
617    fn to_encrypt_key(&self) -> String {
618        self.encrypt_password.clone()
619    }
620
621    fn to_decrypt_key(&self) -> String {
622        self.decrypt_password.clone()
623    }
624}
625
626impl ToEndpoint for Partner {
627    /// Returns the service endpoint to be used for this session as a String.
628    fn to_endpoint(&self) -> String {
629        format!("https://{}/services/json", self.endpoint_host)
630    }
631}
632
633/// A convenience type that holds all the values necessary to maintain an active
634/// session with the Pandora service.
635#[derive(Debug, Clone)]
636pub struct SessionTokens {
637    /// The key used to encrypt the body of certain API requests.
638    pub encrypt_key: String,
639    /// The key used to decrypt certain values from the body of certain API
640    /// responses.
641    pub decrypt_key: String,
642    /// The partner id token returned by the partner login request
643    pub partner_id: Option<String>,
644    /// The partner auth token returned by the partner login request
645    pub partner_token: Option<String>,
646    /// The sync time token returned by the partner login request.  This is
647    /// private so that it will be updated/read by accessor methods that
648    /// correctly adjust for the time offset that needs to be added on.
649    ///
650    /// Also note that this is the decrypted form, not the encrypted bytes
651    /// passed around on types that implement ToPartnerTokens.
652    sync_time: Option<u64>,
653    /// The instant when the sync_time was set, so that when we return sync_time,
654    /// we return the value offset by however much time has passed since we were
655    /// issued the token.
656    local_time_base: Option<std::time::Instant>,
657    /// The user id token returned by the user login request
658    pub user_id: Option<String>,
659    /// The user auth token returned by the user login request
660    pub user_token: Option<String>,
661}
662
663impl SessionTokens {
664    /// Initialize a SessionTokens object with only the encryption keys,
665    /// as those are needed even before authentication begins
666    pub fn new<T: ToEncryptionTokens>(to_encryption_tokens: &T) -> Self {
667        Self {
668            encrypt_key: to_encryption_tokens.to_encrypt_key(),
669            decrypt_key: to_encryption_tokens.to_decrypt_key(),
670            partner_id: None,
671            partner_token: None,
672            sync_time: None,
673            local_time_base: None,
674            user_id: None,
675            user_token: None,
676        }
677    }
678
679    /// Update the current SessionTokens instance using values from the
680    /// response to the PartnerLogin request.
681    pub fn update_partner_tokens<T: ToPartnerTokens>(&mut self, to_partner_tokens: &T) {
682        self.partner_id = to_partner_tokens.to_partner_id();
683        self.partner_token = to_partner_tokens.to_partner_token();
684        // The first four bytes are, reportedly, garbage, but I suspect it's
685        // actually supposed to function as a salt that was intended to make it
686        // difficult to recover the decryption keys.
687        if let Some(sync_time) = to_partner_tokens.to_sync_time() {
688            let sync_time_bytes: Vec<u8> =
689                self.decrypt(&sync_time).iter().skip(4).cloned().collect();
690            let sync_time_str = std::str::from_utf8(&sync_time_bytes).unwrap_or("0");
691            self.set_sync_time(sync_time_str.parse::<u64>().unwrap_or(0));
692        }
693    }
694
695    /// Update the current SessionTokens instance using values from the
696    /// response to the UserLogin request.
697    pub fn update_user_tokens<T: ToUserTokens>(&mut self, to_user_tokens: &T) {
698        self.user_id = to_user_tokens.to_user_id();
699        self.user_token = to_user_tokens.to_user_token();
700    }
701
702    /// The current server time as of the last request.  Submitted requests must
703    /// include a value of syncTime that corresponds to the new server time,
704    /// based on the amount of time elapsed since authenticating.
705    pub fn set_sync_time(&mut self, sync_time: u64) {
706        self.local_time_base = Some(std::time::Instant::now());
707        self.sync_time = Some(sync_time);
708    }
709
710    /// Clear the session syncTime base.
711    pub fn clear_sync_time(&mut self) {
712        self.local_time_base = None;
713        self.sync_time = None;
714    }
715
716    /// Returns the current syncTime relative to the
717    pub fn get_sync_time(&self) -> Option<u64> {
718        self.sync_time
719            .and_then(|st| self.local_time_base.map(|ltb| ltb.elapsed().as_secs() + st))
720    }
721
722    /// Clears all active partner session tokens.
723    pub fn clear_partner_tokens(&mut self) {
724        self.partner_id = None;
725        self.partner_token = None;
726        self.clear_sync_time();
727    }
728
729    /// Clears all active user session tokens.
730    pub fn clear_user_tokens(&mut self) {
731        self.user_id = None;
732        self.user_token = None;
733    }
734}
735
736impl ToEncryptionTokens for SessionTokens {
737    /// Retrieve the encryption key for this session
738    fn to_encrypt_key(&self) -> String {
739        self.encrypt_key.clone()
740    }
741    /// Retrieve the decryption key for this session
742    fn to_decrypt_key(&self) -> String {
743        self.decrypt_key.clone()
744    }
745}
746
747impl<T: ToEncryptionTokens> From<&T> for SessionTokens {
748    fn from(tokens: &T) -> Self {
749        Self::new(tokens)
750    }
751}
752
753/// A representation of a moment in time.
754#[derive(Debug, Clone, Deserialize)]
755#[serde(rename_all = "camelCase")]
756pub struct Timestamp {
757    /// The offset from UTC in minutes
758    _timezone_offset: u32,
759    /// Unix epoch time for the timezone offset
760    time: i64,
761    /// Year, adjusted for timezone offset
762    _year: u32,
763    /// Month, adjusted for timezone offset
764    _month: u8,
765    /// Day of month, adjusted for timezone offset
766    _day: u8,
767    /// Hour, adjusted for timezone offset
768    _hours: u8,
769    /// Minute, adjusted for timezone offset
770    _minutes: u8,
771    /// Seconds, adjusted for timezone offset
772    _seconds: u8,
773    /// Unknown
774    _date: u8,
775}
776
777impl From<Timestamp> for chrono::DateTime<chrono::Utc> {
778    fn from(ts: Timestamp) -> chrono::DateTime<chrono::Utc> {
779        // TODO: Figure out proper handling of timezoneOffset
780        // e.g. is it signed? is the provided time Utc (and offset is applied
781        // to get local) or is it local (and tells the offset used to determine
782        // local)? is it the local time of the user, or the local time for the
783        // system that generated the timestamp?
784        let dt = chrono::DateTime::from_timestamp(ts.time, 0).expect("Invalid date/time timestamp");
785        let naive_utc = dt.naive_utc();
786        let offset = *dt.offset();
787        chrono::DateTime::from_naive_utc_and_offset(naive_utc, offset)
788    }
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    use crate::errors::Error;
796    use crate::json::auth::user_login;
797
798    // TODO: lazy_static create a single session and return a RcRefCell to
799    // it instead.  I suspect that some of the transient
800    // InsufficientConnectivity errors are resulting from simultaneously
801    // creating a large number of sessions, creating race conditions or
802    // invalidating tokens.
803    pub async fn session_login(partner: &Partner) -> Result<PandoraSession, Error> {
804        let mut session = partner.init_session();
805        let _partner_login = partner.login(&mut session).await?;
806
807        let test_username_raw = include_str!("../../test_username.txt");
808        let test_username = test_username_raw.trim();
809        let test_password_raw = include_str!("../../test_password.txt");
810        let test_password = test_password_raw.trim();
811
812        let user_login = user_login(&mut session, &test_username, &test_password).await?;
813        session.update_user_tokens(&user_login);
814        Ok(session)
815    }
816
817    #[tokio::test]
818    async fn partner_test() {
819        let partner = Partner::default();
820        let mut session = partner.init_session();
821        let partner_login = partner
822            .login(&mut session)
823            .await
824            .expect("Failed while performing partner login");
825        session.update_partner_tokens(&partner_login);
826    }
827}