From ca5727f35b3325f45ecadc654cc9b4e0274c7eb7 Mon Sep 17 00:00:00 2001 From: Celeo Date: Thu, 7 Mar 2024 20:11:39 -0800 Subject: [PATCH] More of the same --- src/endpoints/homepage.rs | 218 ++++++++++++++++++++++++++++++++++++++ src/endpoints/mod.rs | 214 +------------------------------------ src/main.rs | 1 + 3 files changed, 224 insertions(+), 209 deletions(-) create mode 100644 src/endpoints/homepage.rs diff --git a/src/endpoints/homepage.rs b/src/endpoints/homepage.rs new file mode 100644 index 0000000..5632615 --- /dev/null +++ b/src/endpoints/homepage.rs @@ -0,0 +1,218 @@ +//! HTTP endpoints. + +use crate::{ + shared::{AppError, AppState, CacheEntry, UserInfo, SESSION_USER_INFO_KEY}, + utils::{parse_metar, parse_vatsim_timestamp}, +}; +use anyhow::{anyhow, Result}; +use axum::{extract::State, http::StatusCode, response::Html, routing::get, Router}; +use log::warn; +use minijinja::{context, Environment}; +use serde::Serialize; +use std::{sync::Arc, time::Instant}; +use tower_sessions::Session; +use vatsim_utils::live_api::Vatsim; + +/// Homepage. +async fn handler_home( + State(state): State>, + session: Session, +) -> Result, StatusCode> { + let user_info: Option = session.get(SESSION_USER_INFO_KEY).await.unwrap(); + let template = state.templates.get_template("home").unwrap(); + let rendered = template.render(context! { user_info }).unwrap(); + Ok(Html(rendered)) +} + +/// Render a list of online controllers. +async fn snippet_online_controllers( + State(state): State>, +) -> Result, AppError> { + #[derive(Serialize)] + struct OnlineController { + cid: u64, + callsign: String, + name: String, + online_for: String, + } + + // cache this endpoint's returned data for 60 seconds + let cache_key = "ONLINE_CONTROLLERS"; + if let Some(cached) = state.cache.get(&cache_key) { + let elapsed = Instant::now() - cached.inserted; + if elapsed.as_secs() < 60 { + return Ok(Html(cached.data)); + } + state.cache.invalidate(&cache_key); + } + + let now = chrono::Utc::now(); + let data = Vatsim::new().await?.get_v3_data().await?; + let online: Vec<_> = data + .controllers + .iter() + .filter(|controller| { + let prefix_match = state + .config + .stats + .position_prefixes + .iter() + .any(|prefix| controller.callsign.starts_with(prefix)); + if !prefix_match { + return false; + } + state + .config + .stats + .position_suffixes + .iter() + .any(|suffix| controller.callsign.ends_with(suffix)) + }) + .map(|controller| { + let logon = parse_vatsim_timestamp(&controller.logon_time).unwrap(); + let seconds = (now - logon).num_seconds() as u32; + OnlineController { + cid: controller.cid, + callsign: controller.callsign.clone(), + name: controller.name.clone(), + online_for: format!("{}h{}m", seconds / 3600, (seconds / 60) % 60), + } + }) + .collect(); + + let template = state.templates.get_template("snippet_online_controllers")?; + let rendered = template.render(context! { online })?; + state + .cache + .insert(cache_key, CacheEntry::new(rendered.clone())); + Ok(Html(rendered)) +} + +async fn snippet_weather(State(state): State>) -> Result, AppError> { + // cache this endpoint's returned data for 5 minutes + let cache_key = "WEATHER_BRIEF"; + if let Some(cached) = state.cache.get(&cache_key) { + let elapsed = Instant::now() - cached.inserted; + if elapsed.as_secs() < 300 { + return Ok(Html(cached.data)); + } + state.cache.invalidate(&cache_key); + } + + let client = reqwest::ClientBuilder::new() + .user_agent("github.com/celeo/vzdv") + .build()?; + let resp = client + .get(format!( + "https://metar.vatsim.net/{}", + state.config.airports.weather_for.join(",") + )) + .send() + .await?; + if !resp.status().is_success() { + return Err(anyhow!("Got status {} from METAR API", resp.status().as_u16()).into()); + } + let text = resp.text().await?; + let weather: Vec<_> = text + .split_terminator('\n') + .flat_map(|line| { + parse_metar(line).map_err(|e| { + let airport = line.split(' ').next().unwrap_or("Unknown"); + warn!("Metar parsing failure for {airport}: {e}"); + e + }) + }) + .collect(); + + let template = state.templates.get_template("snippet_weather")?; + let rendered = template.render(context! { weather })?; + state + .cache + .insert(cache_key, CacheEntry::new(rendered.clone())); + Ok(Html(rendered)) +} + +async fn snippet_flights(State(state): State>) -> Result, AppError> { + #[derive(Serialize, Default)] + struct OnlineFlights { + within: u16, + from: u16, + to: u16, + } + + // cache this endpoint's returned data for 60 seconds + let cache_key = "ONLINE_FLIGHTS"; + if let Some(cached) = state.cache.get(&cache_key) { + let elapsed = Instant::now() - cached.inserted; + if elapsed.as_secs() < 60 { + return Ok(Html(cached.data)); + } + state.cache.invalidate(&cache_key); + } + + let artcc_fields: Vec<_> = state + .config + .airports + .all + .iter() + .map(|airport| &airport.code) + .collect(); + let data = Vatsim::new().await?.get_v3_data().await?; + let flights: OnlineFlights = + data.pilots + .iter() + .fold(OnlineFlights::default(), |mut flights, flight| { + if let Some(plan) = &flight.flight_plan { + let from = artcc_fields.contains(&&plan.departure); + let to = artcc_fields.contains(&&plan.arrival); + match (from, to) { + (true, true) => flights.within += 1, + (false, true) => flights.to += 1, + (true, false) => flights.from += 1, + _ => {} + } + }; + flights + }); + + let template = state.templates.get_template("snippet_flights")?; + let rendered = template.render(context! { flights })?; + state + .cache + .insert(cache_key, CacheEntry::new(rendered.clone())); + Ok(Html(rendered)) +} + +/// This file's routes and templates. +pub fn router(templates: &mut Environment) -> Router> { + templates + .add_template("home", include_str!("../../templates/home.jinja")) + .unwrap(); + templates + .add_template( + "snippet_online_controllers", + include_str!("../../templates/snippets/online_controllers.jinja"), + ) + .unwrap(); + templates + .add_template( + "snippet_weather", + include_str!("../../templates/snippets/weather.jinja"), + ) + .unwrap(); + templates + .add_template( + "snippet_flights", + include_str!("../../templates/snippets/flights.jinja"), + ) + .unwrap(); + + Router::new() + .route("/", get(handler_home)) + .route( + "/snippets/online/controllers", + get(snippet_online_controllers), + ) + .route("/snippets/online/flights", get(snippet_flights)) + .route("/snippets/weather", get(snippet_weather)) +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 56ccd73..556ec2b 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1,30 +1,14 @@ //! HTTP endpoints. -use crate::{ - shared::{AppError, AppState, CacheEntry, UserInfo, SESSION_USER_INFO_KEY}, - utils::{parse_metar, parse_vatsim_timestamp}, -}; -use anyhow::{anyhow, Result}; +use crate::shared::{AppState, UserInfo, SESSION_USER_INFO_KEY}; +use anyhow::Result; use axum::{extract::State, http::StatusCode, response::Html, routing::get, Router}; -use log::warn; use minijinja::{context, Environment}; -use serde::Serialize; -use std::{sync::Arc, time::Instant}; +use std::sync::Arc; use tower_sessions::Session; -use vatsim_utils::live_api::Vatsim; pub mod auth; - -/// Homepage. -async fn handler_home( - State(state): State>, - session: Session, -) -> Result, StatusCode> { - let user_info: Option = session.get(SESSION_USER_INFO_KEY).await.unwrap(); - let template = state.templates.get_template("home").unwrap(); - let rendered = template.render(context! { user_info }).unwrap(); - Ok(Html(rendered)) -} +pub mod homepage; /// 404 not found page. /// @@ -39,199 +23,11 @@ async fn handler_404( Ok(Html(rendered)) } -/// Render a list of online controllers. -async fn snippet_online_controllers( - State(state): State>, -) -> Result, AppError> { - #[derive(Serialize)] - struct OnlineController { - cid: u64, - callsign: String, - name: String, - online_for: String, - } - - // cache this endpoint's returned data for 60 seconds - let cache_key = "ONLINE_CONTROLLERS"; - if let Some(cached) = state.cache.get(&cache_key) { - let elapsed = Instant::now() - cached.inserted; - if elapsed.as_secs() < 60 { - return Ok(Html(cached.data)); - } - state.cache.invalidate(&cache_key); - } - - let now = chrono::Utc::now(); - let data = Vatsim::new().await?.get_v3_data().await?; - let online: Vec<_> = data - .controllers - .iter() - .filter(|controller| { - let prefix_match = state - .config - .stats - .position_prefixes - .iter() - .any(|prefix| controller.callsign.starts_with(prefix)); - if !prefix_match { - return false; - } - state - .config - .stats - .position_suffixes - .iter() - .any(|suffix| controller.callsign.ends_with(suffix)) - }) - .map(|controller| { - let logon = parse_vatsim_timestamp(&controller.logon_time).unwrap(); - let seconds = (now - logon).num_seconds() as u32; - OnlineController { - cid: controller.cid, - callsign: controller.callsign.clone(), - name: controller.name.clone(), - online_for: format!("{}h{}m", seconds / 3600, (seconds / 60) % 60), - } - }) - .collect(); - - let template = state.templates.get_template("snippet_online_controllers")?; - let rendered = template.render(context! { online })?; - state - .cache - .insert(cache_key, CacheEntry::new(rendered.clone())); - Ok(Html(rendered)) -} - -async fn snippet_weather(State(state): State>) -> Result, AppError> { - // cache this endpoint's returned data for 5 minutes - let cache_key = "WEATHER_BRIEF"; - if let Some(cached) = state.cache.get(&cache_key) { - let elapsed = Instant::now() - cached.inserted; - if elapsed.as_secs() < 300 { - return Ok(Html(cached.data)); - } - state.cache.invalidate(&cache_key); - } - - let client = reqwest::ClientBuilder::new() - .user_agent("github.com/celeo/vzdv") - .build()?; - let resp = client - .get(format!( - "https://metar.vatsim.net/{}", - state.config.airports.weather_for.join(",") - )) - .send() - .await?; - if !resp.status().is_success() { - return Err(anyhow!("Got status {} from METAR API", resp.status().as_u16()).into()); - } - let text = resp.text().await?; - let weather: Vec<_> = text - .split_terminator('\n') - .flat_map(|line| { - parse_metar(line).map_err(|e| { - let airport = line.split(' ').next().unwrap_or("Unknown"); - warn!("Metar parsing failure for {airport}: {e}"); - e - }) - }) - .collect(); - - let template = state.templates.get_template("snippet_weather")?; - let rendered = template.render(context! { weather })?; - state - .cache - .insert(cache_key, CacheEntry::new(rendered.clone())); - Ok(Html(rendered)) -} - -async fn snippet_flights(State(state): State>) -> Result, AppError> { - #[derive(Serialize, Default)] - struct OnlineFlights { - within: u16, - from: u16, - to: u16, - } - - // cache this endpoint's returned data for 60 seconds - let cache_key = "ONLINE_FLIGHTS"; - if let Some(cached) = state.cache.get(&cache_key) { - let elapsed = Instant::now() - cached.inserted; - if elapsed.as_secs() < 60 { - return Ok(Html(cached.data)); - } - state.cache.invalidate(&cache_key); - } - - let artcc_fields: Vec<_> = state - .config - .airports - .all - .iter() - .map(|airport| &airport.code) - .collect(); - let data = Vatsim::new().await?.get_v3_data().await?; - let flights: OnlineFlights = - data.pilots - .iter() - .fold(OnlineFlights::default(), |mut flights, flight| { - if let Some(plan) = &flight.flight_plan { - let from = artcc_fields.contains(&&plan.departure); - let to = artcc_fields.contains(&&plan.arrival); - match (from, to) { - (true, true) => flights.within += 1, - (false, true) => flights.to += 1, - (true, false) => flights.from += 1, - _ => {} - } - }; - flights - }); - - let template = state.templates.get_template("snippet_flights")?; - let rendered = template.render(context! { flights })?; - state - .cache - .insert(cache_key, CacheEntry::new(rendered.clone())); - Ok(Html(rendered)) -} - /// This file's routes and templates. pub fn router(templates: &mut Environment) -> Router> { - templates - .add_template("home", include_str!("../../templates/home.jinja")) - .unwrap(); templates .add_template("404", include_str!("../../templates/404.jinja")) .unwrap(); - templates - .add_template( - "snippet_online_controllers", - include_str!("../../templates/snippets/online_controllers.jinja"), - ) - .unwrap(); - templates - .add_template( - "snippet_weather", - include_str!("../../templates/snippets/weather.jinja"), - ) - .unwrap(); - templates - .add_template( - "snippet_flights", - include_str!("../../templates/snippets/flights.jinja"), - ) - .unwrap(); - Router::new() - .route("/404", get(handler_404)) - .route("/", get(handler_home)) - .route( - "/snippets/online/controllers", - get(snippet_online_controllers), - ) - .route("/snippets/online/flights", get(snippet_flights)) - .route("/snippets/weather", get(snippet_weather)) + Router::new().route("/404", get(handler_404)) } diff --git a/src/main.rs b/src/main.rs index 674731c..ffb5b81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,6 +89,7 @@ fn load_router( ) -> Router> { Router::new() .merge(endpoints::router(env)) + .merge(endpoints::homepage::router(env)) .merge(endpoints::auth::router(env)) .layer( ServiceBuilder::new()