Rewrite the site routing with Axum (#441)
* broken state Signed-off-by: Xe Iaso <me@christine.website> * fix??? Signed-off-by: Xe Iaso <me@christine.website> * Port everything else to axum Signed-off-by: Xe <me@christine.website> * headers Signed-off-by: Xe Iaso <me@christine.website> * site update post Signed-off-by: Christine Dodrill <me@christine.website> * fix headers Signed-off-by: Xe Iaso <me@christine.website> * remove warp example Signed-off-by: Xe Iaso <me@christine.website> * 80c wrap Signed-off-by: Xe Iaso <me@christine.website> * bump version Signed-off-by: Xe Iaso <me@christine.website>
This commit is contained in:
parent
f45ca40ae1
commit
8b747c1c40
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "xesite"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
authors = ["Xe Iaso <me@christine.website>"]
|
||||
edition = "2018"
|
||||
build = "src/build.rs"
|
||||
|
@ -9,13 +9,19 @@ repository = "https://github.com/Xe/site"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = "0.4"
|
||||
axum-macros = "0.1"
|
||||
axum-extra = "0.1"
|
||||
color-eyre = "0.6"
|
||||
chrono = "0.4"
|
||||
comrak = "0.12.1"
|
||||
derive_more = "0.99"
|
||||
envy = "0.4"
|
||||
estimated_read_time = "1"
|
||||
futures = "0.3"
|
||||
glob = "0.3"
|
||||
http = "0.2"
|
||||
http-body = "0.4"
|
||||
hyper = "0.14"
|
||||
kankyo = "0.3"
|
||||
lazy_static = "1.4"
|
||||
|
@ -34,24 +40,30 @@ tokio-stream = { version = "0.1", features = ["net"] }
|
|||
tracing = "0.1"
|
||||
tracing-futures = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
warp = "0.3"
|
||||
xml-rs = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
||||
# workspace dependencies
|
||||
cfcache = { path = "./lib/cfcache" }
|
||||
go_vanity = { path = "./lib/go_vanity" }
|
||||
jsonfeed = { path = "./lib/jsonfeed" }
|
||||
mi = { path = "./lib/mi" }
|
||||
patreon = { path = "./lib/patreon" }
|
||||
|
||||
[dependencies.tower]
|
||||
version = "0.4"
|
||||
features = [ "full" ]
|
||||
|
||||
[dependencies.tower-http]
|
||||
version = "0.2"
|
||||
features = [ "full" ]
|
||||
|
||||
# os-specific dependencies
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
sdnotify = { version = "0.2", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
ructe = { version = "0.13", features = ["warp02"] }
|
||||
ructe = { version = "0.13", features = [ "mime03" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
pfacts = "0"
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
title: "Site Update: Axum"
|
||||
date: 2022-03-21
|
||||
---
|
||||
|
||||
I have made a bunch of huge changes to my website that hopefully you won't
|
||||
notice unless you read this post that points them out to you. I have redone how
|
||||
the website's URL routing works to use
|
||||
[axum](https://tokio.rs/blog/2021-07-announcing-axum) instead of
|
||||
[warp](https://docs.rs/warp/latest/warp/).
|
||||
|
||||
I chose warp fairly arbitrarily when I was getting into the swing of Rust. This
|
||||
choice turned out to be a bit of a mistake. Don't get me wrong, warp is a
|
||||
fantastic framework, but overall I've not been happy with how it impacts compile
|
||||
times. Warp works by pushing a lot of the complexities with HTTP routing into
|
||||
the type system. This can lead to undebuggable inscruitable types that make it
|
||||
really hard to understand what is wrong. Here is the autogenerated type for the
|
||||
`/blog/:name` route:
|
||||
|
||||
```
|
||||
warp::filter::and::And<warp::filter::and::And<impl warp::filter::FilterBase<Extract = (), Error = Infallible> + warp::Filter + std::marker::Copy, Exact<warp::path::internal::Opaque<main::{closure#0}::__StaticPath>>>, warp::filter::and_then::AndThen<warp::filter::and::And<warp::filter::and::And<warp::filter::and::And<warp::filter::and::And<impl warp::filter::FilterBase<Extract = (), Error = Infallible> + warp::Filter + std::marker::Copy, impl warp::filter::FilterBase<Extract = (std::string::String,), Error = Rejection> + warp::Filter + std::marker::Copy>, impl warp::filter::FilterBase<Extract = (), Error = Rejection> + warp::Filter + std::marker::Copy>, impl warp::filter::FilterBase<Extract = (Arc<app::State>,), Error = Infallible> + warp::Filter + Clone>, impl warp::filter::FilterBase<Extract = (), Error = Rejection> + warp::Filter + std::marker::Copy>, fn(std::string::String, Arc<app::State>) -> impl warp::Future<Output = Result<Opaque(DefId(0:1249 ~ xesite[3495]::handlers::blog::post_view::{opaque#0}::{opaque#0}), []), Rejection>> {blog::post_view}>>
|
||||
```
|
||||
|
||||
[What the heck is that? Is that a binary tree?](conversation://Numa/delet)
|
||||
|
||||
[Yep. It's insufferable to try and debug too.](conversation://Cadey/coffee)
|
||||
|
||||
Yeah, it's really hard to understand what's going on in error messages because
|
||||
of this. This also means that the routes are put into a binary tree in the type
|
||||
system, which means if your tree is unbalanced then you get slower compile times
|
||||
and a slight hit at runtime as the framework traverses your binary tree to
|
||||
figure out what to do. This has also made it difficult for me to add features
|
||||
such as [historical views of my RSS feed](https://github.com/Xe/site/issues/419)
|
||||
or other things I want to add like the April Fools day feature I've been working
|
||||
on.
|
||||
|
||||
When I went out framework shopping, I tried a few things and got reccomendations
|
||||
from a trusted friend before I finally settled on axum as the heart of this
|
||||
website. Axum has a few major advantages that bbrought me "in the door":
|
||||
|
||||
- It's maintained by the tokio team
|
||||
- It leverages the type system of Rust to make writing handlers easier
|
||||
- It uses extractors (think lenses) to help you pick out the subset of data you
|
||||
need, not blindly giving you everything and hoping you figure it out
|
||||
- It has sub-routers which can have different middleware stacks than the main
|
||||
one (useful for things like API authentication)
|
||||
|
||||
And it has these disadvantages:
|
||||
|
||||
- Writing middleware is kinda weird (though this may be because I'm not used to
|
||||
working with tower), but easy once you get the general flow of things
|
||||
- I can't find a way to have the template data get continuously piped to client
|
||||
connections instead of rendering it to a buffer and then writing that buffer
|
||||
to the client
|
||||
- It doesn't have the biggest mindshare and one of the best ways to get unstuck
|
||||
at the time of writing this article is to ask on their Discord server
|
||||
|
||||
Overall, I've been happy with the experience of porting over my site to using
|
||||
Axum. I did [a stream on Twitch](https://www.twitch.tv/videos/1429533858) where
|
||||
I ported it all over if you want to watch the process and hear my thought
|
||||
processes as I was figuring things out.
|
||||
|
||||
As users, nothing should have changed about this site. However I'm almost
|
||||
certain that I did forget to port _something_ over, so if I missed something you
|
||||
rely on, [get in contact](/contact). I have not gotten the Patreon API
|
||||
interoperability code fixed yet, so that is the next major issue. I am going to
|
||||
figure out how refresh tokens work the hard way and make the patrons page
|
||||
auto-updating instead of having to [manually get a new API key every
|
||||
month](https://github.com/Xe/site/blob/main/docs/patron-page.org). I am also
|
||||
looking into having that patrons page be updated by a cronjob that emits json to
|
||||
the disk and have my site load from that instead of just hoping that the patreon
|
||||
API credentials are up to date. We'll see how that goes, but you can track that
|
||||
[here](https://github.com/Xe/site/issues/442). I will likely do a livestream for
|
||||
this.
|
||||
|
||||
I have also contacted a copyeditor for my blog. I am so happy with the results
|
||||
so far. [My Devops post](/blog/social-quandry-devops-2022-03-17) was the first
|
||||
thing that the editor reviewed and they absolutely tore my first draft in half
|
||||
and helped me put the parts back together into something more palateable. I am
|
||||
beyond satisfied with this and will continue to use this editor in the future. I
|
||||
wish I had gotten a copyeditor sooner.
|
|
@ -1,14 +0,0 @@
|
|||
use warp::Filter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let hello = warp::path!("hello" / String)
|
||||
.map(|name| format!("Hello, {}!", name));
|
||||
let health = warp::path!(".within" / "health")
|
||||
.map(|| "OK");
|
||||
let routes = hello.or(health);
|
||||
|
||||
warp::serve(routes)
|
||||
.run(([0, 0, 0, 0], 3030))
|
||||
.await;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "go_vanity"
|
||||
version = "0.2.0"
|
||||
authors = ["Xe Iaso <me@christine.website>"]
|
||||
edition = "2018"
|
||||
build = "src/build.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
mime = "0.3"
|
||||
warp = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = { version = "0.13", features = ["warp02"] }
|
|
@ -1,5 +0,0 @@
|
|||
use ructe::{Result, Ructe};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
Ructe::from_env()?.compile_templates("templates")
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
use crate::templates::RenderRucte;
|
||||
use warp::{http::Response, Rejection, Reply};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
|
||||
pub async fn gitea(pkg_name: &str, git_repo: &str, branch: &str) -> Result<impl Reply, Rejection> {
|
||||
Response::builder().html(|o| templates::gitea_html(o, pkg_name, git_repo, branch))
|
||||
}
|
||||
|
||||
pub async fn github(pkg_name: &str, git_repo: &str, branch: &str) -> Result<impl Reply, Rejection> {
|
||||
Response::builder().html(|o| templates::github_html(o, pkg_name, git_repo, branch))
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
@(pkg_name: &str, git_repo: &str, branch: &str)
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="go-import" content="@pkg_name git @git_repo">
|
||||
<meta name="go-source" content="@pkg_name @git_repo @git_repo/tree/@branch@{/dir@} @git_repo/blob/@branch@{/dir@}/@{file@}#L@{line@}">
|
||||
<meta http-equiv="refresh" content="0; url=https://godoc.org/@pkg_name">
|
||||
</head>
|
||||
<body>
|
||||
Please see <a href="https://godoc.org/@pkg_name">here</a> for documentation on this package.
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,6 @@
|
|||
use color_eyre::eyre::Result;
|
||||
use reqwest::header;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
const USER_AGENT_BASE: &str = concat!(
|
||||
|
@ -58,7 +58,7 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq, Clone)]
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq, Clone, Serialize)]
|
||||
pub struct WebMention {
|
||||
pub source: String,
|
||||
pub title: Option<String>,
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
use axum::extract::connect_info;
|
||||
use futures::ready;
|
||||
use hyper::{
|
||||
client::connect::{Connected, Connection},
|
||||
server::accept::Accept,
|
||||
};
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::{unix::UCred, UnixListener, UnixStream},
|
||||
};
|
||||
use tower::BoxError;
|
||||
|
||||
pub struct ServerAccept {
|
||||
pub uds: UnixListener,
|
||||
}
|
||||
|
||||
impl Accept for ServerAccept {
|
||||
type Conn = UnixStream;
|
||||
type Error = BoxError;
|
||||
|
||||
fn poll_accept(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
|
||||
let (stream, _addr) = ready!(self.uds.poll_accept(cx))?;
|
||||
Poll::Ready(Some(Ok(stream)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientConnection {
|
||||
pub stream: UnixStream,
|
||||
}
|
||||
|
||||
impl AsyncWrite for ClientConnection {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
Pin::new(&mut self.stream).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
Pin::new(&mut self.stream).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), io::Error>> {
|
||||
Pin::new(&mut self.stream).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for ClientConnection {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection for ClientConnection {
|
||||
fn connected(&self) -> Connected {
|
||||
Connected::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct UdsConnectInfo {
|
||||
pub peer_addr: Arc<tokio::net::unix::SocketAddr>,
|
||||
pub peer_cred: UCred,
|
||||
}
|
||||
|
||||
impl connect_info::Connected<&UnixStream> for UdsConnectInfo {
|
||||
fn connect_info(target: &UnixStream) -> Self {
|
||||
let peer_addr = target.peer_addr().unwrap();
|
||||
let peer_cred = target.peer_cred().unwrap();
|
||||
|
||||
Self {
|
||||
peer_addr: Arc::new(peer_addr),
|
||||
peer_cred,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
use super::{PostNotFound, SeriesNotFound, LAST_MODIFIED};
|
||||
use crate::{
|
||||
app::State,
|
||||
post::Post,
|
||||
templates::{self, Html, RenderRucte},
|
||||
use super::Result;
|
||||
use crate::{app::State, post::Post, templates};
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
response::Html,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, instrument};
|
||||
use warp::{http::Response, Rejection, Reply};
|
||||
|
||||
lazy_static! {
|
||||
static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!(
|
||||
|
@ -19,17 +18,18 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
let state = state.clone();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::blogindex_html(o, state.blog.clone()))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::blogindex_html(&mut result, state.blog.clone())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn series(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn series(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
let state = state.clone();
|
||||
let mut series: Vec<String> = vec![];
|
||||
let mut result: Vec<u8> = vec![];
|
||||
|
||||
for post in &state.blog {
|
||||
if post.front_matter.series.is_some() {
|
||||
|
@ -40,15 +40,18 @@ pub async fn series(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
|||
series.sort();
|
||||
series.dedup();
|
||||
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::series_html(o, series))
|
||||
templates::series_html(&mut result, series)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn series_view(series: String, state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn series_view(
|
||||
Path(series): Path<String>,
|
||||
Extension(state): Extension<Arc<State>>,
|
||||
) -> Result {
|
||||
let state = state.clone();
|
||||
let mut posts: Vec<Post> = vec![];
|
||||
let mut result: Vec<u8> = vec![];
|
||||
|
||||
for post in &state.blog {
|
||||
if post.front_matter.series.is_none() {
|
||||
|
@ -62,16 +65,18 @@ pub async fn series_view(series: String, state: Arc<State>) -> Result<impl Reply
|
|||
|
||||
if posts.len() == 0 {
|
||||
error!("series not found");
|
||||
Err(SeriesNotFound(series).into())
|
||||
} else {
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::series_posts_html(o, series, &posts))
|
||||
return Err(super::Error::SeriesNotFound(series));
|
||||
}
|
||||
|
||||
templates::series_posts_html(&mut result, series, &posts).unwrap();
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn post_view(
|
||||
Path(name): Path<String>,
|
||||
Extension(state): Extension<Arc<State>>,
|
||||
) -> Result {
|
||||
let mut want: Option<Post> = None;
|
||||
|
||||
for post in &state.blog {
|
||||
|
@ -81,15 +86,15 @@ pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Re
|
|||
}
|
||||
|
||||
match want {
|
||||
None => Err(PostNotFound("blog".into(), name).into()),
|
||||
None => Err(super::Error::PostNotFound(name)),
|
||||
Some(post) => {
|
||||
HIT_COUNTER
|
||||
.with_label_values(&[name.clone().as_str()])
|
||||
.inc();
|
||||
let body = Html(post.body_html.clone());
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::blogpost_html(o, post, body))
|
||||
let body = templates::Html(post.body_html.clone());
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::blogpost_html(&mut result, post, body)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use super::LAST_MODIFIED;
|
||||
use crate::{app::State, post::Post, templates};
|
||||
use super::{Result, LAST_MODIFIED};
|
||||
use crate::{
|
||||
app::State,
|
||||
post::{NewPost, Post},
|
||||
templates,
|
||||
};
|
||||
use axum::{body, extract::Extension, response::Response, Json};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
||||
use std::{io, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use warp::{http::Response, Rejection, Reply};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!(
|
||||
|
@ -16,103 +20,56 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn jsonfeed(state: Arc<State>, since: Option<String>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn jsonfeed(Extension(state): Extension<Arc<State>>) -> Json<jsonfeed::Feed> {
|
||||
HIT_COUNTER.with_label_values(&["json"]).inc();
|
||||
let state = state.clone();
|
||||
Ok(warp::reply::json(&state.jf))
|
||||
Json(state.jf.clone())
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn new_post(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
#[axum_macros::debug_handler]
|
||||
pub async fn new_post(Extension(state): Extension<Arc<State>>) -> Result<Json<NewPost>> {
|
||||
let state = state.clone();
|
||||
let everything = state.everything.clone();
|
||||
let p: &Post = everything.iter().next().unwrap();
|
||||
Ok(warp::reply::json(&p.new_post))
|
||||
let p: Post = state.everything.iter().next().unwrap().clone();
|
||||
Ok(Json(p.new_post))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RenderError {
|
||||
Build(warp::http::Error),
|
||||
IO(io::Error),
|
||||
}
|
||||
|
||||
impl warp::reject::Reject for RenderError {}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn atom(state: Arc<State>, since: Option<String>) -> Result<impl Reply, Rejection> {
|
||||
if let Some(etag) = since {
|
||||
if etag == ETAG.clone() {
|
||||
return Response::builder()
|
||||
.status(304)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(
|
||||
"You already have the newest version of this feed."
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.map_err(RenderError::Build)
|
||||
.map_err(warp::reject::custom);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn atom(Extension(state): Extension<Arc<State>>) -> Result<Response> {
|
||||
HIT_COUNTER.with_label_values(&["atom"]).inc();
|
||||
let state = state.clone();
|
||||
let mut buf = Vec::new();
|
||||
templates::blog_atom_xml(&mut buf, state.everything.clone())
|
||||
.map_err(RenderError::IO)
|
||||
.map_err(warp::reject::custom)?;
|
||||
Response::builder()
|
||||
templates::blog_atom_xml(&mut buf, state.everything.clone())?;
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/atom+xml")
|
||||
.header("ETag", ETAG.clone())
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.body(buf)
|
||||
.map_err(RenderError::Build)
|
||||
.map_err(warp::reject::custom)
|
||||
.body(body::boxed(body::Full::from(buf)))?)
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn rss(state: Arc<State>, since: Option<String>) -> Result<impl Reply, Rejection> {
|
||||
if let Some(etag) = since {
|
||||
if etag == ETAG.clone() {
|
||||
return Response::builder()
|
||||
.status(304)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(
|
||||
"You already have the newest version of this feed."
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.map_err(RenderError::Build)
|
||||
.map_err(warp::reject::custom);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rss(Extension(state): Extension<Arc<State>>) -> Result<Response> {
|
||||
HIT_COUNTER.with_label_values(&["rss"]).inc();
|
||||
let state = state.clone();
|
||||
let mut buf = Vec::new();
|
||||
templates::blog_rss_xml(&mut buf, state.everything.clone())
|
||||
.map_err(RenderError::IO)
|
||||
.map_err(warp::reject::custom)?;
|
||||
Response::builder()
|
||||
templates::blog_rss_xml(&mut buf, state.everything.clone())?;
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/rss+xml")
|
||||
.header("ETag", ETAG.clone())
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.body(buf)
|
||||
.map_err(RenderError::Build)
|
||||
.map_err(warp::reject::custom)
|
||||
.body(body::boxed(body::Full::from(buf)))?)
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn sitemap(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
#[axum_macros::debug_handler]
|
||||
pub async fn sitemap(Extension(state): Extension<Arc<State>>) -> Result<Response> {
|
||||
HIT_COUNTER.with_label_values(&["sitemap"]).inc();
|
||||
let state = state.clone();
|
||||
Response::builder()
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/xml")
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.body(state.sitemap.clone())
|
||||
.map_err(RenderError::Build)
|
||||
.map_err(warp::reject::custom)
|
||||
.body(body::boxed(body::Full::from(state.sitemap.clone())))?)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
use super::PostNotFound;
|
||||
use crate::{
|
||||
app::State,
|
||||
post::Post,
|
||||
templates::{self, Html, RenderRucte},
|
||||
use super::{Error::*, Result};
|
||||
use crate::{app::State, post::Post, templates};
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
response::Html,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use warp::{http::Response, Rejection, Reply};
|
||||
|
||||
lazy_static! {
|
||||
static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!(
|
||||
|
@ -19,13 +18,18 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
let state = state.clone();
|
||||
Response::builder().html(|o| templates::galleryindex_html(o, state.gallery.clone()))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::galleryindex_html(&mut result, state.gallery.clone())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn post_view(
|
||||
Path(name): Path<String>,
|
||||
Extension(state): Extension<Arc<State>>,
|
||||
) -> Result {
|
||||
let mut want: Option<Post> = None;
|
||||
|
||||
for post in &state.gallery {
|
||||
|
@ -35,13 +39,15 @@ pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Re
|
|||
}
|
||||
|
||||
match want {
|
||||
None => Err(PostNotFound("gallery".into(), name).into()),
|
||||
None => Err(PostNotFound(name)),
|
||||
Some(post) => {
|
||||
HIT_COUNTER
|
||||
.with_label_values(&[name.clone().as_str()])
|
||||
.inc();
|
||||
let body = Html(post.body_html.clone());
|
||||
Response::builder().html(|o| templates::gallerypost_html(o, post, body))
|
||||
let body = templates::Html(post.body_html.clone());
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::gallerypost_html(&mut result, post, body)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
use crate::{
|
||||
app::State,
|
||||
templates::{self, Html, RenderRucte},
|
||||
use crate::{app::State, templates};
|
||||
use axum::{
|
||||
body,
|
||||
extract::Extension,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use chrono::{Datelike, Timelike, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
||||
use std::{convert::Infallible, fmt, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use warp::{
|
||||
http::{Response, StatusCode},
|
||||
Rejection, Reply,
|
||||
};
|
||||
|
||||
pub mod blog;
|
||||
pub mod feeds;
|
||||
pub mod gallery;
|
||||
pub mod talks;
|
||||
|
||||
lazy_static! {
|
||||
static ref HIT_COUNTER: IntCounterVec =
|
||||
|
@ -32,139 +36,104 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn index() -> Result<impl Reply, Rejection> {
|
||||
pub async fn index() -> Result {
|
||||
HIT_COUNTER.with_label_values(&["index"]).inc();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::index_html(o))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::index_html(&mut result)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn contact() -> Result<impl Reply, Rejection> {
|
||||
pub async fn contact() -> Result {
|
||||
HIT_COUNTER.with_label_values(&["contact"]).inc();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::contact_html(o))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::contact_html(&mut result)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn feeds() -> Result<impl Reply, Rejection> {
|
||||
pub async fn feeds() -> Result {
|
||||
HIT_COUNTER.with_label_values(&["feeds"]).inc();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::feeds_html(o))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::feeds_html(&mut result)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn resume(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
HIT_COUNTER.with_label_values(&["resume"]).inc();
|
||||
let state = state.clone();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::resume_html(o, Html(state.resume.clone())))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::resume_html(&mut result, templates::Html(state.resume.clone()))?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn patrons(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn patrons(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
HIT_COUNTER.with_label_values(&["patrons"]).inc();
|
||||
let state = state.clone();
|
||||
let mut result: Vec<u8> = vec![];
|
||||
match &state.patrons {
|
||||
None => Response::builder().status(500).html(|o| {
|
||||
templates::error_html(
|
||||
o,
|
||||
"Could not load patrons, let me know the API token expired again".to_string(),
|
||||
)
|
||||
}),
|
||||
Some(patrons) => Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::patrons_html(o, patrons.clone())),
|
||||
None => Err(Error::NoPatrons),
|
||||
Some(patrons) => {
|
||||
templates::patrons_html(&mut result, patrons.clone())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn signalboost(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
HIT_COUNTER.with_label_values(&["signalboost"]).inc();
|
||||
let state = state.clone();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::signalboost_html(o, state.signalboost.clone()))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::signalboost_html(&mut result, state.signalboost.clone())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn not_found() -> Result<impl Reply, Rejection> {
|
||||
pub async fn not_found() -> Result {
|
||||
HIT_COUNTER.with_label_values(&["not_found"]).inc();
|
||||
Response::builder()
|
||||
.header("Last-Modified", &*LAST_MODIFIED)
|
||||
.html(|o| templates::notfound_html(o, "some path".into()))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::notfound_html(&mut result, "some path".into())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
pub mod blog;
|
||||
pub mod feeds;
|
||||
pub mod gallery;
|
||||
pub mod talks;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
struct PostNotFound(String, String);
|
||||
pub enum Error {
|
||||
#[error("series not found: {0}")]
|
||||
SeriesNotFound(String),
|
||||
|
||||
impl fmt::Display for PostNotFound {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "not found: {}/{}", self.0, self.1)
|
||||
}
|
||||
#[error("post not found: {0}")]
|
||||
PostNotFound(String),
|
||||
|
||||
#[error("patreon key not working, poke me to get this fixed")]
|
||||
NoPatrons,
|
||||
|
||||
#[error("io error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
|
||||
#[error("axum http error: {0}")]
|
||||
AxumHTTP(#[from] axum::http::Error),
|
||||
}
|
||||
|
||||
impl warp::reject::Reject for PostNotFound {}
|
||||
pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
struct SeriesNotFound(String);
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::error_html(&mut result, format!("{}", self)).unwrap();
|
||||
|
||||
impl fmt::Display for SeriesNotFound {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
let body = body::boxed(body::Full::from(result));
|
||||
|
||||
impl warp::reject::Reject for SeriesNotFound {}
|
||||
|
||||
lazy_static! {
|
||||
static ref REJECTION_COUNTER: IntCounterVec = register_int_counter_vec!(
|
||||
opts!("rejections", "Number of rejections by kind"),
|
||||
&["kind"]
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn rejection(err: Rejection) -> Result<impl Reply, Infallible> {
|
||||
let path: String;
|
||||
let code;
|
||||
|
||||
if err.is_not_found() {
|
||||
REJECTION_COUNTER.with_label_values(&["404"]).inc();
|
||||
path = "".into();
|
||||
code = StatusCode::NOT_FOUND;
|
||||
} else if let Some(SeriesNotFound(series)) = err.find() {
|
||||
REJECTION_COUNTER
|
||||
.with_label_values(&["SeriesNotFound"])
|
||||
.inc();
|
||||
log::error!("invalid series {}", series);
|
||||
path = format!("/blog/series/{}", series);
|
||||
code = StatusCode::NOT_FOUND;
|
||||
} else if let Some(PostNotFound(kind, name)) = err.find() {
|
||||
REJECTION_COUNTER.with_label_values(&["PostNotFound"]).inc();
|
||||
log::error!("unknown post {}/{}", kind, name);
|
||||
path = format!("/{}/{}", kind, name);
|
||||
code = StatusCode::NOT_FOUND;
|
||||
} else {
|
||||
REJECTION_COUNTER.with_label_values(&["Other"]).inc();
|
||||
log::error!("unhandled rejection: {:?}", err);
|
||||
path = format!("weird rejection: {:?}", err);
|
||||
code = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
Ok(warp::reply::with_status(
|
||||
Response::builder()
|
||||
.html(|o| templates::notfound_html(o, path))
|
||||
.unwrap(),
|
||||
code,
|
||||
))
|
||||
.status(match self {
|
||||
Error::SeriesNotFound(_) | Error::PostNotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})
|
||||
.body(body)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
use super::PostNotFound;
|
||||
use crate::{
|
||||
app::State,
|
||||
post::Post,
|
||||
templates::{self, Html, RenderRucte},
|
||||
use super::{Error::*, Result};
|
||||
use crate::{app::State, post::Post, templates};
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
response::Html,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use warp::{http::Response, Rejection, Reply};
|
||||
|
||||
lazy_static! {
|
||||
static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!(
|
||||
|
@ -19,13 +18,18 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
let state = state.clone();
|
||||
Response::builder().html(|o| templates::talkindex_html(o, state.talks.clone()))
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::talkindex_html(&mut result, state.talks.clone())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> {
|
||||
pub async fn post_view(
|
||||
Path(name): Path<String>,
|
||||
Extension(state): Extension<Arc<State>>,
|
||||
) -> Result {
|
||||
let mut want: Option<Post> = None;
|
||||
|
||||
for post in &state.talks {
|
||||
|
@ -35,13 +39,15 @@ pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Re
|
|||
}
|
||||
|
||||
match want {
|
||||
None => Err(PostNotFound("talks".into(), name).into()),
|
||||
None => Err(PostNotFound(name).into()),
|
||||
Some(post) => {
|
||||
HIT_COUNTER
|
||||
.with_label_values(&[name.clone().as_str()])
|
||||
.inc();
|
||||
let body = Html(post.body_html.clone());
|
||||
Response::builder().html(|o| templates::talkpost_html(o, post, body))
|
||||
let body = templates::Html(post.body_html.clone());
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::talkpost_html(&mut result, post, body)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
407
src/main.rs
407
src/main.rs
|
@ -1,29 +1,64 @@
|
|||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use axum::{
|
||||
body,
|
||||
extract::Extension,
|
||||
http::header::{self, HeaderValue, CACHE_CONTROL, CONTENT_TYPE},
|
||||
response::{Html, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use hyper::{header::CONTENT_TYPE, Body, Response};
|
||||
use hyper::StatusCode;
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
env,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::net::UnixListener;
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use warp::{path, Filter};
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
set_header::SetResponseHeaderLayer,
|
||||
trace::TraceLayer,
|
||||
};
|
||||
|
||||
pub mod app;
|
||||
pub mod handlers;
|
||||
pub mod post;
|
||||
pub mod signalboost;
|
||||
|
||||
use app::State;
|
||||
mod domainsocket;
|
||||
use domainsocket::*;
|
||||
|
||||
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())
|
||||
async fn healthcheck() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
fn cache_header(_: &Response) -> Option<header::HeaderValue> {
|
||||
Some(header::HeaderValue::from_static(
|
||||
"public, max-age=3600, stale-if-error=60",
|
||||
))
|
||||
}
|
||||
|
||||
fn webmention_header(_: &Response) -> Option<HeaderValue> {
|
||||
Some(header::HeaderValue::from_static(
|
||||
r#"<https://mi.within.website/api/webmention/accept>; rel="webmention""#,
|
||||
))
|
||||
}
|
||||
|
||||
fn clacks_header(_: &Response) -> Option<HeaderValue> {
|
||||
Some(HeaderValue::from_static("Ashlynn"))
|
||||
}
|
||||
|
||||
fn hacker_header(_: &Response) -> Option<HeaderValue> {
|
||||
Some(header::HeaderValue::from_static(
|
||||
"If you are reading this, check out /signalboost to find people for your team",
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -35,7 +70,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
let state = Arc::new(
|
||||
app::init(
|
||||
std::env::var("CONFIG_FNAME")
|
||||
env::var("CONFIG_FNAME")
|
||||
.unwrap_or("./config.dhall".into())
|
||||
.as_str()
|
||||
.into(),
|
||||
|
@ -43,178 +78,129 @@ async fn main() -> Result<()> {
|
|||
.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 middleware = tower::ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(Extension(state.clone()))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
header::CACHE_CONTROL,
|
||||
cache_header,
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::appending(
|
||||
header::LINK,
|
||||
webmention_header,
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::appending(
|
||||
header::HeaderName::from_static("x-clacks-overhead"),
|
||||
clacks_header,
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
header::HeaderName::from_static("x-hacker"),
|
||||
hacker_header,
|
||||
));
|
||||
|
||||
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 app = Router::new()
|
||||
// meta
|
||||
.route("/.within/health", get(healthcheck))
|
||||
.route(
|
||||
"/.within/website.within.xesite/new_post",
|
||||
get(handlers::feeds::new_post),
|
||||
)
|
||||
});
|
||||
|
||||
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);
|
||||
.route("/jsonfeed", get(go_vanity))
|
||||
.route("/metrics", get(metrics))
|
||||
.route(
|
||||
"/sw.js",
|
||||
axum::routing::get_service(ServeFile::new("./static/js/sw.js")).handle_error(
|
||||
|err: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("unhandled internal server error: {}", err),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/.well-known/assetlinks.json",
|
||||
axum::routing::get_service(ServeFile::new("./static/assetlinks.json")).handle_error(
|
||||
|err: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("unhandled internal server error: {}", err),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/robots.txt",
|
||||
axum::routing::get_service(ServeFile::new("./static/robots.txt")).handle_error(
|
||||
|err: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("unhandled internal server error: {}", err),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/favicon.ico",
|
||||
axum::routing::get_service(ServeFile::new("./static/favicon/favicon.ico"))
|
||||
.handle_error(|err: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("unhandled internal server error: {}", err),
|
||||
)
|
||||
}),
|
||||
)
|
||||
// static pages
|
||||
.route("/", get(handlers::index))
|
||||
.route("/contact", get(handlers::contact))
|
||||
.route("/feeds", get(handlers::feeds))
|
||||
.route("/resume", get(handlers::resume))
|
||||
.route("/patrons", get(handlers::patrons))
|
||||
.route("/signalboost", get(handlers::signalboost))
|
||||
// feeds
|
||||
.route("/blog.json", get(handlers::feeds::jsonfeed))
|
||||
.route("/blog.atom", get(handlers::feeds::atom))
|
||||
.route("/blog.rss", get(handlers::feeds::rss))
|
||||
// blog
|
||||
.route("/blog", get(handlers::blog::index))
|
||||
.route("/blog/", get(handlers::blog::index))
|
||||
.route("/blog/:name", get(handlers::blog::post_view))
|
||||
.route("/blog/series", get(handlers::blog::series))
|
||||
.route("/blog/series/:series", get(handlers::blog::series_view))
|
||||
// gallery
|
||||
.route("/gallery", get(handlers::gallery::index))
|
||||
.route("/gallery/", get(handlers::gallery::index))
|
||||
.route("/gallery/:name", get(handlers::gallery::post_view))
|
||||
// talks
|
||||
.route("/talks", get(handlers::talks::index))
|
||||
.route("/talks/", get(handlers::talks::index))
|
||||
.route("/talks/:name" |