bingus-blog/src/app.rs

205 lines
5.8 KiB
Rust
Raw Normal View History

2024-05-08 23:03:10 +03:00
use std::sync::Arc;
use std::time::Duration;
use askama_axum::Template;
use axum::extract::{Path, Query, State};
use axum::http::header::CONTENT_TYPE;
2024-05-08 23:03:10 +03:00
use axum::http::{header, Request};
use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::get;
use axum::{Json, Router};
use rss::{Category, ChannelBuilder, ItemBuilder};
use serde::Deserialize;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing::{info, info_span, Span};
2024-08-01 00:25:42 +03:00
use crate::config::{Config, DateFormat, Sort};
2024-05-08 23:03:10 +03:00
use crate::error::{AppError, AppResult};
use crate::filters;
use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats, ReturnedPost};
2024-05-08 23:03:10 +03:00
#[derive(Clone)]
pub struct AppState {
pub config: Arc<Config>,
pub posts: Arc<MarkdownPosts<Arc<Config>>>,
2024-05-08 23:03:10 +03:00
}
#[derive(Template)]
#[template(path = "index.html")]
2024-07-01 03:05:48 +03:00
struct IndexTemplate<'a> {
title: &'a str,
description: &'a str,
2024-05-08 23:03:10 +03:00
posts: Vec<PostMetadata>,
2024-06-13 23:19:47 +03:00
rss: bool,
2024-07-01 03:05:48 +03:00
df: &'a DateFormat,
js: bool,
2024-07-01 03:16:17 +03:00
color: Option<&'a str>,
2024-08-01 00:25:42 +03:00
sort: Sort,
2024-05-08 23:03:10 +03:00
}
#[derive(Template)]
#[template(path = "post.html")]
2024-07-01 03:05:48 +03:00
struct PostTemplate<'a> {
meta: &'a PostMetadata,
2024-05-08 23:03:10 +03:00
rendered: String,
rendered_in: RenderStats,
markdown_access: bool,
2024-07-01 03:05:48 +03:00
df: &'a DateFormat,
js: bool,
2024-07-01 03:05:48 +03:00
color: Option<&'a str>,
2024-05-08 23:03:10 +03:00
}
#[derive(Deserialize)]
struct QueryParams {
tag: Option<String>,
#[serde(rename = "n")]
num_posts: Option<usize>,
}
2024-07-01 03:05:48 +03:00
async fn index<'a>(
2024-05-08 23:03:10 +03:00
State(AppState { config, posts }): State<AppState>,
Query(query): Query<QueryParams>,
2024-07-01 03:05:48 +03:00
) -> AppResult<Response> {
2024-05-08 23:03:10 +03:00
let posts = posts
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
.await?;
Ok(IndexTemplate {
2024-07-01 03:05:48 +03:00
title: &config.title,
description: &config.description,
2024-05-08 23:03:10 +03:00
posts,
2024-06-13 23:19:47 +03:00
rss: config.rss.enable,
2024-07-01 03:05:48 +03:00
df: &config.date_format,
js: config.js_enable,
2024-07-01 03:16:17 +03:00
color: config.default_color.as_deref(),
2024-08-01 00:25:42 +03:00
sort: config.default_sort,
2024-07-01 03:05:48 +03:00
}
.into_response())
2024-05-08 23:03:10 +03:00
}
async fn all_posts(
State(AppState { posts, .. }): State<AppState>,
Query(query): Query<QueryParams>,
) -> AppResult<Json<Vec<PostMetadata>>> {
let posts = posts
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
.await?;
Ok(Json(posts))
}
async fn rss(
State(AppState { config, posts }): State<AppState>,
Query(query): Query<QueryParams>,
) -> AppResult<Response> {
if !config.rss.enable {
return Err(AppError::RssDisabled);
}
let posts = posts
.get_all_posts(|metadata, _| {
2024-05-08 23:03:10 +03:00
!query
.tag
.as_ref()
.is_some_and(|tag| !metadata.tags.contains(tag))
})
.await?;
let mut channel = ChannelBuilder::default();
channel
.title(&config.title)
.link(config.rss.link.to_string())
.description(&config.description);
//TODO: .language()
for (metadata, content, _) in posts {
channel.item(
ItemBuilder::default()
.title(metadata.title)
.description(metadata.description)
.author(metadata.author)
.categories(
metadata
.tags
.into_iter()
.map(|tag| Category {
name: tag,
domain: None,
})
.collect::<Vec<Category>>(),
)
.pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
.content(content)
.link(
config
.rss
.link
.join(&format!("/posts/{}", metadata.name))?
.to_string(),
)
.build(),
);
}
let body = channel.build().to_string();
drop(channel);
Ok(([(header::CONTENT_TYPE, "text/xml")], body).into_response())
}
async fn post(
State(AppState { config, posts }): State<AppState>,
Path(name): Path<String>,
) -> AppResult<Response> {
match posts.get_post(&name).await? {
2024-07-01 03:05:48 +03:00
ReturnedPost::Rendered(ref meta, rendered, rendered_in) => Ok(PostTemplate {
meta,
rendered,
rendered_in,
markdown_access: config.markdown_access,
df: &config.date_format,
js: config.js_enable,
color: meta.color.as_deref().or(config.default_color.as_deref()),
}
2024-07-01 03:05:48 +03:00
.into_response()),
ReturnedPost::Raw(body, content_type) => {
Ok(([(CONTENT_TYPE, content_type)], body).into_response())
}
2024-05-08 23:03:10 +03:00
}
}
pub fn new(config: &Config) -> Router<AppState> {
2024-05-08 23:03:10 +03:00
Router::new()
.route("/", get(index))
.route(
"/post/:name",
get(
|Path(name): Path<String>| async move { Redirect::to(&format!("/posts/{}", name)) },
),
)
.route("/posts/:name", get(post))
.route("/posts", get(all_posts))
.route("/feed.xml", get(rss))
.nest_service(
"/static",
ServeDir::new(&config.dirs._static).precompressed_gzip(),
)
.nest_service("/media", ServeDir::new(&config.dirs.media))
2024-05-08 23:03:10 +03:00
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
info_span!(
"request",
method = ?request.method(),
path = ?request.uri().path(),
)
})
.on_response(|response: &Response<_>, duration: Duration, span: &Span| {
let _ = span.enter();
let status = response.status();
info!(?status, ?duration, "response");
}),
)
}