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}