use clap::{Parser, Subcommand}; use colored::*; use serde::Deserialize; use traveltext::onboard::{choose_api, OnBoardAPI}; use traveltext::{traits::*, travelynx::*, types::*}; #[derive(Parser)] struct Cli { #[clap(subcommand)] command: Command, /// print requests that couldn't be parsed to help debugging #[clap(long)] debug: bool, #[clap(default_value = "https://travelynx.de")] baseurl: String, /// API token to use in requests token: Option } #[derive(Subcommand)] enum Command { /// Get current travelynx status Status, /// Check in to a train using travelynx Checkin { from: String, to: String, // TODO: make this optional and guess which train if not given #[clap(flatten)] train: TrainRef }, /// (If already checked in) change the trip's destination Destination { to: String }, Station { station: String }, Arewethereyet, /// If iceportal.de is available, ask it which train we're in and /// check in Autocheckin { train: String }, /// Undo the last checkin (if any). Undo, /// Query a train's on-board API Query { train: String } } #[derive(Deserialize)] struct Config { token_status: String, token_travel: String } fn main() -> Result<(), ureq::Error> { let cli = Cli::parse(); let traveltext = format!( "{}{}el{}{}", "tr".cyan(), "av".bright_magenta(), "te".bright_magenta(), "xt".cyan() ); let configpath = { let mut path = dirs::config_dir().unwrap(); path.push("traveltext.toml"); path }; let config: Config = match std::fs::read_to_string(&configpath) { Ok(text) => match toml::from_str(&text) { Ok(config) => config, Err(err) => exit_err( &err.to_string(), &format!( "failed parsing config file {}", configpath.to_string_lossy() ) ) }, Err(err) => exit_err( &err.to_string(), &format!("failed reading config at: {}", configpath.to_string_lossy()) ) }; match cli.command { Command::Status => { let status: Status = exiting_get_request( &format!("{}/api/v1/status/{}", cli.baseurl, config.token_status), cli.debug ); println!("{}: {}", traveltext, status); } Command::Arewethereyet => { let status: Status = exiting_get_request( &format!("{}/api/v1/status/{}", cli.baseurl, config.token_status), cli.debug ); match status.to_station { None => println!("{}: Fahrt ins {}", traveltext, "Blaue".blue()), Some(to) if status.checked_in => { let now = chrono::Utc::now(); let duration = to.real_arrival().map(|dt| *dt - now); match duration { Some(d) if d < chrono::Duration::zero() => println!( "{}: {}", traveltext, "we should be there already!".green() ), Some(d) => println!( "{}: we'll be there in {} minutes", traveltext, d.num_minutes() ), None => println!("{}: I have no idea", traveltext) } } Some(_to) => { println!("{}: {}", traveltext, "you're not checked in".red()) } } } Command::Checkin { from, to, train } => { let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::CheckIn { train, from_station: from, to_station: Some(to), comment: None, token: format!("{}", config.token_travel) }, cli.debug ); println!("{}: {}", traveltext, resp); } Command::Destination { to } => { let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::CheckOut { to_station: to, force: false, token: config.token_travel, comment: None }, cli.debug ); println!("{}: {}", traveltext, resp); } Command::Undo => { let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::Undo { token: config.token_travel.to_owned() }, cli.debug ); println!("{}: {}", traveltext, resp); } Command::Autocheckin { train } => { let onboard = match choose_api(&train).request(cli.debug) { Ok(resp) => resp, Err(e) => exit_err(&e.to_string(), "failed to parse api response") }; let last_stop = onboard.guess_last_station().unwrap(); let train = onboard.get_train_ref(); println!( "{}: guessing you got onto {} {} in {}, checking in …", traveltext, train._type, train.no, last_stop.to_fancy_string() ); let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::CheckIn { train, from_station: last_stop.name().to_owned(), to_station: None, comment: None, token: format!("{}", config.token_travel) }, cli.debug ); // eprintln!("{:?}", resp); println!("{}: {}", traveltext, resp); } Command::Query { train } => { let api: &dyn OnBoardAPI = choose_api(&train); match api.request(cli.debug) { Ok(resp) => { println!( "{}: Currently in {}\n", traveltext, resp.get_train_ref().to_string().green() ); println!( "guessing last stop was: {:?}\n", resp.guess_last_station().unwrap().name() ); // println!("Stops:\n{}", resp.stops()) } Err(err) => { println!("either this tool or the zugportal broke or you're not actually on an ICE\n\ (get a response but couldn't parse it)"); } } } Command::Station { station } => { // let c = HafasClient::new(DbProfile, HyperRustlsRequester::new()); // println!("{:#?}", c.suggestions("München Hbf", None).await.unwrap()); } } Ok(()) } fn get_request(uri: &str) -> Result where R: serde::de::DeserializeOwned { let resp: String = ureq::get(uri) .call() .unwrap_or_else(|err| exit_err(&err.to_string(), "get request failed")) .into_string() .unwrap_or_else(|err| { exit_err(&err.to_string(), "get request response failed") }); match serde_json::from_str::(&resp) { Ok(obj) => Ok(obj), Err(err) => Err((err, resp)) } } fn exiting_get_request( uri: &str, debug: bool ) -> R { match get_request(uri) { Ok(obj) => obj, Err((err, resp)) => { if debug { eprintln!("DEBUG: {}", resp); } exit_err(&err.to_string(), "parsing response failed") } } } fn exiting_post_request(uri: &str, payload: P, debug: bool) -> R where P: serde::Serialize, R: serde::de::DeserializeOwned { let resp: String = ureq::post(uri) .send_json(payload) .unwrap_or_else(|err| exit_err(&err.to_string(), "post request failed")) .into_string() .unwrap_or_else(|err| { exit_err(&err.to_string(), "post request response failed") }); match serde_json::from_str::(&resp) { Ok(obj) => obj, Err(err) => { if debug { eprintln!("DEBUG: {}", resp); } exit_err(&err.to_string(), "parsing response failed") } } } fn exit_err(msg: &str, scope: &str) -> ! { eprintln!("{}: {}", scope, msg); std::process::exit(1) }