a rust tui to view amtrak train status
1// Amtrak API client - fetch and decrypt train data
2
3use anyhow::{Context, Result};
4use serde::Deserialize;
5
6use crate::crypto::{self, DecryptionKeys};
7use crate::model::{self, Station, Train};
8
9const TRAINS_URL: &str = "https://maps.amtrak.com/services/MapDataService/trains/getTrainsData";
10const STATIONS_URL: &str = "https://maps.amtrak.com/services/MapDataService/stations/trainStations";
11const ROUTES_URL: &str = "https://maps.amtrak.com/rttl/js/RoutesList.json";
12const ROUTES_V_URL: &str = "https://maps.amtrak.com/rttl/js/RoutesList.v.json";
13
14#[derive(Deserialize)]
15struct RouteEntry {
16 #[serde(default)]
17 #[serde(alias = "ZoomLevel")]
18 zoom_level: Option<u32>,
19}
20
21#[derive(Deserialize)]
22struct RoutesVConfig {
23 arr: Vec<String>,
24 s: Vec<String>,
25 v: Vec<String>,
26}
27
28pub async fn fetch_decryption_keys(client: &reqwest::Client) -> Result<DecryptionKeys> {
29 // Fetch route list to calculate masterZoom
30 let routes: Vec<RouteEntry> = client
31 .get(ROUTES_URL)
32 .send()
33 .await
34 .context("failed to fetch routes list")?
35 .json()
36 .await
37 .context("failed to parse routes list")?;
38
39 let master_zoom = routes
40 .iter()
41 .map(|r| r.zoom_level.unwrap_or(0))
42 .sum::<u32>() as usize;
43
44 // Fetch keys config
45 let config: RoutesVConfig = client
46 .get(ROUTES_V_URL)
47 .send()
48 .await
49 .context("failed to fetch routes config")?
50 .json()
51 .await
52 .context("failed to parse routes config")?;
53
54 let public_key = config
55 .arr
56 .get(master_zoom)
57 .context("masterZoom index out of bounds")?
58 .clone();
59
60 let salt = crypto::extract_salt(&config.s);
61 let iv = crypto::extract_iv(&config.v);
62
63 Ok(DecryptionKeys {
64 public_key,
65 salt,
66 iv,
67 })
68}
69
70pub async fn fetch_stations(
71 client: &reqwest::Client,
72 keys: &DecryptionKeys,
73) -> Result<Vec<Station>> {
74 let encrypted = client
75 .get(STATIONS_URL)
76 .send()
77 .await
78 .context("failed to fetch station data")?
79 .text()
80 .await
81 .context("failed to read station data response")?;
82
83 let json = crypto::decrypt_train_data(encrypted.trim(), keys)
84 .context("failed to decrypt station data")?;
85
86 model::parse_station_response(&json)
87}
88
89pub async fn fetch_trains(client: &reqwest::Client, keys: &DecryptionKeys) -> Result<Vec<Train>> {
90 let encrypted = client
91 .get(TRAINS_URL)
92 .send()
93 .await
94 .context("failed to fetch train data")?
95 .text()
96 .await
97 .context("failed to read train data response")?;
98
99 let json = crypto::decrypt_train_data(encrypted.trim(), keys)
100 .context("failed to decrypt train data")?;
101
102 model::parse_train_response(&json)
103}