#![allow(dead_code)] use std::path::PathBuf; use anyhow::{anyhow, Result}; use chrono::naive::NaiveDate; use chrono::Datelike; use chrono::Local; use std::cmp::min; use clap::{Args, Parser, Subcommand}; use async_trait::async_trait; use google_walletobjects1::api::{ FlightClass, FlightObject, FlightclasMethods, FlightobjectMethods, }; use google_walletobjects1::oauth2::authenticator::{ Authenticator, DefaultHyperClient, HyperClientBuilder, }; use google_walletobjects1::oauth2::{read_service_account_key, ServiceAccountAuthenticator}; use jwt_simple::algorithms::{RS256KeyPair, RSAKeyPairLike}; use jwt_simple::claims::JWTClaims; use strum::IntoEnumIterator; use strum_macros::EnumIter; use serde::{Deserialize, Serialize}; use phf::phf_map; use tokio::try_join; #[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, }, } #[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, } struct AirlineDefinition<'a> { iata_code: &'a str, name: &'a str, boarding_policy: &'a str, seat_class_policy: &'a str, boarding_pass_background_colour: &'a str, frequent_flyer_program_name: Option<&'a str>, logo_url: &'a str, alliance_logo_url: Option<&'a str>, hero_image_logo_url: Option<&'a str>, boarding_privilege_logo_url: Option<&'a str>, } const TSA_PRECHECK_LOGO: &str = "https://p.lukegb.com/raw/MiserablyDirectPiglet.jpg"; static AIRLINE_DATA: phf::Map<&'static str, AirlineDefinition<'static>> = phf_map! { "VS" => AirlineDefinition{ iata_code: "VS", name: "Virgin Atlantic", boarding_policy: "GROUP_BASED", seat_class_policy: "CABIN_BASED", boarding_pass_background_colour: "#4f145b", frequent_flyer_program_name: Some("Flying Club"), logo_url: "https://p.lukegb.com/raw/VirtuallyCrispViper.png", alliance_logo_url: None, hero_image_logo_url: Some("https://p.lukegb.com/raw/FormerlyDistinctToad.png"), boarding_privilege_logo_url: Some("https://p.lukegb.com/raw/DefinitelyVerifiedTitmouse.png"), }, }; fn scan(image_path: &PathBuf) -> Result<String> { let image_path_str = (*image_path).to_str().ok_or(anyhow!("invalid path"))?; rxing::helpers::detect_in_file(image_path_str, Some(rxing::BarcodeFormat::AZTEC)) .map_err(|e| { anyhow!( "could not parse Aztec barcode from image {}: {}", image_path_str, e ) }) .map(|r| r.getText().clone()) } struct ServiceAccount { service_account_name: String, authenticator: Authenticator<<DefaultHyperClient as HyperClientBuilder>::Connector>, key_pair: RS256KeyPair, } #[derive(Deserialize)] struct ServiceAccountJSON { // We omit a lot of fields we don't care about. /// Service Account email client_email: String, /// RSA private key private_key: String, } async fn load_service_account(path: &PathBuf) -> Result<ServiceAccount> { let creds = read_service_account_key(path).await?; let sa = ServiceAccountAuthenticator::builder(creds).build().await?; let sa_data = tokio::fs::read(path).await?; let sa_parsed: ServiceAccountJSON = serde_json::from_slice(&sa_data[..])?; let key_pair = RS256KeyPair::from_pem(&sa_parsed.private_key)?; Ok(ServiceAccount { service_account_name: sa_parsed.client_email, authenticator: sa, key_pair: key_pair, }) } 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)?; return Ok(parsed); } #[async_trait] trait InsertOrUpdate<T: Clone> { async fn insert_now(&self, value: &T) -> Result<bool>; async fn update_now(&self, value: &T) -> Result<()>; } fn is_already_exists(err: &google_walletobjects1::Error) -> bool { match err { google_walletobjects1::Error::BadRequest(v) => match v.pointer("/error/code") { Some(num) => num == 409, None => false, }, _ => false, } } #[async_trait] impl<S> InsertOrUpdate<FlightClass> for FlightclasMethods<'_, S> where S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static, S::Response: hyper::client::connect::Connection + tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static, S::Future: Send + Unpin + 'static, S::Error: Into<Box<dyn std::error::Error + Send + Sync>>, { async fn insert_now(&self, value: &FlightClass) -> Result<bool> { self.insert(value.clone()) .doit() .await .and(Ok(true)) .or_else(|e| { if is_already_exists(&e) { Ok(false) } else { Err(anyhow!(e)) } }) } async fn update_now(&self, value: &FlightClass) -> Result<()> { self.update( value.clone(), &value.id.clone().ok_or(anyhow!("no id on FlightClass"))?, ) .doit() .await?; Ok(()) } } #[async_trait] impl<S> InsertOrUpdate<FlightObject> for FlightobjectMethods<'_, S> where S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static, S::Response: hyper::client::connect::Connection + tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static, S::Future: Send + Unpin + 'static, S::Error: Into<Box<dyn std::error::Error + Send + Sync>>, { async fn insert_now(&self, value: &FlightObject) -> Result<bool> { self.insert(value.clone()) .doit() .await .and(Ok(true)) .or_else(|e| { if is_already_exists(&e) { Ok(false) } else { Err(anyhow!(e)) } }) } async fn update_now(&self, value: &FlightObject) -> Result<()> { self.update( value.clone(), &value.id.clone().ok_or(anyhow!("no id on FlightObject"))?, ) .doit() .await?; Ok(()) } } #[derive(Serialize, Deserialize)] struct AddToWalletPayloadObject { id: String, #[serde(rename = "classId", skip_serializing_if = "Option::is_none")] class_id: Option<String>, } #[derive(Serialize, Deserialize)] struct AddToWalletPayload { #[serde(skip_serializing_if = "Vec::is_empty", rename = "flightClasses")] flight_classes: Vec<AddToWalletPayloadObject>, #[serde(skip_serializing_if = "Vec::is_empty", rename = "flightObjects")] flight_objects: Vec<AddToWalletPayloadObject>, } #[derive(Serialize, Deserialize)] struct AddToWalletClaims { origins: Vec<String>, #[serde(rename = "typ")] jwt_type: String, payload: AddToWalletPayload, } #[derive(Clone, Copy, Debug, EnumIter)] #[repr(u8)] enum PassengerStatus { NotCheckedIn = b'0', CheckedIn = b'1', BagsCheckedAndNotCheckedIn = b'2', BagsCheckedAndCheckedIn = b'3', PastSecurity = b'4', PastGate = b'5', Transit = b'6', Standby = b'7', BoardingDataRevalidated = b'8', OriginalBoardingLineUsedWhenTicketIssued = b'9', UpOrDownGradingPending = b'A', } impl PassengerStatus { fn from(c: u8) -> Result<PassengerStatus> { for v in PassengerStatus::iter() { if v as u8 == c { return Ok(v); } } Err(anyhow!( "{} ({}) is not a valid PassengerStatus value", c, c as char )) } } #[derive(Clone, Copy, Debug, EnumIter)] #[repr(u8)] enum DocumentationVerificationStatus { NotRequired = b'0', Required = b'1', Performed = b'2', } impl DocumentationVerificationStatus { fn from(c: u8) -> Result<DocumentationVerificationStatus> { for v in DocumentationVerificationStatus::iter() { if v as u8 == c { return Ok(v); } } Err(anyhow!( "{} ({}) is not a valid DocumentationVerificationStatus value", c, c as char )) } } #[derive(Clone, Copy, Debug, EnumIter)] #[repr(u8)] enum PassengerDescription { Adult = b'0', Male = b'1', Female = b'2', Child = b'3', Infant = b'4', NoPassenger = b'5', // cabin baggage AdultWithInfant = b'6', UnaccompaniedMinor = b'7', } impl PassengerDescription { fn from(c: u8) -> Result<PassengerDescription> { for v in PassengerDescription::iter() { if v as u8 == c { return Ok(v); } } Err(anyhow!( "{} ({}) is not a valid PassengerDescription value", c, c as char )) } } #[derive(Clone, Copy, Debug, EnumIter)] #[repr(u8)] enum CheckInSource { Web = b'W', AirportKiosk = b'K', RemoteKiosk = b'R', MobileDevice = b'M', AirportAgent = b'O', TownAgent = b'T', ThirdPartyVendor = b'V', } impl CheckInSource { fn from(c: u8) -> Result<CheckInSource> { for v in CheckInSource::iter() { if v as u8 == c { return Ok(v); } } Err(anyhow!( "{} ({}) is not a valid CheckInSource value", c, c as char )) } } #[derive(Debug)] struct BoardingPass { passenger_name: String, eticket_indicator: char, version_number: Option<char>, passenger_description: Option<PassengerDescription>, checkin_source: Option<CheckInSource>, boarding_pass_source: Option<CheckInSource>, date_of_issue: Option<NaiveDate>, document_type: Option<char>, boarding_pass_issuing_airline: Option<String>, baggage_tag_plate_number: Option<String>, legs: Vec<BoardingPassLeg>, security_data_type: u8, security_data: Vec<u8>, } fn parse_boarding_pass_date( from_boarding_pass_str: &str, nearby_date: &NaiveDate, ) -> Result<NaiveDate> { // Range is 0 to 9999 (although not really). let from_boarding_pass: u16 = from_boarding_pass_str.parse()?; match from_boarding_pass_str.len() { 4 => Ok(NaiveDate::from_yo_opt( (nearby_date.year() / 10 * 10) + ((from_boarding_pass / 1000) as i32), (from_boarding_pass % 1000).into(), ) .ok_or(anyhow!("could not parse date {}", from_boarding_pass_str))?), 3 => { // Find the one that's the fewest days away. let year = nearby_date.year(); let day: u32 = (from_boarding_pass % 1000).into(); let mut options = vec![ NaiveDate::from_yo_opt(year, day), NaiveDate::from_yo_opt(year - 1, day), NaiveDate::from_yo_opt(year + 1, day), ]; options.sort_by(|a, b| { days_between(nearby_date, a) .unwrap_or(9999) .cmp(&days_between(nearby_date, b).unwrap_or(9999)) }); Ok(options[0].unwrap()) } _ => Err(anyhow!( "boarding pass date {} is wrong length", from_boarding_pass_str )), } } fn days_between(nearby_date: &NaiveDate, maybe_date: &Option<NaiveDate>) -> Option<u64> { if maybe_date.is_none() { return None; } let is_date = maybe_date.as_ref().unwrap(); let days_between = (*nearby_date - *is_date).num_days(); Some(if days_between < 0 { (-days_between) as u64 } else { days_between as u64 }) } impl BoardingPass { fn parse(data: Vec<u8>, nearby_date: &NaiveDate) -> Result<BoardingPass> { if data[0] != b'M' { return Err(anyhow!( "format code was {} ({}), not M", data[0] as char, data[0] )); } let leg_count = match data[1] { b'1' => Ok(1), b'2' => Ok(2), b'3' => Ok(3), b'4' => Ok(4), _ => Err(anyhow!( "leg count was {} ({}), should be 1 to 4", data[1] as char, data[1] )), }?; let passenger_name: String = std::str::from_utf8(&data[2..22])?.trim_end().to_string(); let eticket_indicator: char = data[22] as char; // First leg mandatory data: let (first_leg, mut p) = BoardingPassLeg::parse_mandatory(&data[23..], nearby_date)?; p += 23; // We now have enough to start composing the boardingpass. let mut boarding_pass = BoardingPass { passenger_name, eticket_indicator, version_number: None, passenger_description: None, checkin_source: None, boarding_pass_source: None, date_of_issue: None, document_type: None, boarding_pass_issuing_airline: None, baggage_tag_plate_number: None, legs: vec![first_leg], security_data_type: 0xff, security_data: vec![], }; // Header + first leg optional data (why, IATA, why) let optional_data_size: usize = usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?; let mut optional_data = &data[p + 2..p + 2 + optional_data_size]; p += 2 + optional_data_size; if optional_data.len() >= 1 { if optional_data[0] != b'>' { return Err(anyhow!("expected Beginning of version number ('>') at beginning of conditional fields, got {} ({})", optional_data[0], optional_data[0] as char)); } optional_data = &optional_data[1..]; } if optional_data.len() >= 1 { boarding_pass.version_number = Some(optional_data[0] as char); optional_data = &optional_data[1..]; } if optional_data.len() >= 1 { let optional_data_unique_size: usize = usize::from_str_radix(std::str::from_utf8(&optional_data[0..2])?, 16)?; let mut optional_data_unique = &optional_data[2..optional_data_unique_size + 2]; if optional_data_unique.len() >= 1 { boarding_pass.passenger_description = if optional_data_unique[0] == b' ' { None } else { Some(PassengerDescription::from(optional_data_unique[0])?) }; optional_data_unique = &optional_data_unique[1..]; } if optional_data_unique.len() >= 1 { boarding_pass.checkin_source = if optional_data_unique[0] == b' ' { None } else { Some(CheckInSource::from(optional_data_unique[0])?) }; optional_data_unique = &optional_data_unique[1..]; } if optional_data_unique.len() >= 1 { boarding_pass.boarding_pass_source = if optional_data_unique[0] == b' ' { None } else { Some(CheckInSource::from(optional_data_unique[0])?) }; optional_data_unique = &optional_data_unique[1..]; } if optional_data_unique.len() >= 4 { let date_of_issue_str = std::str::from_utf8(&optional_data_unique[0..4])?; boarding_pass.date_of_issue = if date_of_issue_str == " " { None } else { Some(parse_boarding_pass_date(date_of_issue_str, nearby_date)?) }; optional_data_unique = &optional_data_unique[4..]; } // TODO: the above might fall through if we don't have 4 bytes of data if optional_data_unique.len() >= 1 { boarding_pass.document_type = if optional_data_unique[0] == b' ' { None } else { Some(optional_data_unique[0] as char) }; optional_data_unique = &optional_data_unique[1..]; } if optional_data_unique.len() >= 3 { let issuing_airline_str = std::str::from_utf8(&optional_data_unique[0..3])?.trim_end(); boarding_pass.boarding_pass_issuing_airline = if issuing_airline_str == "" { None } else { Some(issuing_airline_str.to_string()) }; optional_data_unique = &optional_data_unique[3..]; } // TODO: the above might fall through if we don't have 3 bytes of data let bag_tags_str_len = min(13, optional_data_unique.len()); let bag_tags_str = std::str::from_utf8(&optional_data_unique[0..bag_tags_str_len])?.trim_end(); boarding_pass.baggage_tag_plate_number = if bag_tags_str == "" { None } else { Some(bag_tags_str.to_string()) }; optional_data_unique = &optional_data_unique[bag_tags_str_len..]; if optional_data_unique.len() != 0 { return Err(anyhow!( "trailing unique optional data ({} bytes)", optional_data_unique.len() )); } optional_data = &optional_data[optional_data_unique_size + 2..]; } // Now for the repeated optional data for the first leg. boarding_pass.legs[0].add_optional(optional_data)?; // Now any additional legs(!) for _legnum in 1..leg_count { let (mut leg, leg_bytes) = BoardingPassLeg::parse_mandatory(&data[p..], nearby_date)?; p += leg_bytes; let optional_data_size: usize = usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?; let optional_data = &data[p + 2..p + 2 + optional_data_size]; p += 2 + optional_data_size; leg.add_optional(optional_data)?; boarding_pass.legs.push(leg); } // What's left is the security data. if data[p] != b'^' { return Err(anyhow!( "expected security data beginning indicator (^) but got {} ({})", data[p], data[p] as char )); } boarding_pass.security_data_type = data[p + 1]; let security_data_size: usize = usize::from_str_radix(std::str::from_utf8(&data[p + 2..p + 4])?, 16)?; boarding_pass.security_data = data[p + 4..p + 4 + security_data_size].to_vec(); if data.len() > p + 4 + security_data_size { return Err(anyhow!( "trailing bytes after security data (expected {} bytes of security data)", security_data_size )); } Ok(boarding_pass) } } #[derive(Debug)] struct BoardingPassLeg { operating_carrier_pnr: String, // 7, left justified w/ trailing blanks origin_airport: String, // 3 destination_airport: String, // 3 operating_carrier: String, // 3, left justified w/ trailing blanks flight_number: String, // 5, NNNN[a] (leading zeros + alpha/blank on last digit) date_of_flight: NaiveDate, // 3, leading zeros (this is the day in the year e.g. 001 = Jan 1st, // 365 == Dec 31st) compartment_code: char, seat_number: String, // 4, NNNa (leading zeros) check_in_sequence: String, // NNNN[f] (leading zeros + alpha/blank on last digit) passenger_status: PassengerStatus, airline_numeric_code: Option<String>, document_serial_number: Option<String>, selectee_indicator: Option<char>, documentation_verification: Option<DocumentationVerificationStatus>, marketing_carrier: Option<String>, frequent_flyer_airline: Option<String>, frequent_flyer_number: Option<String>, id_ad_indicator: Option<char>, free_baggage_allowance: Option<String>, airline_data: Option<Vec<u8>>, } impl BoardingPassLeg { fn parse_mandatory(data: &[u8], nearby_date: &NaiveDate) -> Result<(BoardingPassLeg, usize)> { let operating_carrier_pnr: String = std::str::from_utf8(&data[0..7])?.trim_end().to_string(); let origin_airport: String = std::str::from_utf8(&data[7..10])?.to_string(); let destination_airport: String = std::str::from_utf8(&data[10..13])?.to_string(); let operating_carrier: String = std::str::from_utf8(&data[13..16])?.trim_end().to_string(); let flight_number: String = std::str::from_utf8(&data[16..21])? .trim_end() .trim_start_matches('0') .to_string(); let date_of_flight_str: &str = std::str::from_utf8(&data[21..24])?; let date_of_flight: NaiveDate = parse_boarding_pass_date(date_of_flight_str, nearby_date)?; let compartment_code: char = data[24] as char; let seat_number: String = std::str::from_utf8(&data[25..29])? .trim_start_matches('0') .to_string(); let check_in_sequence: String = std::str::from_utf8(&data[29..34])? .trim_end() .trim_start_matches('0') .to_string(); let passenger_status: PassengerStatus = PassengerStatus::from(data[34])?; Ok(( BoardingPassLeg { operating_carrier_pnr, origin_airport, destination_airport, operating_carrier, flight_number, date_of_flight, compartment_code, seat_number, check_in_sequence, passenger_status, airline_numeric_code: None, document_serial_number: None, selectee_indicator: None, documentation_verification: None, marketing_carrier: None, frequent_flyer_airline: None, frequent_flyer_number: None, id_ad_indicator: None, free_baggage_allowance: None, airline_data: None, }, 35, )) } fn add_optional(&mut self, data: &[u8]) -> Result<()> { let structured_data_size: usize = usize::from_str_radix(std::str::from_utf8(&data[0..2])?, 16)?; let mut structured_data = &data[2..structured_data_size + 2]; if structured_data.len() >= 3 { let airline_code_str = std::str::from_utf8(&structured_data[0..3])?.trim_end(); self.airline_numeric_code = if airline_code_str == "" { None } else { Some(airline_code_str.to_string()) }; structured_data = &structured_data[3..]; } if structured_data.len() >= 10 { let document_serial_number_str = std::str::from_utf8(&structured_data[0..10])?.trim_end(); self.document_serial_number = if document_serial_number_str == "" { None } else { Some(document_serial_number_str.to_string()) }; structured_data = &structured_data[10..]; } if structured_data.len() >= 1 { self.selectee_indicator = if structured_data[0] == b' ' { None } else { Some(structured_data[0] as char) }; structured_data = &structured_data[1..]; } if structured_data.len() >= 1 { self.documentation_verification = if structured_data[0] == b' ' { None } else { Some(DocumentationVerificationStatus::from(structured_data[0])?) }; structured_data = &structured_data[1..]; } if structured_data.len() >= 3 { let marketing_carrier_str = std::str::from_utf8(&structured_data[0..3])?.trim_end(); self.marketing_carrier = if marketing_carrier_str == "" { None } else { Some(marketing_carrier_str.to_string()) }; structured_data = &structured_data[3..]; } if structured_data.len() >= 3 { let frequent_flyer_airline_str = std::str::from_utf8(&structured_data[0..3])?.trim_end(); self.frequent_flyer_airline = if frequent_flyer_airline_str == "" { None } else { Some(frequent_flyer_airline_str.to_string()) }; structured_data = &structured_data[3..]; } if structured_data.len() >= 16 { let frequent_flyer_number_str = std::str::from_utf8(&structured_data[0..16])?.trim_end(); self.frequent_flyer_number = if frequent_flyer_number_str == "" { None } else { Some(frequent_flyer_number_str.to_string()) }; structured_data = &structured_data[16..]; } if structured_data.len() >= 1 { self.id_ad_indicator = if structured_data[0] == b' ' { None } else { Some(structured_data[0] as char) }; structured_data = &structured_data[1..]; } if structured_data.len() >= 3 { let free_baggage_allowance_str = std::str::from_utf8(&structured_data[0..3])?.trim_end(); self.free_baggage_allowance = if free_baggage_allowance_str == "" { None } else { Some(free_baggage_allowance_str.to_string()) }; structured_data = &structured_data[3..]; } if structured_data.len() != 0 { return Err(anyhow!( "trailing data in structured data section ({} bytes)", structured_data.len() )); } let unstructured_data = &data[structured_data_size + 2..]; if unstructured_data.len() > 0 { self.airline_data = Some(unstructured_data.to_vec()); } Ok(()) } } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); let command = cli.command.unwrap(); match &command { Commands::Scan { image } => { println!("{}", scan(&image)?); return Ok(()); } _ => {} }; let service_account_file = cli .service_account_file .ok_or(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, ); 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... let created_class: bool = hub.flightclass().insert_now(&class).await?; if !created_class { // Already exists. hub.flightclass().update_now(&class).await?; } // Then the pass... let created_pass: bool = hub.flightobject().insert_now(&pass).await?; if !created_pass { // Already exists. hub.flightobject().update_now(&pass).await?; } // Now generate the URL. let claims: JWTClaims<AddToWalletClaims> = JWTClaims { issued_at: None, expires_at: None, invalid_before: None, audiences: Some(jwt_simple::claims::Audiences::AsString( "google".to_string(), )), issuer: Some(sa.service_account_name), jwt_id: None, subject: None, nonce: None, custom: AddToWalletClaims { jwt_type: "savetowallet".to_string(), origins: vec!["www.lukegb.com".to_string()], payload: AddToWalletPayload { flight_classes: vec![AddToWalletPayloadObject { id: class.id.clone().unwrap(), class_id: None, }], flight_objects: vec![AddToWalletPayloadObject { id: pass.id.unwrap(), class_id: Some(class.id.unwrap()), }], }, }, }; let token = sa.key_pair.sign(claims)?; println!("https://pay.google.com/gp/v/save/{}", token); 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())?; if boarding_pass.legs.len() > 1 { return Err(anyhow!("multi-leg trips aren't supported yet")); } let leg = &boarding_pass.legs[0]; let airline_data = AIRLINE_DATA.get(&leg.operating_carrier).ok_or(anyhow!( "no AIRLINE_DATA has been provided for {} yet", leg.operating_carrier ))?; let id_prefix = format!( "{}.{}{}-{}-{}{}", cli.issuer_id, leg.operating_carrier, leg.flight_number, leg.date_of_flight, leg.origin_airport, leg.destination_airport ); let class_id = &id_prefix; let pass_id = format!("{}-{}", &id_prefix, leg.check_in_sequence); let mut class = FlightClass::default(); class.id = Some(class_id.clone()); class.issuer_name = Some(airline_data.name.to_string()); class.hex_background_color = Some(airline_data.boarding_pass_background_colour.to_string()); class.review_status = Some("UNDER_REVIEW".to_string()); class.boarding_and_seating_policy = Some(google_walletobjects1::api::BoardingAndSeatingPolicy { boarding_policy: Some(airline_data.boarding_policy.to_string()), seat_class_policy: Some(airline_data.seat_class_policy.to_string()), kind: None, }); class.origin = Some(airport(&leg.origin_airport)); class.destination = Some(airport(&leg.destination_airport)); let mut flight_header = google_walletobjects1::api::FlightHeader::default(); let mut flight_header_carrier = google_walletobjects1::api::FlightCarrier::default(); flight_header_carrier.airline_alliance_logo = airline_data.alliance_logo_url.map(url_to_image); flight_header_carrier.airline_logo = Some(url_to_image(airline_data.logo_url)); flight_header_carrier.airline_name = Some(to_localized_string("en-GB", airline_data.name)); flight_header_carrier.carrier_iata_code = Some(leg.operating_carrier.to_string()); flight_header.carrier = Some(flight_header_carrier); flight_header.flight_number = Some(leg.flight_number.to_string()); class.flight_header = Some(flight_header); let mut pass = FlightObject::default(); pass.id = Some(pass_id.to_string()); pass.class_id = Some(class_id.clone()); Ok(()) } } } fn airport(iata_code: &str) -> google_walletobjects1::api::AirportInfo { let mut info = google_walletobjects1::api::AirportInfo::default(); info.airport_iata_code = Some(iata_code.to_string()); info } fn to_localized_string(locale: &str, s: &str) -> google_walletobjects1::api::LocalizedString { google_walletobjects1::api::LocalizedString { default_value: Some(google_walletobjects1::api::TranslatedString { kind: None, language: Some(locale.to_string()), value: Some(s.to_string()), }), kind: None, translated_values: None, } } fn url_to_image(url: &str) -> google_walletobjects1::api::Image { google_walletobjects1::api::Image { kind: None, content_description: None, source_uri: Some(google_walletobjects1::api::ImageUri { description: None, localized_description: None, uri: Some(url.to_string()), }), } }