use std::collections::HashSet; use std::path::PathBuf; use anyhow::{anyhow, Result}; use chrono::Local; use clap::{Args, Parser, Subcommand}; use futures_util::future::try_join_all; use google_walletobjects1::api::{FlightClass, FlightObject}; use serde::Deserialize; use tokio::try_join; use passgen::add_to_wallet::{AddToWallet, PassClassIdentifier, PassIdentifier}; use passgen::insert_or_update::*; use passgen::resolution792::BoardingPass; use passgen::scanner::scan; use passgen::service_account::load_service_account; use passgen::walletobjects::*; #[derive(Parser)] #[command(author, version, about, long_about = None, propagate_version = true, subcommand_required = true, arg_required_else_help = true)] struct Cli { #[arg(short, long)] service_account_file: Option, #[arg(short, long, default_value = "3388000000022186420")] issuer_id: String, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands { /// gets an existing pass GetPass(GetPass), /// gets an existing class GetClass(GetClass), /// uploads a class/pass pair, creating it if necessary. A Google Wallet Add link is printed. Upload(Upload), /// generates a class/pass pair from an image, uploads it, and then prints a Google Wallet Add /// link. UploadScan(UploadScan), /// reads the aztec code out of an image Scan { /// Path to the image to read. image: PathBuf, }, /// generates a add-to-wallet URL for a set of passes. GenerateURL(GenerateURL), } #[derive(Args)] struct GetClass { #[arg(short, long)] object_type: String, #[arg(short, long)] class_id: String, } #[derive(Args)] struct GetPass { #[arg(short, long)] object_type: String, #[arg(short, long)] pass_id: String, } #[derive(Args)] struct Upload { #[arg(short, long)] class_json: PathBuf, #[arg(short, long)] pass_json: PathBuf, } #[derive(Args)] struct UploadScan { image: PathBuf, } #[derive(Args)] struct GenerateURL { pass_id: Vec, } async fn load_thing<'a, T: Deserialize<'a>>(path: &PathBuf) -> Result { let data = tokio::fs::read(path).await?; let mut deser = serde_json::Deserializer::from_reader(data.as_slice()); let parsed: T = T::deserialize(&mut deser)?; Ok(parsed) } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); let command = cli.command.unwrap(); if let Commands::Scan { image } = &command { let scanned = scan(image)?; println!("{}", scanned); let boarding_pass = BoardingPass::parse(scanned.as_bytes().to_vec(), &Local::now().date_naive())?; println!("{:#?}", boarding_pass); return Ok(()); }; let service_account_file = cli .service_account_file .ok_or_else(|| anyhow!("--service-account-file must be set"))?; let sa = load_service_account(&service_account_file).await?; let hub = google_walletobjects1::Walletobjects::new( hyper::Client::builder().build( hyper_rustls::HttpsConnectorBuilder::new() .with_native_roots() .https_or_http() .enable_http1() .enable_http2() .build(), ), sa.authenticator.clone(), ); match &command { Commands::Scan { image: _ } => unreachable!(), Commands::GetClass(get) => { let class_id: String = if get.class_id.contains('.') { get.class_id.clone() } else { format!("{}.{}", cli.issuer_id, get.class_id) }; let (_, class) = hub.flightclass().get(&class_id).doit().await?; println!("{}", serde_json::to_string(&class)?); Ok(()) } Commands::GetPass(get) => { let pass_id: String = if get.pass_id.contains('.') { get.pass_id.clone() } else { format!("{}.{}", cli.issuer_id, get.pass_id) }; let (_, pass) = hub.flightobject().get(&pass_id).doit().await?; println!("{}", serde_json::to_string(&pass)?); Ok(()) } Commands::Upload(upload) => { let (pass, mut class): (FlightObject, FlightClass) = try_join!( load_thing(&upload.pass_json), load_thing(&upload.class_json) )?; // Always reset the class state to "UNDER_REVIEW". class.review_status = Some("UNDER_REVIEW".to_string()); // Try to create the class first... insert_or_update_now(&hub.flightclass(), &class).await?; // Then the pass... insert_or_update_now(&hub.flightobject(), &pass).await?; // Now generate the URL. println!( "{}", AddToWallet { service_account_email: sa.service_account_name.to_string(), flight_classes: vec![PassClassIdentifier { id: class.id.clone().unwrap(), }], flight_passes: vec![PassIdentifier { id: pass.id.unwrap(), class_id: class.id.unwrap(), }], } .to_url(&sa)? ); Ok(()) } Commands::UploadScan(upload_scan) => { let scanned = scan(&upload_scan.image)?; let boarding_pass = BoardingPass::parse(scanned.as_bytes().to_vec(), &Local::now().date_naive())?; let (classes, passes) = passes_from_barcode(&cli.issuer_id, boarding_pass)?; // Create all the classes. insert_or_update_many(&hub.flightclass(), &classes).await?; // Create all the objects. insert_or_update_many(&hub.flightobject(), &passes).await?; for pass in (&passes).iter() { println!("Generated pass: {}", pass.id.clone().unwrap()); } // Now generate the URL. println!( "{}", AddToWallet { service_account_email: sa.service_account_name.to_string(), flight_classes: classes .iter() .map(|c| PassClassIdentifier { id: c.id.clone().unwrap(), }) .collect(), flight_passes: passes .iter() .map(|p| PassIdentifier { id: p.id.clone().unwrap(), class_id: p.class_id.clone().unwrap(), }) .collect(), } .to_url(&sa)? ); Ok(()) } Commands::GenerateURL(generate_url) => { let pass_ids: Vec = generate_url .pass_id .iter() .map(|pid| { if pid.contains('.') { pid.clone() } else { format!("{}.{}", cli.issuer_id, pid) } }) .collect(); let passes_responses = try_join_all( pass_ids .iter() .map(|pid| hub.flightobject().get(&pid).doit()), ) .await?; let passes: Vec<&FlightObject> = passes_responses .iter() .map(|result| { let (_, pass) = result; pass }) .collect(); let class_ids: HashSet = HashSet::from_iter(passes.iter().map(|p| p.class_id.clone().unwrap())); println!( "{}", AddToWallet { service_account_email: sa.service_account_name.to_string(), flight_classes: class_ids .iter() .map(|c| PassClassIdentifier { id: c.clone() }) .collect(), flight_passes: passes .iter() .map(|p| PassIdentifier { id: p.id.clone().unwrap(), class_id: p.class_id.clone().unwrap(), }) .collect(), } .to_url(&sa)? ); Ok(()) } } }