Skip to main content

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