271 lines
9.0 KiB
Rust
271 lines
9.0 KiB
Rust
#[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<State>,
|
|
) -> impl Filter<Extract = (Arc<State>,), 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 asset_links = warp::path!(".well-known" / "assetlinks.json")
|
|
.and(warp::fs::file("./static/assetlinks.json"));
|
|
|
|
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(asset_links))
|
|
.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::<u16>()?,
|
|
))
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|