summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorstuebinm2022-01-25 23:07:23 +0100
committerstuebinm2022-01-25 23:07:23 +0100
commit130d8907335c93e48017c13e3202816d552683c9 (patch)
treed94809523a3e47d3eadf94d19cd26b22cdb3a0ca /src
hacky proof of concept
Diffstat (limited to 'src')
-rw-r--r--src/iceportal.rs67
-rw-r--r--src/lib.rs5
-rw-r--r--src/main.rs145
-rw-r--r--src/travelynx.rs64
-rw-r--r--src/types.rs208
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 <= &current_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())
+ )
+ }
+ }
+}