diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/iceportal.rs | 67 | ||||
-rw-r--r-- | src/lib.rs | 5 | ||||
-rw-r--r-- | src/main.rs | 145 | ||||
-rw-r--r-- | src/travelynx.rs | 64 | ||||
-rw-r--r-- | src/types.rs | 208 |
5 files changed, 489 insertions, 0 deletions
diff --git a/src/iceportal.rs b/src/iceportal.rs new file mode 100644 index 0000000..1b36556 --- /dev/null +++ b/src/iceportal.rs @@ -0,0 +1,67 @@ +use serde::Deserialize; +use serde_json::Value; + +use crate::travelynx::TrainRef; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TripInfo { + trip: Trip, + connection: Option<Value>, + selected_route: Option<Value>, + active: Option<Value> +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Trip { + train_type: String, + vzn: String, // train number + // some position info here + actual_position: u64, // distance along track, presumably + stops: Vec<Stop> +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Stop { + info: StopInfo, + station: Station +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct StopInfo { + distance_from_start: u64, + position_status: String // one of "departed", "future", ... ? +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Station { + eva_nr: String, + name: String +} + + +impl TripInfo { + + pub fn guess_last_station (&self) -> Option<String> { + let current_pos = self.trip.actual_position; + self.trip + .stops + .iter() + .rev() + .map(|stop| (stop.info.distance_from_start, stop)) + .filter(|(dist,_)| dist <= ¤t_pos) + .next() + .map(|(_,stop)| stop.station.name.clone()) + } + + pub fn get_train_ref (&self) -> TrainRef { + TrainRef { + _type: self.trip.train_type.clone(), + no: self.trip.vzn.clone() + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a528cec --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ + + +pub mod types; +pub mod travelynx; +pub mod iceportal; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..12c67a8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,145 @@ +use clap::{Parser, Subcommand}; + +use colored::*; + +use traveltext::types::*; +use traveltext::{travelynx::*, iceportal::*}; + + +#[allow(non_upper_case_globals)] +const token: &str = "1387-d942ee22-1d34-4dc2-89b6-5e7ef229fb5e"; +#[allow(non_upper_case_globals)] +const baseurl: &str = "https://travelynx.de"; + + + +#[derive(Parser)] +struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[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 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 +} + + + +fn main() -> Result<(), ureq::Error> { + let cli = Cli::parse(); + + let traveltext = format!( + "{}{}el{}{}", + "tr".cyan(), + "av".bright_magenta(), + "te".bright_magenta(), + "xt".cyan() + ); + + match cli.command { + Command::Status => { + let body: Status = ureq::get(&format!("{}/api/v1/status/{}", baseurl, token)) + .call() + // TODO: this prints the token! + .unwrap_or_else(|err| exit_err(&err.to_string())) + .into_json() + .unwrap_or_else(|err| exit_err(&err.to_string())); + + println!("{}: {}", traveltext, body); + } + Command::Checkin {from, to, train} => { + let request = Action::CheckIn { + train, + from_station: from, + to_station: Some(to), + comment: None, + token: format!("{}", token) + }; + + // println!("{}", serde_json::to_string(&request).unwrap()); + + let resp: Response = ureq::post(&format!("{}/api/v1/travel", baseurl)) + .send_json(request) + .unwrap_or_else(|err| exit_err(&err.to_string())) + .into_json() + .unwrap_or_else(|err| exit_err(&err.to_string())); + + // eprintln!("{:?}", resp); + println!("{}: {}", traveltext, resp); + }, + Command::Undo => { + let resp: Response = ureq::post(&format!("{}/api/v1/travel", baseurl)) + .send_json(Action::Undo {token: token.to_owned()}) + .unwrap_or_else(|err| exit_err(&err.to_string())) + .into_json() + .unwrap_or_else(|err| exit_err(&err.to_string())); + + println!("{}: {}", traveltext, resp); + }, + Command::Autocheckin => { + let iceportal: TripInfo = ureq::get("https://iceportal.de/api1/rs/tripInfo/trip") + .call()? + .into_json() + .unwrap_or_else(|err| exit_err(&err.to_string())); + 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 request = Action::CheckIn { + train, + from_station: last_stop, + to_station: None, + comment: None, + token: format!("{}", token) + }; + + // println!("{}", serde_json::to_string(&request).unwrap()); + + let resp: Response = ureq::post(&format!("{}/api/v1/travel", baseurl)) + .send_json(request) + .unwrap_or_else(|err| exit_err(&err.to_string())) + .into_json() + .unwrap_or_else(|err| exit_err(&err.to_string())); + + // eprintln!("{:?}", resp); + println!("{}: {}", traveltext, resp); + + }, + Command::ICEPortal => { + let resp: TripInfo = ureq::get("https://iceportal.de/api1/rs/tripInfo/trip") + .call()? + .into_json() + .unwrap_or_else(|err| exit_err(&err.to_string())); + println!("{:?}", resp); + println!("guessing last stop was: {:?}", resp.guess_last_station()); + } + } + Ok(()) +} + +fn exit_err(msg: &str) -> ! { + eprintln!("{}", msg); + std::process::exit(1) +} diff --git a/src/travelynx.rs b/src/travelynx.rs new file mode 100644 index 0000000..0188f32 --- /dev/null +++ b/src/travelynx.rs @@ -0,0 +1,64 @@ +use clap::Args; +use serde::{Serialize, Deserialize}; +use colored::*; + +use crate::types::Status; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Travel { + token: String, + #[serde(flatten)] + action: Action, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "action")] +pub enum Action { + #[serde(rename = "checkin")] + #[serde(rename_all = "camelCase")] + CheckIn { + token: String, + train: TrainRef, + from_station: String, + #[serde(skip_serializing_if = "Option::is_none")] + to_station: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + comment: Option<String>, + }, + #[serde(rename = "checkout")] + CheckOut { + to_station: String, + force: bool, + #[serde(skip_serializing_if = "Option::is_none")] + comment: Option<String>, + }, + Undo {token: String}, +} + +#[derive(Args, Serialize, Debug)] +pub struct TrainRef { + #[clap(name = "TRAIN TYPE")] + #[serde(rename = "type")] + pub _type: String, + #[clap(name = "NUMBER")] + pub no: String, +} + +#[derive(Deserialize, Debug)] +pub struct Response { + success: Option<bool>, + deprecated: bool, + status: Status, + error: Option<String> +} + +impl std::fmt::Display for Response { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.error { + Some(msg) => write!(f, "{}", msg.red()), + None => write!(f, "{}\n\n{}", "Success!".green(), self.status) + } + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..3dcb6db --- /dev/null +++ b/src/types.rs @@ -0,0 +1,208 @@ +use serde::{Deserialize, Deserializer}; + +use chrono::NaiveDateTime; +use colored::*; +use itertools::Itertools; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Station { + name: String, + ds100: String, + uic: u64, + latitude: f64, + longitude: f64, + #[serde(deserialize_with = "naive_read_unixtime")] + scheduled_time: NaiveDateTime, + #[serde(deserialize_with = "naive_read_unixtime")] + real_time: NaiveDateTime, +} + + +pub fn parse_optional_station<'de, D>(d: D) -> Result<Option<Station>, D::Error> +where + D: Deserializer<'de>, +{ + let val = <serde_json::Value>::deserialize(d)?; + match serde_json::from_value(val) { + Ok(station) => Ok(Some(station)), + Err(_) => Ok(None) + } +} + + +fn naive_read_unixtime<'de, D>(d: D) -> Result<NaiveDateTime, D::Error> +where + D: Deserializer<'de>, +{ + let ts = <i64>::deserialize(d)?; + Ok(NaiveDateTime::from_timestamp(ts, 0)) +} +fn option_naive_read_unixtime<'de, D>(d: D) -> Result<Option<NaiveDateTime>, D::Error> +where + D: Deserializer<'de>, +{ + match <i64>::deserialize(d) { + Ok(ts) => + Ok(Some(NaiveDateTime::from_timestamp(ts, 0))), + Err(_) => Ok(None) + } +} + + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Stop { + name: String, + #[serde(deserialize_with = "option_naive_read_unixtime")] + scheduled_arrival: Option<NaiveDateTime>, + #[serde(deserialize_with = "option_naive_read_unixtime")] + real_arrival: Option<NaiveDateTime>, + #[serde(deserialize_with = "option_naive_read_unixtime")] + scheduled_departure: Option<NaiveDateTime>, + #[serde(deserialize_with = "option_naive_read_unixtime")] + real_departure: Option<NaiveDateTime>, +} + +trait IsStation { + fn name (&self) -> &str; + fn scheduled_arrival (&self) -> Option<&NaiveDateTime>; + fn real_arrival (&self) -> Option<&NaiveDateTime>; + fn ds100 (&self) -> &str; + + fn to_fancy_string (&self) -> String { + format!( + "{} {} – {} ({})", + self.real_arrival().map(|t| t.time().to_string()).unwrap_or("??:??:??".to_string()).blue(), + { + let delay = match (self.real_arrival(), self.scheduled_arrival()) { + (Some(a), Some(s)) => (a.time() - s.time()).num_minutes(), + _ => 0 + }; + let text = format!("({:+})", delay); + if delay > 0 { + text.red() + } else { + text.green() + } + }, + self.ds100().red(), + self.name() + ) + } +} + +impl IsStation for Station { + fn name (&self) -> &str { + &self.name + } + fn scheduled_arrival (&self) -> Option<&NaiveDateTime> { + Some(&self.scheduled_time) + } + fn real_arrival (&self) -> Option<&NaiveDateTime> { + Some(&self.real_time) + } + + fn ds100 (&self) -> &str { + &self.ds100 + } +} + +impl IsStation for Stop { + fn name (&self) -> &str { + &self.name + } + fn scheduled_arrival (&self) -> Option<&NaiveDateTime> { + self.scheduled_arrival.as_ref() + } + fn real_arrival (&self) -> Option<&NaiveDateTime> { + self.real_arrival.as_ref() + } + + fn ds100 (&self) -> &str { + "[??]" + } +} + + + + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Train { + #[serde(rename = "type")] + _type: String, + line: Option<String>, + no: String, + id: String, +} + + + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Status { + deprecated: bool, + checked_in: bool, + from_station: Station, + #[serde(deserialize_with = "parse_optional_station")] + to_station: Option<Station>, + intermediate_stops: Vec<Stop>, + train: Option<Train>, + action_time: u64, +} + +#[allow(dead_code)] +pub struct Ds100 { + inner: String, +} + +struct Trip<'a> (&'a Vec<Stop>); + +impl std::fmt::Display for Train { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self._type, self.no) + } +} + +#[allow(unstable_name_collisions)] +impl std::fmt::Display for Trip<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0.len() == 0 { + write!(f, "(none)") + } else { + self.0.iter() + .map(|stop| stop.to_fancy_string()) + .intersperse(" ↓".to_string()) + .for_each(|l| writeln!(f, " {}", l).unwrap()); + Ok(()) + } + } +} + + +impl std::fmt::Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.checked_in { + false => write!( + f, + "not checked in. \n\n\ + last trip: \n {}\n ↓\n {}", + self.from_station.to_fancy_string(), + self.to_station.as_ref().unwrap().to_fancy_string() + ), + true => write!( + f, + "checked in to: {}.\n\n\ + stops:\n {}\n ↓\n{} ↓\n {}", + self.train.as_ref().map(|t| t.to_string()).unwrap_or("".to_string()).green(), + self.from_station.to_fancy_string(), + Trip(&self.intermediate_stops), + self.to_station + .as_ref() + .map(|s| s.to_fancy_string()) + .unwrap_or_else(|| "🚄 Fahrt ins Blaue".blue().to_string()) + ) + } + } +} |