pandora_api/json/
auth.rs

1/*!
2Authentication/authorization support messages.
3*/
4// SPDX-License-Identifier: MIT AND WTFPL
5
6use std::collections::HashMap;
7
8use pandora_api_derive::PandoraJsonRequest;
9use serde::{Deserialize, Serialize};
10
11use crate::errors::Error;
12use crate::json::{PandoraJsonApiRequest, PandoraSession, ToPartnerTokens, ToUserTokens};
13
14/// **Unsupported!**
15/// Undocumented method
16/// [auth.getAdMetadata()](https://6xq.net/pandora-apidoc/json/methods/)
17pub struct GetAdMetadataUnsupported {}
18
19/// **Unsupported!**
20/// Undocumented method
21/// [auth.partnerAdminLogin()](https://6xq.net/pandora-apidoc/json/methods/)
22pub struct PartnerAdminLoginUnsupported {}
23
24/// This request additionally serves as API version validation, time synchronization and endpoint detection and must be sent over a TLS-encrypted link. The POST body however is not encrypted.
25///
26/// | Name | Type | Description |
27/// | username | string | See Partner passwords |
28/// | password | string | See Partner passwords |
29/// | deviceModel | string | See Partner passwords |
30/// | version | string | Current version number, “5”. |
31/// | includeUrls | boolean |  |
32/// | returnDeviceType | boolean |  |
33/// | returnUpdatePromptVersions | boolean |  |
34/// ``` json
35/// {
36///     "username": "pandora one",
37///     "password": "TVCKIBGS9AO9TSYLNNFUML0743LH82D",
38///     "deviceModel": "D01",
39///     "version": "5"
40/// }
41/// ```
42#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
43#[serde(rename_all = "camelCase")]
44pub struct PartnerLogin {
45    /// The partner login name (not the account-holder's username)
46    /// used to authenticate the application with the Pandora service.
47    pub username: String,
48    /// The partner login password (not the account-holder's username)
49    /// used to authenticate the application with the Pandora service.
50    pub password: String,
51    /// The partner device model name.
52    pub device_model: String,
53    /// The Pandora JSON API version
54    pub version: String,
55    /// Optional parameters on the call
56    #[serde(flatten)]
57    pub optional: HashMap<String, serde_json::value::Value>,
58}
59
60impl PartnerLogin {
61    /// Create a new PartnerLogin with some values. All Optional fields are
62    /// set to None.
63    pub fn new(
64        username: &str,
65        password: &str,
66        device_model: &str,
67        version: Option<String>,
68    ) -> Self {
69        PartnerLogin {
70            username: username.to_string(),
71            password: password.to_string(),
72            device_model: device_model.to_string(),
73            version: version.unwrap_or_else(|| String::from("5")),
74            optional: HashMap::new(),
75        }
76    }
77
78    /// Convenience function for setting boolean flags in the request. (Chaining call)
79    pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
80        self.optional
81            .insert(option.to_string(), serde_json::value::Value::from(value));
82        self
83    }
84
85    /// Whether to request to include urls in the response. (Chaining call)
86    pub fn include_urls(self, value: bool) -> Self {
87        self.and_boolean_option("includeUrls", value)
88    }
89
90    /// Whether to request to include the device type in the response. (Chaining call)
91    pub fn return_device_type(self, value: bool) -> Self {
92        self.and_boolean_option("returnDeviceType", value)
93    }
94
95    /// Whether to request to return a prompt to update versions in the response. (Chaining call)
96    pub fn return_update_prompt_versions(self, value: bool) -> Self {
97        self.and_boolean_option("returnUpdatePromptVersions", value)
98    }
99
100    /// This is a wrapper around the `response` method from the
101    /// PandoraJsonApiRequest trait that automatically merges the partner tokens
102    /// from the response back into the session.
103    pub async fn merge_response(
104        &self,
105        session: &mut PandoraSession,
106    ) -> Result<PartnerLoginResponse, Error> {
107        let response = self.response(session).await?;
108        session.update_partner_tokens(&response);
109        Ok(response)
110    }
111}
112
113/// syncTime is used to calculate the server time, see synctime. partnerId and authToken are required to proceed with user authentication.
114///
115/// | Name | Type | Description |
116/// | syncTime | string | Hex-encoded, encrypted server time. Decrypt with password from Partner passwords and skip first four bytes of garbage. |
117/// | partnerAuthToken | string |   |
118/// | partnerId | string |   |
119/// ``` json
120/// {
121///     "stat": "ok",
122///     "result": {
123///         "syncTime": "6923e263a8c3ac690646146b50065f43",
124///         "deviceProperties": {
125///             "videoAdRefreshInterval": 900,
126///             "videoAdUniqueInterval": 0,
127///             "adRefreshInterval": 5,
128///             "videoAdStartInterval": 180
129///         },
130///         "partnerAuthToken": "VAzrFQTtsy3BQ3K+3iqFi0WF5HA63B1nFA",
131///         "partnerId": "42",
132///         "stationSkipUnit": "hour",
133///         "urls": {
134///             "autoComplete": "http://autocomplete.pandora.com/search"
135///         },
136///         "stationSkipLimit": 6
137///     }
138/// }
139/// ```
140/// | Code | Description |
141/// | 1002 | INVALID_PARTNER_LOGIN. Invalid partner credentials. |
142#[derive(Debug, Clone, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct PartnerLoginResponse {
145    /// The partner id that should be used for this session
146    pub partner_id: String,
147    /// The partner auth token that should be used for this session
148    pub partner_auth_token: String,
149    /// The server sync time that should be used for this session
150    /// Note that this field is encrypted, and must be decrypted before use
151    pub sync_time: String,
152    /// Unknown field
153    pub station_skip_unit: String,
154    /// Unknown field
155    pub station_skip_limit: u32,
156    /// Unknown field
157    pub urls: Option<HashMap<String, String>>,
158    /// Optional response fields
159    #[serde(flatten)]
160    pub optional: HashMap<String, serde_json::value::Value>,
161}
162
163impl ToPartnerTokens for PartnerLoginResponse {
164    fn to_partner_id(&self) -> Option<String> {
165        Some(self.partner_id.clone())
166    }
167
168    fn to_partner_token(&self) -> Option<String> {
169        Some(self.partner_auth_token.clone())
170    }
171
172    fn to_sync_time(&self) -> Option<String> {
173        Some(self.sync_time.clone())
174    }
175}
176
177/// Convenience function to do a basic partnerLogin call.
178pub async fn partner_login(
179    session: &mut PandoraSession,
180    username: &str,
181    password: &str,
182    device_model: &str,
183) -> Result<PartnerLoginResponse, Error> {
184    PartnerLogin::new(username, password, device_model, None)
185        .include_urls(false)
186        .return_device_type(false)
187        .return_update_prompt_versions(false)
188        .merge_response(session)
189        .await
190}
191
192/// This request *must* be sent over a TLS-encrypted link. It authenticates the Pandora user by sending his username, usually his email address, and password as well as the partnerAuthToken obtained by Partner login.
193///
194/// Additional response data can be requested by setting flags listed below.
195///
196/// | Name | Type | Description |
197/// | loginType | string | “user” |
198/// | username | string | Username |
199/// | password | string | User’s password |
200/// | partnerAuthToken | string | Partner token obtained by Partner login |
201/// | returnGenreStations | boolean | (optional) |
202/// | returnCapped | boolean | return isCapped parameter (optional) |
203/// | includePandoraOneInfo | boolean | (optional) |
204/// | includeDemographics | boolean | (optional) |
205/// | includeAdAttributes | boolean | (optional) |
206/// | returnStationList | boolean | Return station list, see Retrieve station list (optional) |
207/// | includeStationArtUrl | boolean | (optional) |
208/// | includeStationSeeds | boolean | (optional) |
209/// | includeShuffleInsteadOfQuickMix | boolean | (optional) |
210/// | stationArtSize | string | W130H130(optional) |
211/// | returnCollectTrackLifetimeStats | boolean | (optional) |
212/// | returnIsSubscriber | boolean | (optional) |
213/// | xplatformAdCapable | boolean | (optional) |
214/// | complimentarySponsorSupported | boolean | (optional) |
215/// | includeSubscriptionExpiration | boolean | (optional) |
216/// | returnHasUsedTrial | boolean | (optional) |
217/// | returnUserstate | boolean | (optional) |
218/// | includeAccountMessage | boolean | (optional) |
219/// | includeUserWebname | boolean | (optional) |
220/// | includeListeningHours | boolean | (optional) |
221/// | includeFacebook | boolean | (optional) |
222/// | includeTwitter | boolean | (optional) |
223/// | includeDailySkipLimit | boolean | (optional) |
224/// | includeSkipDelay | boolean | (optional) |
225/// | includeGoogleplay | boolean | (optional) |
226/// | includeShowUserRecommendations | boolean | (optional) |
227/// | includeAdvertiserAttributes | boolean | (optional) |
228/// ``` json
229/// {
230///    "loginType": "user",
231///    "username": "user@example.com",
232///    "password": "example",
233///    "partnerAuthToken": "VAzrFQTtsy3BQ3K+3iqFi0WF5HA63B1nFA",
234///    "includePandoraOneInfo":true,
235///    "includeAdAttributes":true,
236///    "includeSubscriptionExpiration":true,
237///    "includeStationArtUrl":true,
238///    "returnStationList":true,
239///    "returnGenreStations":true,
240///    "syncTime": 1335777573
241/// }
242/// ```
243#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
244#[pandora_request(encrypted = true)]
245#[serde(rename_all = "camelCase")]
246pub struct UserLogin {
247    /// This field should always have the value `user`.
248    pub login_type: String,
249    /// The account username to login with.
250    pub username: String,
251    /// The account password to login with.
252    pub password: String,
253    /// Optional parameters on the call
254    #[serde(flatten)]
255    pub optional: HashMap<String, serde_json::value::Value>,
256}
257
258impl UserLogin {
259    /// Initialize a basic UserLogin request. All optional fields are set to None.
260    pub fn new(username: &str, password: &str) -> Self {
261        UserLogin {
262            // This field should always have the value `user`.
263            login_type: "user".to_string(),
264            username: username.to_string(),
265            password: password.to_string(),
266            optional: HashMap::new(),
267        }
268    }
269
270    /// Convenience function for setting boolean flags in the request. (Chaining call)
271    pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
272        self.optional
273            .insert(option.to_string(), serde_json::value::Value::from(value));
274        self
275    }
276
277    /// Convenience function for setting string flags in the request. (Chaining call)
278    pub fn and_string_option(mut self, option: &str, value: &str) -> Self {
279        self.optional
280            .insert(option.to_string(), serde_json::value::Value::from(value));
281        self
282    }
283
284    /// Whether request should return genre stations in the response. (Chaining call)
285    pub fn return_genre_stations(self, value: bool) -> Self {
286        self.and_boolean_option("returnGenreStations", value)
287    }
288
289    /// Whether request should return capped in the response. (Chaining call)
290    pub fn return_capped(self, value: bool) -> Self {
291        self.and_boolean_option("returnCapped", value)
292    }
293
294    /// Whether request should include PandoraOne info in the response. (Chaining call)
295    pub fn include_pandora_one_info(self, value: bool) -> Self {
296        self.and_boolean_option("includePandoraOneInfo", value)
297    }
298
299    /// Whether request should include demographics in the response. (Chaining call)
300    pub fn include_demographics(self, value: bool) -> Self {
301        self.and_boolean_option("includeDemographics", value)
302    }
303
304    /// Whether request should include ad attributes in the response. (Chaining call)
305    pub fn include_ad_attributes(self, value: bool) -> Self {
306        self.and_boolean_option("includeAdAttributes", value)
307    }
308
309    /// Whether request should return station list in the response. (Chaining call)
310    pub fn return_station_list(self, value: bool) -> Self {
311        self.and_boolean_option("returnStationList", value)
312    }
313
314    /// Whether request should include the station art url in the response. (Chaining call)
315    pub fn include_station_art_url(self, value: bool) -> Self {
316        self.and_boolean_option("includeStationArtUrl", value)
317    }
318
319    /// Whether request should include the station seeds in the response. (Chaining call)
320    pub fn include_station_seeds(self, value: bool) -> Self {
321        self.and_boolean_option("includeStationSeeds", value)
322    }
323
324    /// Whether request should include shuffle stations instead of quickmix in the response. (Chaining call)
325    pub fn include_shuffle_instead_of_quick_mix(self, value: bool) -> Self {
326        self.and_boolean_option("includeShuffleInsteadOfQuickMix", value)
327    }
328
329    /// The size of station art to include in the response (if includeStationArlUrl was set). (Chaining call)
330    pub fn station_art_size(self, value: &str) -> Self {
331        self.and_string_option("includeShuffleInsteadOfQuickMix", value)
332    }
333
334    /// Whether request should return collect track lifetime stats in the response. (Chaining call)
335    pub fn return_collect_track_lifetime_stats(self, value: bool) -> Self {
336        self.and_boolean_option("returnCollectTrackLifetimeStats", value)
337    }
338
339    /// Whether request should return whether the user is a subscriber in the response. (Chaining call)
340    pub fn return_is_subscriber(self, value: bool) -> Self {
341        self.and_boolean_option("returnIsSubscriber", value)
342    }
343
344    /// Whether the requesting client is cross-platform ad capable. (Chaining call)
345    pub fn xplatform_ad_capable(self, value: bool) -> Self {
346        self.and_boolean_option("xplatformAdCapable", value)
347    }
348
349    /// Whether the complimentary sponsors are supported. (Chaining call)
350    pub fn complimentary_sponsor_supported(self, value: bool) -> Self {
351        self.and_boolean_option("complimentarySponsorSupported", value)
352    }
353
354    /// Whether request should include subscription expiration in the response. (Chaining call)
355    pub fn include_subscription_expiration(self, value: bool) -> Self {
356        self.and_boolean_option("includeSubscriptionExpiration", value)
357    }
358
359    /// Whether request should return whether the user has used their trial
360    /// subscription in the response. (Chaining call)
361    pub fn return_has_used_trial(self, value: bool) -> Self {
362        self.and_boolean_option("returnHasUsedTrial", value)
363    }
364
365    /// Whether request should return user state in the response. (Chaining call)
366    pub fn return_userstate(self, value: bool) -> Self {
367        self.and_boolean_option("returnUserstate", value)
368    }
369
370    /// Whether request should return account message in the response. (Chaining call)
371    pub fn include_account_message(self, value: bool) -> Self {
372        self.and_boolean_option("includeAccountMessage", value)
373    }
374
375    /// Whether request should include user webname in the response. (Chaining call)
376    pub fn include_user_webname(self, value: bool) -> Self {
377        self.and_boolean_option("includeUserWebname", value)
378    }
379
380    /// Whether request should include listening hours in the response. (Chaining call)
381    pub fn include_listening_hours(self, value: bool) -> Self {
382        self.and_boolean_option("includeListeningHours", value)
383    }
384
385    /// Whether request should include facebook connections in the response. (Chaining call)
386    pub fn include_facebook(self, value: bool) -> Self {
387        self.and_boolean_option("includeFacebook", value)
388    }
389
390    /// Whether request should include twitter connections in the response. (Chaining call)
391    pub fn include_twitter(self, value: bool) -> Self {
392        self.and_boolean_option("includeTwitter", value)
393    }
394
395    /// Whether request should include daily skip limit in the response. (Chaining call)
396    pub fn include_daily_skip_limit(self, value: bool) -> Self {
397        self.and_boolean_option("includeDailySkipLimit", value)
398    }
399
400    /// Whether request should include the track skip delay in the response. (Chaining call)
401    pub fn include_skip_delay(self, value: bool) -> Self {
402        self.and_boolean_option("includeSkipDelay", value)
403    }
404
405    /// Whether request should include Google Play metadata in the response. (Chaining call)
406    pub fn include_googleplay(self, value: bool) -> Self {
407        self.and_boolean_option("includeGoogleplay", value)
408    }
409
410    /// Whether request should include the user recommendations in the response. (Chaining call)
411    pub fn include_show_user_recommendations(self, value: bool) -> Self {
412        self.and_boolean_option("includeShowUserRecommendations", value)
413    }
414
415    /// Whether request should include advertiser attributes in the response. (Chaining call)
416    pub fn include_advertiser_attributes(self, value: bool) -> Self {
417        self.and_boolean_option("includeAdvertiserAttributes", value)
418    }
419
420    /// This is a wrapper around the `response` method from the
421    /// PandoraJsonApiRequest trait that automatically merges the user tokens from
422    /// the response back into the session.
423    pub async fn merge_response(
424        &self,
425        session: &mut PandoraSession,
426    ) -> Result<UserLoginResponse, Error> {
427        let response = self.response(session).await?;
428        session.update_user_tokens(&response);
429        Ok(response)
430    }
431}
432
433/// The returned userAuthToken is used to authenticate access to other API methods.
434///
435/// | Name | Type | Description |
436/// | isCapped | boolean |  |
437/// | userAuthToken | string |  |
438/// ``` json
439/// {
440///    "stat": "ok",
441///    "result": {
442///        "stationCreationAdUrl": "http://ad.doubleclick.net/adx/pand.android/prod.createstation;ag=112;gnd=1;zip=23950;genre=0;model=;app=;OS=;dma=560;clean=0;logon=__LOGON__;tile=1;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*2jedvn446s7ce!ag*112!gnd*1!zip*23950!dma*560!clean*0!logon*__LOGON__!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!genre*0;sz=320x50;ord=__CACHEBUST__",
443///        "hasAudioAds": true,
444///        "splashScreenAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.welcome;ag=112;gnd=1;zip=23950;model=;app=;OS=;dma=560;clean=0;hours=1;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*op4jfgdxmddjk!ag*112!gnd*1!zip*23950!dma*560!clean*0!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!hours*1;sz=320x50;ord=__CACHEBUST__",
445///        "videoAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.nowplaying;ag=112;gnd=1;zip=23950;dma=560;clean=0;hours=1;app=;index=__INDEX__;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*2jedvn446s7ce!ag*112!gnd*1!zip*23950!dma*560!clean*0!index*__INDEX__!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!hours*1;sz=442x188;ord=__CACHEBUST__",
446///        "username": "user@example.com",
447///        "canListen": true,
448///        "nowPlayingAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.nowplaying;ag=112;gnd=1;zip=23950;genre=0;station={4};model=;app=;OS=;dma=560;clean=0;hours=1;artist=;interaction=__INTERACTION__;index=__INDEX__;newUser=__AFTERREG__;logon=__LOGON__;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*op4jfgdxmddjk!ag*112!gnd*1!zip*23950!station*{4}!dma*560!clean*0!index*__INDEX__!newUser*__AFTERREG__!logon*__LOGON__!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!genre*0!interaction*__INTERACTION__!hours*1;sz=320x50;ord=__CACHEBUST__",
449///        "userId": "272772589",
450///        "listeningTimeoutMinutes": "180",
451///        "maxStationsAllowed": 100,
452///        "listeningTimeoutAlertMsgUri": "/mobile/still_listening.vm",
453///        "userProfileUrl": "https://www.pandora.com/login?auth_token=XXX&target=%2Fpeople%2FXXX",
454///        "minimumAdRefreshInterval": 5,
455///        "userAuthToken": "XXX"
456///    }
457/// }
458/// ```
459/// | Code | Description |
460/// | 1002 | Wrong user credentials. |
461#[derive(Debug, Clone, Deserialize)]
462#[serde(rename_all = "camelCase")]
463pub struct UserLoginResponse {
464    /// The user id that should be used for this session
465    pub user_id: String,
466    /// The user auth token that should be used for this session
467    pub user_auth_token: String,
468    /// Unknown field.
469    pub station_creation_ad_url: String,
470    /// Unknown field.
471    pub has_audio_ads: bool,
472    /// Unknown field.
473    pub splash_screen_ad_url: String,
474    /// Unknown field.
475    pub video_ad_url: String,
476    /// Unknown field.
477    pub username: String,
478    /// Unknown field.
479    pub can_listen: bool,
480    /// Unknown field.
481    pub listening_timeout_minutes: String,
482    /// Unknown field.
483    pub max_stations_allowed: u32,
484    /// Unknown field.
485    pub listening_timeout_alert_msg_uri: String,
486    /// Unknown field.
487    pub user_profile_url: String,
488    /// Unknown field.
489    pub minimum_ad_refresh_interval: u32,
490    /// Additional optional fields that may appear in the response.
491    #[serde(flatten)]
492    pub optional: HashMap<String, serde_json::value::Value>,
493}
494
495impl ToUserTokens for UserLoginResponse {
496    fn to_user_id(&self) -> Option<String> {
497        Some(self.user_id.clone())
498    }
499
500    fn to_user_token(&self) -> Option<String> {
501        Some(self.user_auth_token.clone())
502    }
503}
504
505/// Convenience function to perform a basic user login.
506pub async fn user_login(
507    session: &mut PandoraSession,
508    username: &str,
509    password: &str,
510) -> Result<UserLoginResponse, Error> {
511    UserLogin::new(username, password)
512        .return_genre_stations(false)
513        .return_capped(false)
514        .include_pandora_one_info(false)
515        .include_demographics(false)
516        .include_ad_attributes(false)
517        .return_station_list(false)
518        .include_station_art_url(false)
519        .include_station_seeds(false)
520        .include_shuffle_instead_of_quick_mix(false)
521        .return_collect_track_lifetime_stats(false)
522        .return_is_subscriber(false)
523        .xplatform_ad_capable(false)
524        .complimentary_sponsor_supported(false)
525        .include_subscription_expiration(false)
526        .return_has_used_trial(false)
527        .return_userstate(false)
528        .include_account_message(false)
529        .include_user_webname(false)
530        .include_listening_hours(false)
531        .include_facebook(false)
532        .include_twitter(false)
533        .include_daily_skip_limit(false)
534        .include_skip_delay(false)
535        .include_googleplay(false)
536        .include_show_user_recommendations(false)
537        .include_advertiser_attributes(false)
538        .merge_response(session)
539        .await
540}
541
542#[cfg(test)]
543mod tests {
544    use crate::json::{tests::session_login, Partner};
545
546    // Tests both PartnerLogin and UserLogin
547    #[tokio::test]
548    async fn auth_test() {
549        /*
550        flexi_logger::Logger::try_with_str("info, pandora_api=debug")
551            .expect("Failed to set logging configuration")
552            .start()
553            .expect("Failed to start logger");
554        */
555
556        let partner = Partner::default();
557        let session = session_login(&partner)
558            .await
559            .expect("Failed initializing login session");
560        log::debug!("Session tokens: {:?}", session);
561    }
562}