diff --git a/src/cli.rs b/src/cli.rs index de1e67a..d6a1133 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -43,7 +43,7 @@ pub fn app() -> Command { .long("app") .action(ArgAction::Set) .conflicts_with("csv") - .required_unless_present("csv"), + .required_unless_present_any(["csv", "google_oauth_token"]), ) .arg( Arg::new("csv") @@ -105,19 +105,31 @@ pub fn app() -> Command { .required(false), ) .arg( - Arg::new("google_username") - .help("Google Username (required if download source is Google Play)") - .short('u') - .long("username") + Arg::new("google_oauth_token") + .help("Google oauth token, required to retrieve long-lived aas token") + .long("oauth-token") .action(ArgAction::Set) ) .arg( - Arg::new("google_password") - .help("Google App Password (required if download source is Google Play)") - .short('p') - .long("password") + Arg::new("google_email") + .help("Google account email address (required if download source is Google Play)") + .short('e') + .long("email") .action(ArgAction::Set) ) + .arg( + Arg::new("google_aas_token") + .help("Google aas token (required if download source is Google Play)") + .short('t') + .long("aas-token") + .action(ArgAction::Set) + ) + .arg( + Arg::new("google_accept_tos") + .help("Accept Google Play Terms of Service") + .long("accept-tos") + .action(ArgAction::SetTrue) + ) .arg( Arg::new("sleep_duration") .help("Sleep duration (in ms) before download requests") @@ -141,6 +153,7 @@ pub fn app() -> Command { Arg::new("OUTPATH") .help("Path to store output files") .action(ArgAction::Set) - .index(1), + .index(1) + .required_unless_present("google_oauth_token"), ) } diff --git a/src/google_play.rs b/src/google_play.rs index 6946c3b..8c22c13 100644 --- a/src/google_play.rs +++ b/src/google_play.rs @@ -14,14 +14,13 @@ pub async fn download_apps( apps: Vec<(String, Option)>, parallel: usize, sleep_duration: u64, - username: &str, - password: &str, + email: &str, + aas_token: &str, outpath: &Path, + accept_tos: bool, mut options: HashMap<&str, &str>, ) { - let locale = options.remove("locale").unwrap_or("en_US"); - let timezone = options.remove("timezone").unwrap_or("UTC"); - let device = options.remove("device").unwrap_or("hero2lte"); + let device = options.remove("device").unwrap_or("px_7a"); let split_apk = match options.remove("split_apk") { Some(val) if val == "1" || val.to_lowercase() == "true" => true, _ => false, @@ -30,14 +29,43 @@ pub async fn download_apps( Some(val) if val == "1" || val.to_lowercase() == "true" => true, _ => false, }; - let mut gpa = Gpapi::new(locale, timezone, device); + let mut gpa = Gpapi::new(device, email); - if let Err(err) = gpa.login(username, password).await { + if let Some(locale) = options.remove("locale") { + gpa.set_locale(locale); + } + if let Some(timezone) = options.remove("timezone") { + gpa.set_timezone(timezone); + } + + gpa.set_aas_token(aas_token); + if let Err(err) = gpa.login().await { match err.kind() { - GpapiErrorKind::SecurityCheck | GpapiErrorKind::EncryptLogin => println!("{}", err), - _ => println!("Could not log in to Google Play. Please check your credentials and try again later."), + GpapiErrorKind::TermsOfService => { + if accept_tos { + match gpa.accept_tos().await { + Ok(_) => { + if let Err(_) = gpa.login().await { + println!("Could not log in, even after accepting the Google Play Terms of Service"); + std::process::exit(1); + } + println!("Google Play Terms of Service accepted."); + }, + _ => { + println!("Could not accept Google Play Terms of Service"); + std::process::exit(1); + }, + } + } else { + println!("{}\nPlease read the ToS here: https://play.google.com/about/play-terms/index.html\nIf you accept, please pass the --accept-tos flag.", err); + std::process::exit(1); + } + }, + _ => { + println!("Could not log in to Google Play. Please check your credentials and try again later. {}", err); + std::process::exit(1); + } } - std::process::exit(1); } let mp = Rc::new(MultiProgress::new()); @@ -95,6 +123,24 @@ pub async fn download_apps( ).buffer_unordered(parallel).collect::>().await; } +pub async fn request_aas_token( + email: &str, + oauth_token: &str, + mut options: HashMap<&str, &str>, +) { + let device = options.remove("device").unwrap_or("px_7a"); + let mut api = Gpapi::new(device, email); + match api.request_aas_token(oauth_token).await { + Ok(()) => { + let aas_token = api.get_aas_token().unwrap(); + println!("AAS Token: {}", aas_token); + }, + Err(_) => { + println!("Error: was not able to retrieve AAS token with the provided OAuth token. Please provide new OAuth token and try again."); + } + } +} + pub fn list_versions(apps: Vec<(String, Option)>) { for app in apps { let (app_id, _) = app; diff --git a/src/main.rs b/src/main.rs index c253752..390a801 100644 --- a/src/main.rs +++ b/src/main.rs @@ -200,7 +200,7 @@ async fn main() { }; let matches = cli::app().get_matches(); - let download_source = *matches.get_one::("download_source").unwrap(); + let mut download_source = *matches.get_one::("download_source").unwrap(); let options: HashMap<&str, &str> = match matches.get_one::("options") { Some(options) => { let mut options_map = HashMap::new(); @@ -216,43 +216,50 @@ async fn main() { }, None => HashMap::new() }; - let list = match matches.get_one::("app") { - Some(app) => { - let mut app_vec: Vec = app.splitn(2, '@').map(String::from).collect(); - let app_id = app_vec.remove(0); - let app_version = match app_vec.len() { - 1 => Some(app_vec.remove(0)), - _ => None, - }; - vec![(app_id, app_version)] - }, - None => { - let csv = matches.get_one::("csv").unwrap(); - let field = *matches.get_one::("field").unwrap(); - let version_field = matches.get_one::("version_field").map(|v| *v); - if field < 1 { - println!("{}\n\nApp ID field must be 1 or greater", usage); - std::process::exit(1); - } - if let Some(version_field) = version_field { - if version_field < 1 { - println!("{}\n\nVersion field must be 1 or greater", usage); + + let oauth_token = matches.get_one::("google_oauth_token").map(|v| v.to_string()); + if oauth_token.is_some() { + download_source = DownloadSource::GooglePlay; + } + let list: Vec<(String, Option)> = if oauth_token.is_none() { + match matches.get_one::("app") { + Some(app) => { + let mut app_vec: Vec = app.splitn(2, '@').map(String::from).collect(); + let app_id = app_vec.remove(0); + let app_version = match app_vec.len() { + 1 => Some(app_vec.remove(0)), + _ => None, + }; + vec![(app_id, app_version)] + }, + None => { + let csv = matches.get_one::("csv").unwrap(); + let field = *matches.get_one::("field").unwrap(); + let version_field = matches.get_one::("version_field").map(|v| *v); + if field < 1 { + println!("{}\n\nApp ID field must be 1 or greater", usage); std::process::exit(1); } - if field == version_field { - println!("{}\n\nApp ID and Version fields must be different", usage); - std::process::exit(1); + if let Some(version_field) = version_field { + if version_field < 1 { + println!("{}\n\nVersion field must be 1 or greater", usage); + std::process::exit(1); + } + if field == version_field { + println!("{}\n\nApp ID and Version fields must be different", usage); + std::process::exit(1); + } } - } - match fetch_csv_list(csv, field, version_field) { - Ok(csv_list) => csv_list, - Err(err) => { - println!("{}\n\n{:?}", usage, err); - std::process::exit(1); + match fetch_csv_list(csv, field, version_field) { + Ok(csv_list) => csv_list, + Err(err) => { + println!("{}\n\n{:?}", usage, err); + std::process::exit(1); + } } } } - }; + } else { Vec::new() }; if let Some(true) = matches.get_one::("list_versions") { match download_source { @@ -272,85 +279,106 @@ async fn main() { } else { let parallel = matches.get_one::("parallel").map(|v| *v).unwrap(); let sleep_duration = matches.get_one::("sleep_duration").map(|v| *v).unwrap(); - let outpath = matches.get_one::("OUTPATH"); - if outpath.is_none() { - println!("{}\n\nOUTPATH must be specified when downloading files", usage); - std::process::exit(1); - } - let outpath = match fs::canonicalize(outpath.unwrap()) { - Ok(outpath) if Path::new(&outpath).is_dir() => { - outpath - }, - _ => { - println!("{}\n\nOUTPATH is not a valid directory", usage); + let outpath = matches.get_one::("OUTPATH").map_or_else(|| { + if oauth_token.is_none() { + println!("{}\n\nOUTPATH must be specified when downloading files", usage); std::process::exit(1); } - }; + None + }, |outpath| { + match fs::canonicalize(outpath) { + Ok(outpath) if Path::new(&outpath).is_dir() => { + Some(outpath) + }, + _ => { + println!("{}\n\nOUTPATH is not a valid directory", usage); + std::process::exit(1); + } + } + }); match download_source { DownloadSource::APKPure => { - apkpure::download_apps(list, parallel, sleep_duration, &outpath).await; + apkpure::download_apps(list, parallel, sleep_duration, &outpath.unwrap()).await; } DownloadSource::GooglePlay => { - let mut username = matches.get_one::("google_username").map(|v| v.to_string()); - let mut password = matches.get_one::("google_password").map(|v| v.to_string()); + let mut email = matches.get_one::("google_email").map(|v| v.to_string()); - let ini_file = matches.get_one::("ini").map(|ini_file| { - match fs::canonicalize(ini_file) { - Ok(ini_file) if Path::new(&ini_file).is_file() => { - ini_file - }, - _ => { - println!("{}\n\nSpecified ini is not a valid file", usage); - std::process::exit(1); - }, - } - }); + if email.is_some() && oauth_token.is_some() { + google_play::request_aas_token( + &email.unwrap(), + &oauth_token.unwrap(), + options, + ).await; + } else { + let mut aas_token = matches.get_one::("google_aas_token").map(|v| v.to_string()); + let accept_tos = match matches.get_one::("list_versions") { + Some(true) => true, + _ => false, + }; - if username.is_none() || password.is_none() { - if let Ok(conf) = load_config(ini_file) { - if username.is_none() { - username = conf.get("google", "username"); + let ini_file = matches.get_one::("ini").map(|ini_file| { + match fs::canonicalize(ini_file) { + Ok(ini_file) if Path::new(&ini_file).is_file() => { + ini_file + }, + _ => { + println!("{}\n\nSpecified ini is not a valid file", usage); + std::process::exit(1); + }, } - if password.is_none() { - password = conf.get("google", "password"); + }); + + if email.is_none() || aas_token.is_none() { + if let Ok(conf) = load_config(ini_file) { + if email.is_none() { + email = conf.get("google", "email"); + } + if aas_token.is_none() { + aas_token = conf.get("google", "aas_token"); + } } } - } - - if username.is_none() { - let mut prompt_username = String::new(); - print!("Username: "); - io::stdout().flush().unwrap(); - io::stdin().read_line(&mut prompt_username).unwrap(); - username = Some(prompt_username); - } - if password.is_none() { - password = Some(rpassword::prompt_password("Password: ").unwrap()); - } + if email.is_none() { + let mut prompt_email = String::new(); + print!("Email: "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut prompt_email).unwrap(); + email = Some(prompt_email.trim().to_string()); + } - google_play::download_apps( - list, - parallel, - sleep_duration, - &username.unwrap(), - &password.unwrap(), - &outpath, - options, - ) - .await; + if aas_token.is_none() { + let mut prompt_aas_token = String::new(); + print!("AAS Token: "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut prompt_aas_token).unwrap(); + aas_token = Some(prompt_aas_token.trim().to_string()); + } + + google_play::download_apps( + list, + parallel, + sleep_duration, + &email.unwrap(), + &aas_token.unwrap(), + &outpath.unwrap(), + accept_tos, + options, + ) + .await; + } } DownloadSource::FDroid => { fdroid::download_apps(list, parallel, sleep_duration, - &outpath, + &outpath.unwrap(), options, ).await; } DownloadSource::HuaweiAppGallery => { - huawei_app_gallery::download_apps(list, parallel, sleep_duration, &outpath).await; + huawei_app_gallery::download_apps(list, parallel, sleep_duration, &outpath.unwrap()).await; } } }