depot/rust/passgen/src/main.rs

287 lines
8.5 KiB
Rust

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<PathBuf>,
#[arg(short, long, default_value = "3388000000022186420")]
issuer_id: String,
#[command(subcommand)]
command: Option<Commands>,
}
#[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<String>,
}
async fn load_thing<'a, T: Deserialize<'a>>(path: &PathBuf) -> Result<T> {
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<String> = 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<String> =
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(())
}
}
}