287 lines
8.5 KiB
Rust
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(())
|
|
}
|
|
}
|
|
}
|