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; #[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, infos: Vec } #[derive(Deserialize, Debug, Clone)] struct Report { period: Vec, content: Vec, headline: Vec, hint: Vec } #[derive(Deserialize, Debug)] struct InfoPdfLink { title: String, url: String } #[tokio::main] async fn main() -> Result<(), Box> { 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 = 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| m.into_values()) .flatten() .collect(); for line in lines { print!("{}\n", line); } Ok(()) } impl MagicIncantation { async fn conjure(args: &Cli) -> Result { // 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::>(); 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 = 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()) } }