summaryrefslogtreecommitdiff
path: root/mindthegap/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'mindthegap/src/main.rs')
-rw-r--r--mindthegap/src/main.rs246
1 files changed, 246 insertions, 0 deletions
diff --git a/mindthegap/src/main.rs b/mindthegap/src/main.rs
new file mode 100644
index 0000000..11f707a
--- /dev/null
+++ b/mindthegap/src/main.rs
@@ -0,0 +1,246 @@
+use owo_colors::OwoColorize;
+use std::{collections::HashMap, fmt::Display};
+
+use reqwest::header::HeaderValue;
+use serde::Deserialize;
+use serde_json::json;
+use clap::Parser;
+
+type SomeError = Box<dyn std::error::Error>;
+
+#[derive(clap::Parser, Debug)]
+#[clap(about, author)]
+struct Cli {
+ /// from station (ds100 or exact name)
+ from: String,
+ /// to station (ds100 or exact name)
+ to: String,
+ /// talk about what I do more
+ #[clap(long, short)]
+ verbose: bool,
+ /// dump the raw json data as received from server
+ #[clap(long)]
+ dump_raw: bool,
+ #[clap(long, default_value="https://bauinfos.deutschebahn.com")]
+ apiurl: String,
+}
+
+struct MagicIncantation {
+ xsrf_token: String,
+ cookie: String,
+}
+
+#[derive(Deserialize, Debug)]
+struct LineReport {
+ title: String,
+ name: String,
+ route: String,
+ updated_at: String,
+ reports: Vec<Report>,
+ infos: Vec<InfoPdfLink>
+}
+
+#[derive(Deserialize, Debug, Clone)]
+struct Report {
+ period: Vec<String>,
+ content: Vec<String>,
+ headline: Vec<String>,
+ hint: Vec<String>
+}
+
+#[derive(Deserialize, Debug)]
+struct InfoPdfLink {
+ title: String,
+ url: String
+}
+
+
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let args = Cli::parse();
+
+ let magic = MagicIncantation::conjure(&args).await?;
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert("X-Requested-With", HeaderValue::from_static("XMLHttpRequest"));
+ headers.insert("X-XSRF-TOKEN", HeaderValue::from_str(&magic.xsrf_token)?);
+ headers.insert("Cookie", HeaderValue::from_str(&magic.cookie)?);
+
+ let client = reqwest::Client::builder()
+ .default_headers(headers)
+ .build()?;
+
+ let url = args.apiurl.to_owned() + "/getFilterLines";
+ if args.verbose {
+ eprintln!("> GET {}", url);
+ }
+ let from = ds100::ds100(&args.from).unwrap_or(&args.from);
+ let to = ds100::ds100(&args.to).unwrap_or(&args.to);
+ let req = client
+ .post(url)
+ .json(&json!({
+ "filter": {
+ "bidirektional":true,
+ "dateFrom":"",
+ "dateTo":"",
+ "rangeTimes":[], // e.g. [0,1440] — minutes in the day?
+ "stations":[from,to], // must be exact
+ "weekdays":[]
+ },
+ "nomap":true,
+ "noshow_fv":false,
+ "state":"brd"
+ }));
+
+ let resp = req.send().await?;
+
+ if args.verbose {
+ eprintln!("> Response from Server: {:?}", resp);
+ }
+ let json: serde_json::Value = resp.json().await?;
+ if args.dump_raw {
+ eprintln!("> Got JSON: {}", json)
+ }
+
+
+ let lines: Vec<LineReport> = json["linetypes"]
+ .as_array().unwrap()
+ .iter()
+ .map(|json| vec![&json["linetypes"]["1"],
+ &json["linetypes"]["2"],
+ &json["linetypes"]["3"]])
+ .flatten()
+ .filter_map(
+ |json| serde_json::from_value(json["lines"]["active"].clone()).ok())
+ .map(|m: HashMap<String, LineReport>| m.into_values())
+ .flatten()
+ .collect();
+
+ for line in lines {
+ print!("{}\n", line);
+ }
+
+ Ok(())
+}
+
+
+impl MagicIncantation {
+ async fn conjure(args: &Cli) -> Result<MagicIncantation, SomeError> {
+ // headers get sent to any request; the error page is faster than a normal one, though
+ let url = args.apiurl.to_owned() + "/lol";
+ if args.verbose {
+ eprintln!("> GET {}", url);
+ }
+ let resp = reqwest::get(url).await?;
+
+ let cookies = resp
+ .headers()
+ .get_all("set-cookie")
+ .iter()
+ .filter_map(|v| Some(v.to_str().ok()?.to_owned()))
+ .filter_map(|s| Some(s.split(";").next()?.to_owned()))
+ .collect::<Vec<_>>();
+
+ let xsrf_token = cookies
+ .iter()
+ .filter_map(|s| s.strip_prefix("XSRF-TOKEN="))
+ .find_map(|s| s.strip_suffix("%3D"))
+ .unwrap()
+ .to_owned()
+ + "=";
+
+ let cookie = cookies.join(";") + "=";
+
+ if args.verbose {
+ eprintln!("> Aquired cookies: \n> {}\n> {}", xsrf_token, cookie);
+ }
+ Ok(MagicIncantation { xsrf_token, cookie })
+ }
+}
+
+
+impl Display for LineReport {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(
+ f, "{} {} ({})\n Updated: {}",
+ "Mind the Gap!".red(),
+ self.title.yellow(),
+ self.route,
+ self.updated_at.green(),
+ )?;
+ for link in &self.infos {
+ writeln!(f, "{}", link)?;
+ }
+ write!(f, "\n")?;
+
+ let mut buf: Vec<Report> = Vec::new();
+ let shrunken = self
+ .reports
+ .iter()
+ .fold(&mut buf, |acc, report| {
+ if let Some(report2) = acc.iter_mut().find(|r| r.headline == report.headline) {
+ let period = report
+ .period
+ .iter()
+ .zip(report2.period.iter())
+ .map(|(p1,p2)| p1.to_owned() + "\n & " + p2)
+ .collect();
+ *report2 = Report {period, ..report.clone()}
+ } else {
+ acc.push(report.clone())
+ }
+ acc
+ });
+ for report in shrunken {
+ writeln!(f, "{}", report)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl Display for Report {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if self.period.len() != self.headline.len() {
+
+ for period in &self.period {
+ writeln!(f, " {}", period.blue())?;
+ }
+ for h in &self.headline {
+ writeln!(f, " {}", html_escape::decode_html_entities(h))?;
+ }
+ } else {
+ let mut buf: Vec<(String, &String)> = Vec::new();
+ let shrunken = self
+ .period
+ .iter()
+ .zip(self.headline.iter())
+ .fold(&mut buf, |acc, (period,msg)| {
+ let x = if let Some((period2,_)) = acc.iter().find(|(_,msg2)| &msg == msg2) {
+ (period.to_owned() + "\n & " + period2, msg)
+ } else {
+ (period.to_owned(), msg)
+ };
+ acc.push(x);
+ acc
+ });
+ for (period, h) in shrunken {
+ let (terminal_size::Width(twidth),_) = terminal_size::terminal_size().unwrap();
+ let opt = textwrap::Options::new(twidth as usize - 4);
+ writeln!(
+ f,
+ "{}:\n{}",
+ textwrap::indent(&textwrap::fill(period,&opt), " ").blue(),
+ textwrap::indent(&textwrap::fill(&html_escape::decode_html_entities(h), &opt), " ")
+ )?;
+ }
+ }
+ Ok(())
+ }
+}
+
+impl Display for InfoPdfLink {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, " pdf: {}\n {}", self.title.blue(), self.url.cyan())
+ }
+}