use crate::{ config::{Config, CONFIG}, server::RequestExt, utils::{ErrorTemplate, Preferences}, }; use askama::Template; use build_html::{Container, Html, HtmlContainer, Table}; use hyper::{http::Error, Body, Request, Response}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; // This is the local static that is intialized at runtime (technically at // the first request to the info endpoint) and contains the data // retrieved from the info endpoint. pub static INSTANCE_INFO: Lazy = Lazy::new(InstanceInfo::new); /// Handles instance info endpoint pub async fn instance_info(req: Request) -> Result, String> { // This will retrieve the extension given, or create a new string - which will // simply become the last option, an HTML page. let extension = req.param("extension").unwrap_or_default(); let response = match extension.as_str() { "yaml" | "yml" => info_yaml(), "txt" => info_txt(), "json" => info_json(), "html" | "" => info_html(&req), _ => { let error = ErrorTemplate { msg: "Error: Invalid info extension".into(), prefs: Preferences::new(&req), url: req.uri().to_string(), } .render() .unwrap(); Response::builder().status(404).header("content-type", "text/html; charset=utf-8").body(error.into()) } }; response.map_err(|err| format!("{err}")) } fn info_json() -> Result, Error> { if let Ok(body) = serde_json::to_string(&*INSTANCE_INFO) { Response::builder().status(200).header("content-type", "application/json").body(body.into()) } else { Response::builder() .status(500) .header("content-type", "text/plain") .body(Body::from("Error serializing JSON")) } } fn info_yaml() -> Result, Error> { if let Ok(body) = serde_yaml::to_string(&*INSTANCE_INFO) { // We can use `application/yaml` as media type, though there is no guarantee // that browsers will honor it. But we'll do it anyway. See: // https://github.com/ietf-wg-httpapi/mediatypes/blob/main/draft-ietf-httpapi-yaml-mediatypes.md#media-type-applicationyaml-application-yaml Response::builder().status(200).header("content-type", "application/yaml").body(body.into()) } else { Response::builder() .status(500) .header("content-type", "text/plain") .body(Body::from("Error serializing YAML.")) } } fn info_txt() -> Result, Error> { Response::builder() .status(200) .header("content-type", "text/plain") .body(Body::from(INSTANCE_INFO.to_string(&StringType::Raw))) } fn info_html(req: &Request) -> Result, Error> { let message = MessageTemplate { title: String::from("Instance information"), body: INSTANCE_INFO.to_string(&StringType::Html), prefs: Preferences::new(req), url: req.uri().to_string(), } .render() .unwrap(); Response::builder().status(200).header("content-type", "text/html; charset=utf8").body(Body::from(message)) } #[derive(Serialize, Deserialize, Default)] pub struct InstanceInfo { package_name: String, crate_version: String, git_commit: String, deploy_date: String, compile_mode: String, deploy_unix_ts: i64, config: Config, } impl InstanceInfo { pub fn new() -> Self { Self { package_name: env!("CARGO_PKG_NAME").to_string(), crate_version: env!("CARGO_PKG_VERSION").to_string(), git_commit: env!("GIT_HASH").to_string(), deploy_date: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()).to_string(), #[cfg(debug_assertions)] compile_mode: "Debug".into(), #[cfg(not(debug_assertions))] compile_mode: "Release".into(), deploy_unix_ts: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()).unix_timestamp(), config: CONFIG.clone(), } } fn to_table(&self) -> String { let mut container = Container::default(); let convert = |o: &Option| -> String { o.clone().unwrap_or_else(|| "Unset".to_owned()) }; if let Some(banner) = &self.config.banner { container.add_header(3, "Instance banner"); container.add_raw("
"); container.add_paragraph(banner); container.add_raw("
"); } container.add_table( Table::from([ ["Package name", &self.package_name], ["Crate version", &self.crate_version], ["Git commit", &self.git_commit], ["Deploy date", &self.deploy_date], ["Deploy timestamp", &self.deploy_unix_ts.to_string()], ["Compile mode", &self.compile_mode], ["SFW only", &convert(&self.config.sfw_only)], ["Pushshift frontend", &convert(&self.config.pushshift)], ["RSS enabled", &convert(&self.config.enable_rss)], ["Full URL", &convert(&self.config.full_url)], //TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND ]) .with_header_row(["Settings"]), ); container.add_raw("
"); container.add_table( Table::from([ ["Hide awards", &convert(&self.config.default_hide_awards)], ["Hide score", &convert(&self.config.default_hide_score)], ["Theme", &convert(&self.config.default_theme)], ["Front page", &convert(&self.config.default_front_page)], ["Layout", &convert(&self.config.default_layout)], ["Wide", &convert(&self.config.default_wide)], ["Comment sort", &convert(&self.config.default_comment_sort)], ["Post sort", &convert(&self.config.default_post_sort)], ["Blur Spoiler", &convert(&self.config.default_blur_spoiler)], ["Show NSFW", &convert(&self.config.default_show_nsfw)], ["Blur NSFW", &convert(&self.config.default_blur_nsfw)], ["Use HLS", &convert(&self.config.default_use_hls)], ["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)], ["Subscriptions", &convert(&self.config.default_subscriptions)], ["Filters", &convert(&self.config.default_filters)], ]) .with_header_row(["Default preferences"]), ); container.to_html_string().replace("", "") } fn to_string(&self, string_type: &StringType) -> String { match string_type { StringType::Raw => { format!( "Package name: {}\n Crate version: {}\n Git commit: {}\n Deploy date: {}\n Deploy timestamp: {}\n Compile mode: {}\n SFW only: {:?}\n Pushshift frontend: {:?}\n RSS enabled: {:?}\n Full URL: {:?}\n Config:\n Banner: {:?}\n Hide awards: {:?}\n Hide score: {:?}\n Default theme: {:?}\n Default front page: {:?}\n Default layout: {:?}\n Default wide: {:?}\n Default comment sort: {:?}\n Default post sort: {:?}\n Default blur Spoiler: {:?}\n Default show NSFW: {:?}\n Default blur NSFW: {:?}\n Default use HLS: {:?}\n Default hide HLS notification: {:?}\n Default subscriptions: {:?}\n Default filters: {:?}\n", self.package_name, self.crate_version, self.git_commit, self.deploy_date, self.deploy_unix_ts, self.compile_mode, self.config.sfw_only, self.config.enable_rss, self.config.full_url, self.config.pushshift, self.config.banner, self.config.default_hide_awards, self.config.default_hide_score, self.config.default_theme, self.config.default_front_page, self.config.default_layout, self.config.default_wide, self.config.default_comment_sort, self.config.default_post_sort, self.config.default_blur_spoiler, self.config.default_show_nsfw, self.config.default_blur_nsfw, self.config.default_use_hls, self.config.default_hide_hls_notification, self.config.default_subscriptions, self.config.default_filters, ) } StringType::Html => self.to_table(), } } } enum StringType { Raw, Html, } #[derive(Template)] #[template(path = "message.html")] struct MessageTemplate { title: String, body: String, prefs: Preferences, url: String, }