use chrono::{DateTime, Utc}; use clap::{Parser, Subcommand}; use colored::*; use serde::Deserialize; use serde_json::json; use traveltext::onboard::{choose_api, OnBoardAPI}; use traveltext::{traits::*, travelynx::*, types::*}; use hafas_rs::api::journeys::*; use hafas_rs::api::locations::*; use hafas_rs::client::HafasClient; use hafas_rs::profile::db::DbProfile; use hafas_rs::requester::hyper::HyperRustlsRequester; use hafas_rs::{Place, ProductsSelection}; #[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, } #[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 To { to: String }, /// (If already checked in) change the trip's starting point, /// Warning: may lead to doubled entries in your trip history! From { from: String }, /// Find a (single) trip of a single train via HAFAS. /// Mostly intended to ferry trip-data into travelynx 'by hand' if it doesn't know some connection Journey { /// starting station's name (fuzzy-matched) from_name: String, /// destination station's name (fuzzy-matched) to_name: String, /// departure time; takes the next available connection time: Option>, /// set the dryRun flag to 'true' when importing to travelynx #[clap(short = 'd', long = "dry-run")] dry_run: bool, /// import the found trip into travelynx (needs the token_import config option!) #[clap(short = 'i', long = "import")] import: bool }, 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, token_import: Option } #[tokio::main] async 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::From { from } => { println!( "{}: {}", traveltext, "warning: this command may pollute your trip history".red() ); let status: Status = exiting_get_request( &format!("{}/api/v1/status/{}", cli.baseurl, config.token_status), cli.debug ); match status.train { Some(train) => { let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::CheckIn { train: train.into(), to_station: status.to_station.map(|s| s.ds100).flatten(), token: config.token_travel, comment: None, from_station: from }, cli.debug ); println!("{}: {}", traveltext, resp); } None => { println!("{}: {}", traveltext, "not checked in?".red()) } } } Command::To { 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::Journey { from_name, to_name, time, dry_run, import } => { let c = HafasClient::new(DbProfile, HyperRustlsRequester::new()); let from = &c .locations(LocationsOptions { query: from_name, results: None, // TODO: respect locale set language: Some("de".to_string()) }) .await .unwrap()[0]; let to = &c .locations(LocationsOptions { query: to_name, results: None, language: None }) .await .unwrap()[0]; let opts = JourneysOptions { products: ProductsSelection { bus: Some(false), ..ProductsSelection::default() }, departure: time.map(|t| t.timestamp()), //Some(1650536340); stopovers: Some(true), language: Some("de".to_string()), ..JourneysOptions::default() }; let journey = &c .journeys(from.clone(), to.clone(), opts) .await .unwrap() .journeys[0] .legs[0]; let stops = journey.stopovers.as_ref().unwrap(); println!("{}: found this trip:", traveltext); for stop in stops { match &stop.stop { Place::Stop(station) => { println!("{}", station.name.as_ref().unwrap()) } _ => panic!("this train stops somewhere that's not a station??") } stop.arrival.map(|t| println!("arr: {:?}", t.to_rfc3339())); stop.departure.map(|t| println!("dep: {:?}", t.to_rfc3339())); } if import { let travelynx = json!({ "token": config.token_import.unwrap(), "dryRun": dry_run, "fromStation": match &stops[0].stop { Place::Stop (station) => json!({ "name":station.name, "scheduledTime":stops[0].departure.unwrap().timestamp() }), _ => panic!("this trip lacks a first station?") }, "toStation": match &stops[stops.len()-1].stop { Place::Stop (station) => json!({ "name": station.name, "scheduledTime": stops[stops.len()-1].arrival.unwrap().timestamp() }), _ => panic!("this trip lacks an end station?") }, "train": match &journey.line { Some(line) => json!({ "type": line.name.as_ref().unwrap().split_ascii_whitespace().next().unwrap(), "line": serde_json::Value::Null, "no": line.fahrt_nr.as_ref().unwrap() }), None => panic!("could not find train information") }, "intermediateStops": &stops[1..stops.len()-1] .iter() .filter_map(|s| match &s.stop { Place::Stop (station) => station.name.as_ref(), _ => None }) .collect::>() }); if cli.debug { println!("{}", serde_json::to_string(&travelynx).unwrap()); } let resp: serde_json::Value = exiting_post_request( &format!("{}/api/v1/import", cli.baseurl), travelynx, cli.debug ); println!("{:?}", resp); } } } 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) }