diff options
Diffstat (limited to 'mindthegap/src/main.rs')
-rw-r--r-- | mindthegap/src/main.rs | 246 |
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()) + } +} |