use clap::{Parser, Subcommand}; use colored::*; use serde::Deserialize; use traveltext::types::*; use traveltext::{iceportal::*, travelynx::*}; #[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 }, Arewethereyet, /// If iceportal.de is available, ask it which train we're in and /// check in Autocheckin, /// Undo the last checkin (if any). Undo, /// Query iceportal.de (for testing) ICEPortal, } #[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) => { let now = chrono::Utc::now(); let duration = to.real_arrival().map(|dt| *dt - now); match duration { Some (d) => println!( "{}: we'll be there in {} minutes", traveltext, d.num_minutes() ), None => println!("{}: I have no idea", traveltext) } } } }, 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 => { let iceportal: TripInfo = exiting_get_request("https://iceportal.de/api1/rs/tripInfo/trip", cli.debug); let last_stop = iceportal.guess_last_station().unwrap(); let train = iceportal.get_train_ref(); println!( "{}: guessing you got onto {} {} in {}, checking in …", traveltext, train._type, train.no, last_stop ); let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::CheckIn { train, from_station: last_stop, to_station: None, comment: None, token: format!("{}", config.token_travel), }, cli.debug, ); // eprintln!("{:?}", resp); println!("{}: {}", traveltext, resp); } Command::ICEPortal => { match get_request::("https://iceportal.de/api1/rs/tripInfo/trip") { Ok(resp) => { println!("{}: Currently in {}\n", traveltext, resp.get_train_ref().to_string().green()); println!("guessing last stop was: {:?}\n", resp.guess_last_station()); println!("Stops:\n{}", resp.trip()) } Err(err) => { if cli.debug { eprintln!("{:?}", err); } println!("either this tool or the iceportal broke or you're not actually on an ICE\n\ (get a response but couldn't parse it)"); } } } } 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) }