// Amtrak API client - fetch and decrypt train data use anyhow::{Context, Result}; use serde::Deserialize; use crate::crypto::{self, DecryptionKeys}; use crate::model::{self, Station, Train}; const TRAINS_URL: &str = "https://maps.amtrak.com/services/MapDataService/trains/getTrainsData"; const STATIONS_URL: &str = "https://maps.amtrak.com/services/MapDataService/stations/trainStations"; const ROUTES_URL: &str = "https://maps.amtrak.com/rttl/js/RoutesList.json"; const ROUTES_V_URL: &str = "https://maps.amtrak.com/rttl/js/RoutesList.v.json"; #[derive(Deserialize)] struct RouteEntry { #[serde(default)] #[serde(alias = "ZoomLevel")] zoom_level: Option, } #[derive(Deserialize)] struct RoutesVConfig { arr: Vec, s: Vec, v: Vec, } pub async fn fetch_decryption_keys(client: &reqwest::Client) -> Result { // Fetch route list to calculate masterZoom let routes: Vec = client .get(ROUTES_URL) .send() .await .context("failed to fetch routes list")? .json() .await .context("failed to parse routes list")?; let master_zoom = routes .iter() .map(|r| r.zoom_level.unwrap_or(0)) .sum::() as usize; // Fetch keys config let config: RoutesVConfig = client .get(ROUTES_V_URL) .send() .await .context("failed to fetch routes config")? .json() .await .context("failed to parse routes config")?; let public_key = config .arr .get(master_zoom) .context("masterZoom index out of bounds")? .clone(); let salt = crypto::extract_salt(&config.s); let iv = crypto::extract_iv(&config.v); Ok(DecryptionKeys { public_key, salt, iv, }) } pub async fn fetch_stations( client: &reqwest::Client, keys: &DecryptionKeys, ) -> Result> { let encrypted = client .get(STATIONS_URL) .send() .await .context("failed to fetch station data")? .text() .await .context("failed to read station data response")?; let json = crypto::decrypt_train_data(encrypted.trim(), keys) .context("failed to decrypt station data")?; model::parse_station_response(&json) } pub async fn fetch_trains(client: &reqwest::Client, keys: &DecryptionKeys) -> Result> { let encrypted = client .get(TRAINS_URL) .send() .await .context("failed to fetch train data")? .text() .await .context("failed to read train data response")?; let json = crypto::decrypt_train_data(encrypted.trim(), keys) .context("failed to decrypt train data")?; model::parse_train_response(&json) }