#[macro_use] extern crate tracing; use color_eyre::eyre::Result; use hyper::{header::CONTENT_TYPE, Body, Response}; use prometheus::{Encoder, TextEncoder}; use std::net::IpAddr; use std::str::FromStr; use std::sync::Arc; use tokio::net::UnixListener; use tokio_stream::wrappers::UnixListenerStream; use warp::{path, Filter}; pub mod app; pub mod handlers; pub mod post; pub mod signalboost; use app::State; const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); fn with_state( state: Arc, ) -> impl Filter,), Error = std::convert::Infallible> + Clone { warp::any().map(move || state.clone()) } #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; let _ = kankyo::init(); tracing_subscriber::fmt::init(); info!("starting up commit {}", env!("GITHUB_SHA")); let state = Arc::new( app::init( std::env::var("CONFIG_FNAME") .unwrap_or("./config.dhall".into()) .as_str() .into(), ) .await?, ); let healthcheck = warp::get().and(warp::path(".within").and(warp::path("health")).map(|| "OK")); let new_post = warp::path!(".within" / "website.within.xesite" / "new_post") .and(with_state(state.clone())) .and_then(handlers::feeds::new_post); let base = warp::path!("blog" / ..); let blog_index = base .and(warp::path::end()) .and(with_state(state.clone())) .and_then(handlers::blog::index); let series = base .and(warp::path!("series").and(with_state(state.clone()).and_then(handlers::blog::series))); let series_view = base.and( warp::path!("series" / String) .and(with_state(state.clone())) .and(warp::get()) .and_then(handlers::blog::series_view), ); let post_view = base.and( warp::path!(String) .and(with_state(state.clone())) .and(warp::get()) .and_then(handlers::blog::post_view), ); let gallery_base = warp::path!("gallery" / ..); let gallery_index = gallery_base .and(warp::path::end()) .and(with_state(state.clone())) .and_then(handlers::gallery::index); let gallery_post_view = gallery_base.and( warp::path!(String) .and(with_state(state.clone())) .and(warp::get()) .and_then(handlers::gallery::post_view), ); let talk_base = warp::path!("talks" / ..); let talk_index = talk_base .and(warp::path::end()) .and(with_state(state.clone())) .and_then(handlers::talks::index); let talk_post_view = talk_base.and( warp::path!(String) .and(with_state(state.clone())) .and(warp::get()) .and_then(handlers::talks::post_view), ); let index = warp::get().and(path::end().and_then(handlers::index)); let contact = warp::path!("contact").and_then(handlers::contact); let feeds = warp::path!("feeds").and_then(handlers::feeds); let resume = warp::path!("resume") .and(with_state(state.clone())) .and_then(handlers::resume); let signalboost = warp::path!("signalboost") .and(with_state(state.clone())) .and_then(handlers::signalboost); let patrons = warp::path!("patrons") .and(with_state(state.clone())) .and_then(handlers::patrons); let files = warp::path("static") .and(warp::fs::dir("./static")) .map(|reply| { warp::reply::with_header( reply, "Cache-Control", "public, max-age=86400, stale-if-error=60", ) }); let css = warp::path("css").and(warp::fs::dir("./css")).map(|reply| { warp::reply::with_header( reply, "Cache-Control", "public, max-age=86400, stale-if-error=60", ) }); let sw = warp::path("sw.js").and(warp::fs::file("./static/js/sw.js")); let robots = warp::path("robots.txt").and(warp::fs::file("./static/robots.txt")); let favicon = warp::path("favicon.ico").and(warp::fs::file("./static/favicon/favicon.ico")); let jsonfeed = warp::path("blog.json") .and(with_state(state.clone())) .and(warp::header::optional("if-none-match")) .and_then(handlers::feeds::jsonfeed); let atom = warp::path("blog.atom") .and(with_state(state.clone())) .and(warp::header::optional("if-none-match")) .and_then(handlers::feeds::atom); let rss = warp::path("blog.rss") .and(with_state(state.clone())) .and(warp::header::optional("if-none-match")) .and_then(handlers::feeds::rss); let sitemap = warp::path("sitemap.xml") .and(with_state(state.clone())) .and_then(handlers::feeds::sitemap); let go_vanity_jsonfeed = warp::path("jsonfeed") .and(warp::any().map(move || "christine.website/jsonfeed")) .and(warp::any().map(move || "https://tulpa.dev/Xe/jsonfeed")) .and(warp::any().map(move || "master")) .and_then(go_vanity::gitea); let metrics_endpoint = warp::path("metrics").and(warp::path::end()).map(move || { let encoder = TextEncoder::new(); let metric_families = prometheus::gather(); let mut buffer = vec![]; encoder.encode(&metric_families, &mut buffer).unwrap(); Response::builder() .status(200) .header(CONTENT_TYPE, encoder.format_type()) .body(Body::from(buffer)) .unwrap() }); let static_pages = index .or(feeds) .or(resume.or(signalboost)) .or(patrons) .or(jsonfeed.or(atom.or(sitemap)).or(rss)) .or(favicon.or(robots).or(sw)) .or(contact.or(new_post)) .map(|reply| { warp::reply::with_header( reply, "Cache-Control", "public, max-age=86400, stale-if-error=60", ) }); let dynamic_pages = blog_index .or(series.or(series_view).or(post_view)) .or(gallery_index.or(gallery_post_view)) .or(talk_index.or(talk_post_view)) .map(|reply| { warp::reply::with_header( reply, "Cache-Control", "public, max-age=600, stale-if-error=60", ) }); let site = static_pages .or(dynamic_pages) .or(healthcheck.or(metrics_endpoint).or(go_vanity_jsonfeed)) .or(files.or(css)) .map(|reply| { warp::reply::with_header( reply, "X-Hacker", "If you are reading this, check out /signalboost to find people for your team", ) }) .map(|reply| warp::reply::with_header(reply, "X-Clacks-Overhead", "GNU Ashlynn")) .map(|reply| { warp::reply::with_header( reply, "Link", format!( r#"<{}>; rel="webmention""#, std::env::var("WEBMENTION_URL") .unwrap_or("https://mi.within.website/api/webmention/accept".to_string()) ), ) }) .with(warp::log(APPLICATION_NAME)) .recover(handlers::rejection); #[cfg(target_os = "linux")] { match sdnotify::SdNotify::from_env() { Ok(ref mut n) => { // shitty heuristic for detecting if we're running in prod tokio::spawn(async { if let Err(why) = app::poke::the_cloud().await { error!("Unable to poke the cloud: {}", why); } }); n.notify_ready().map_err(|why| { error!("can't signal readiness to systemd: {}", why); why })?; n.set_status(format!("hosting {} posts", state.clone().everything.len())) .map_err(|why| { error!("can't signal status to systemd: {}", why); why })?; } Err(why) => error!("not running under systemd with Type=notify: {}", why), } } let server = warp::serve(site); match std::env::var("SOCKPATH") { Ok(sockpath) => { let _ = std::fs::remove_file(&sockpath); let listener = UnixListener::bind(sockpath)?; let incoming = UnixListenerStream::new(listener); server.run_incoming(incoming).await; Ok(()) } Err(_) => { server .run(( IpAddr::from_str(&std::env::var("HOST").unwrap_or("::".into()))?, std::env::var("PORT") .unwrap_or("3030".into()) .parse::()?, )) .await; Ok(()) } } } include!(concat!(env!("OUT_DIR"), "/templates.rs"));