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};
|
2024-05-14 12:26:43 +03:00
|
|
|
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;
|
2024-05-14 12:26:43 +03:00
|
|
|
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>,
|
2024-05-14 10:11:41 +03:00
|
|
|
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,
|
2024-06-13 21:52:18 +03:00
|
|
|
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,
|
2024-06-13 21:52:18 +03:00
|
|
|
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,
|
2024-06-13 21:52:18 +03:00
|
|
|
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
|
2024-05-14 10:11:41 +03:00
|
|
|
.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> {
|
2024-05-14 12:26:43 +03:00
|
|
|
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-05-14 12:26:43 +03:00
|
|
|
}
|
2024-07-01 03:05:48 +03:00
|
|
|
.into_response()),
|
2024-05-14 12:26:43 +03:00
|
|
|
ReturnedPost::Raw(body, content_type) => {
|
|
|
|
Ok(([(CONTENT_TYPE, content_type)], body).into_response())
|
|
|
|
}
|
2024-05-08 23:03:10 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-14 10:11:41 +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))
|
2024-05-14 10:11:41 +03:00
|
|
|
.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");
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|