diff options
| author | Christine Dodrill <me@christine.website> | 2020-07-16 15:32:30 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-16 15:32:30 -0400 |
| commit | 385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7 (patch) | |
| tree | af789f7250519b23038a7e5ea0ae7f4f4c1ffdfc /src/handlers | |
| parent | 449e934246c82d90dd0aac2644d67f928befeeb4 (diff) | |
| download | xesite-385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7.tar.xz xesite-385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7.zip | |
Rewrite site backend in Rust (#178)
* add shell.nix changes for Rust #176
* set up base crate layout
* add first set of dependencies
* start adding basic app modules
* start html templates
* serve index page
* add contact and feeds pages
* add resume rendering support
* resume cleanups
* get signalboost page working
* rewrite config to be in dhall
* more work
* basic generic post loading
* more tests
* initial blog index support
* fix routing?
* render blogposts
* X-Clacks-Overhead
* split blog handlers into blog.rs
* gallery index
* gallery posts
* fix hashtags
* remove instantpage (it messes up the metrics)
* talk support + prometheus
* Create rust.yml
* Update rust.yml
* Update codeql-analysis.yml
* add jsonfeed library
* jsonfeed support
* rss/atom
* go mod tidy
* atom: add posted date
* rss: add publishing date
* nix: build rust program
* rip out go code
* rip out go templates
* prepare for serving in docker
* create kubernetes deployment
* create automagic deployment
* build docker images on non-master
* more fixes
* fix timestamps
* fix RSS/Atom/JSONFeed validation errors
* add go vanity import redirecting
* templates/header: remove this
* atom feed: fixes
* fix?
* fix??
* fix rust tests
* Update rust.yml
* automatically show snow during the winter
* fix dates
* show commit link in footer
* sitemap support
* fix compiler warning
* start basic patreon client
* integrate kankyo
* fix patreon client
* add patrons page
* remove this
* handle patron errors better
* fix build
* clean up deploy
* sort envvars for deploy
* remove deps.nix
* shell.nix: remove go
* update README
* fix envvars for tests
* nice
* blog: add rewrite in rust post
* blog/site-update: more words
Diffstat (limited to 'src/handlers')
| -rw-r--r-- | src/handlers/blog.rs | 77 | ||||
| -rw-r--r-- | src/handlers/feeds.rs | 73 | ||||
| -rw-r--r-- | src/handlers/gallery.rs | 40 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 145 | ||||
| -rw-r--r-- | src/handlers/talks.rs | 40 |
5 files changed, 375 insertions, 0 deletions
diff --git a/src/handlers/blog.rs b/src/handlers/blog.rs new file mode 100644 index 0000000..e494e04 --- /dev/null +++ b/src/handlers/blog.rs @@ -0,0 +1,77 @@ +use super::{PostNotFound, SeriesNotFound}; +use crate::{ + app::State, + post::Post, + templates::{self, Html, RenderRucte}, +}; +use lazy_static::lazy_static; +use prometheus::{IntCounterVec, register_int_counter_vec, opts}; +use std::sync::Arc; +use warp::{http::Response, Rejection, Reply}; + +lazy_static! { + static ref HIT_COUNTER: IntCounterVec = + register_int_counter_vec!(opts!("blogpost_hits", "Number of hits to blogposts"), &["name"]) + .unwrap(); +} + +pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> { + let state = state.clone(); + Response::builder().html(|o| templates::blogindex_html(o, state.blog.clone())) +} + +pub async fn series(state: Arc<State>) -> Result<impl Reply, Rejection> { + let state = state.clone(); + let mut series: Vec<String> = vec![]; + + for post in &state.blog { + if post.front_matter.series.is_some() { + series.push(post.front_matter.series.as_ref().unwrap().clone()); + } + } + + series.sort(); + series.dedup(); + + Response::builder().html(|o| templates::series_html(o, series)) +} + +pub async fn series_view(series: String, state: Arc<State>) -> Result<impl Reply, Rejection> { + let state = state.clone(); + let mut posts: Vec<Post> = vec![]; + + for post in &state.blog { + if post.front_matter.series.is_none() { + continue; + } + if post.front_matter.series.as_ref().unwrap() != &series { + continue; + } + posts.push(post.clone()); + } + + if posts.len() == 0 { + Err(SeriesNotFound(series).into()) + } else { + Response::builder().html(|o| templates::series_posts_html(o, series, &posts)) + } +} + +pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> { + let mut want: Option<Post> = None; + + for post in &state.blog { + if post.link == format!("blog/{}", name) { + want = Some(post.clone()); + } + } + + match want { + None => Err(PostNotFound("blog".into(), 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::blogpost_html(o, post, body)) + } + } +} diff --git a/src/handlers/feeds.rs b/src/handlers/feeds.rs new file mode 100644 index 0000000..7d7db32 --- /dev/null +++ b/src/handlers/feeds.rs @@ -0,0 +1,73 @@ +use crate::app::State; +use lazy_static::lazy_static; +use prometheus::{opts, register_int_counter_vec, IntCounterVec}; +use std::sync::Arc; +use warp::{http::Response, Rejection, Reply}; + +lazy_static! { + static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!( + opts!("feed_hits", "Number of hits to various feeds"), + &["kind"] + ) + .unwrap(); +} + +pub async fn jsonfeed(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["json"]).inc(); + let state = state.clone(); + Ok(warp::reply::json(&state.jf)) +} + +#[derive(Debug)] +pub enum RenderError { + WriteAtom(atom_syndication::Error), + WriteRss(rss::Error), + Build(warp::http::Error), +} + +impl warp::reject::Reject for RenderError {} + +pub async fn atom(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["atom"]).inc(); + let state = state.clone(); + let mut buf = Vec::new(); + state + .af + .write_to(&mut buf) + .map_err(RenderError::WriteAtom) + .map_err(warp::reject::custom)?; + Response::builder() + .status(200) + .header("Content-Type", "application/atom+xml") + .body(buf) + .map_err(RenderError::Build) + .map_err(warp::reject::custom) +} + +pub async fn rss(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["rss"]).inc(); + let state = state.clone(); + let mut buf = Vec::new(); + state + .rf + .write_to(&mut buf) + .map_err(RenderError::WriteRss) + .map_err(warp::reject::custom)?; + Response::builder() + .status(200) + .header("Content-Type", "application/rss+xml") + .body(buf) + .map_err(RenderError::Build) + .map_err(warp::reject::custom) +} + +pub async fn sitemap(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["sitemap"]).inc(); + let state = state.clone(); + Response::builder() + .status(200) + .header("Content-Type", "application/xml") + .body(state.sitemap.clone()) + .map_err(RenderError::Build) + .map_err(warp::reject::custom) +} diff --git a/src/handlers/gallery.rs b/src/handlers/gallery.rs new file mode 100644 index 0000000..2094ab2 --- /dev/null +++ b/src/handlers/gallery.rs @@ -0,0 +1,40 @@ +use super::PostNotFound; +use crate::{ + app::State, + post::Post, + templates::{self, Html, RenderRucte}, +}; +use lazy_static::lazy_static; +use prometheus::{IntCounterVec, register_int_counter_vec, opts}; +use std::sync::Arc; +use warp::{http::Response, Rejection, Reply}; + +lazy_static! { + static ref HIT_COUNTER: IntCounterVec = + register_int_counter_vec!(opts!("gallery_hits", "Number of hits to gallery images"), &["name"]) + .unwrap(); +} + +pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> { + let state = state.clone(); + Response::builder().html(|o| templates::galleryindex_html(o, state.gallery.clone())) +} + +pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> { + let mut want: Option<Post> = None; + + for post in &state.gallery { + if post.link == format!("gallery/{}", name) { + want = Some(post.clone()); + } + } + + match want { + None => Err(PostNotFound("gallery".into(), 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::gallerypost_html(o, post, body)) + } + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..5c51352 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,145 @@ +use crate::{ + app::State, + templates::{self, Html, RenderRucte}, +}; +use lazy_static::lazy_static; +use prometheus::{opts, register_int_counter_vec, IntCounterVec}; +use std::{convert::Infallible, fmt, sync::Arc}; +use warp::{ + http::{Response, StatusCode}, + Rejection, Reply, +}; + +lazy_static! { + static ref HIT_COUNTER: IntCounterVec = + register_int_counter_vec!(opts!("hits", "Number of hits to various pages"), &["page"]) + .unwrap(); +} + +pub async fn index() -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["index"]).inc(); + Response::builder().html(|o| templates::index_html(o)) +} + +pub async fn contact() -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["contact"]).inc(); + Response::builder().html(|o| templates::contact_html(o)) +} + +pub async fn feeds() -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["feeds"]).inc(); + Response::builder().html(|o| templates::feeds_html(o)) +} + +pub async fn resume(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["resume"]).inc(); + let state = state.clone(); + Response::builder().html(|o| templates::resume_html(o, Html(state.resume.clone()))) +} + +pub async fn patrons(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["patrons"]).inc(); + let state = state.clone(); + 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().html(|o| templates::patrons_html(o, patrons.clone())), + } +} + +pub async fn signalboost(state: Arc<State>) -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["signalboost"]).inc(); + let state = state.clone(); + Response::builder().html(|o| templates::signalboost_html(o, state.signalboost.clone())) +} + +pub async fn not_found() -> Result<impl Reply, Rejection> { + HIT_COUNTER.with_label_values(&["not_found"]).inc(); + Response::builder().html(|o| templates::notfound_html(o, "some path".into())) +} + +pub mod blog; +pub mod feeds; +pub mod gallery; +pub mod talks; + +#[derive(Debug, thiserror::Error)] +struct PostNotFound(String, String); + +impl fmt::Display for PostNotFound { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "not found: {}/{}", self.0, self.1) + } +} + +impl warp::reject::Reject for PostNotFound {} + +impl From<PostNotFound> for warp::reject::Rejection { + fn from(error: PostNotFound) -> Self { + warp::reject::custom(error) + } +} + +#[derive(Debug, thiserror::Error)] +struct SeriesNotFound(String); + +impl fmt::Display for SeriesNotFound { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl warp::reject::Reject for SeriesNotFound {} + +impl From<SeriesNotFound> for warp::reject::Rejection { + fn from(error: SeriesNotFound) -> Self { + warp::reject::custom(error) + } +} + +lazy_static! { + static ref REJECTION_COUNTER: IntCounterVec = register_int_counter_vec!( + opts!("rejections", "Number of rejections by kind"), + &["kind"] + ) + .unwrap(); +} + +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, + )) +} diff --git a/src/handlers/talks.rs b/src/handlers/talks.rs new file mode 100644 index 0000000..54f1e64 --- /dev/null +++ b/src/handlers/talks.rs @@ -0,0 +1,40 @@ +use super::PostNotFound; +use crate::{ + app::State, + post::Post, + templates::{self, Html, RenderRucte}, +}; +use lazy_static::lazy_static; +use prometheus::{IntCounterVec, register_int_counter_vec, opts}; +use std::sync::Arc; +use warp::{http::Response, Rejection, Reply}; + +lazy_static! { + static ref HIT_COUNTER: IntCounterVec = + register_int_counter_vec!(opts!("talks_hits", "Number of hits to talks images"), &["name"]) + .unwrap(); +} + +pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> { + let state = state.clone(); + Response::builder().html(|o| templates::talkindex_html(o, state.talks.clone())) +} + +pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> { + let mut want: Option<Post> = None; + + for post in &state.talks { + if post.link == format!("talks/{}", name) { + want = Some(post.clone()); + } + } + + match want { + None => Err(PostNotFound("talks".into(), 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)) + } + } +} |
