1use std::sync::LazyLock;
2
3use super::{Claim, SiteConfig, db_utils::u32_to_ivec, fmt::md2html};
4use crate::{DB, controller::filters, error::AppError};
5use askama::Template;
6use axum::{
7 Form,
8 extract::{FromRequest, Request, rejection::FormRejection},
9 http::{HeaderMap, HeaderValue, Uri},
10 response::{Html, IntoResponse, Redirect, Response},
11};
12use axum_extra::{
13 TypedHeader,
14 headers::{Cookie, Referer},
15};
16use http::{HeaderName, StatusCode};
17use serde::de::DeserializeOwned;
18use tracing::error;
19use validator::Validate;
20
21#[derive(Template)]
22#[template(path = "error.html", escape = "none")]
23struct PageError<'a> {
24 page_data: PageData<'a>,
25 status: String,
26 error: String,
27}
28
29impl IntoResponse for AppError {
30 fn into_response(self) -> Response {
31 let status = match self {
32 AppError::CaptchaError
33 | AppError::NameExists
34 | AppError::InnCreateLimit
35 | AppError::NameInvalid
36 | AppError::WrongPassword
37 | AppError::ImageError(_)
38 | AppError::LockedOrHidden
39 | AppError::ReadOnly
40 | AppError::ValidationError(_)
41 | AppError::NoJoinedInn
42 | AppError::Custom(_)
43 | AppError::AxumFormRejection(_) => StatusCode::BAD_REQUEST,
44 AppError::NotFound => StatusCode::NOT_FOUND,
45 AppError::WriteInterval => StatusCode::TOO_MANY_REQUESTS,
46 AppError::NonLogin => return Redirect::to("/signin").into_response(),
47 AppError::Unauthorized => StatusCode::UNAUTHORIZED,
48 AppError::Banned => StatusCode::FORBIDDEN,
49 _ => StatusCode::INTERNAL_SERVER_ERROR,
50 };
51
52 error!("{}, {}", status, self);
53 let site_config = SiteConfig::get(&DB).unwrap_or_default();
54 let page_data = PageData::new("Error", &site_config, None, false);
55 let page_error = PageError {
56 page_data,
57 status: status.to_string(),
58 error: self.to_string(),
59 };
60
61 into_response(&page_error)
62 }
63}
64
65pub(crate) async fn handler_404(uri: Uri) -> impl IntoResponse {
66 error!("No route for {}", uri);
67 AppError::NotFound
68}
69
70pub(crate) async fn home(
71 cookie: Option<TypedHeader<Cookie>>,
72) -> Result<impl IntoResponse, AppError> {
73 let site_config = SiteConfig::get(&DB)?;
74 let claim = cookie.and_then(|cookie| Claim::get(&DB, &cookie, &site_config));
75 let mut home_page_code = site_config.home_page;
76
77 if let Some(claim) = claim {
78 if let Some(home_page) = DB.open_tree("home_pages")?.get(u32_to_ivec(claim.uid))? {
79 if let Some(code) = home_page.first() {
80 home_page_code = *code;
81 if home_page_code == 1 {
82 return Ok(Redirect::to(&format!("/feed/{}", claim.uid)));
83 }
84 };
85 }
86 }
87
88 let redirect = match home_page_code {
89 0 => "/inn/0",
90 2 => "/inn/0?filter=joined",
91 3 => "/inn/0?filter=following",
92 4 => "/solo/user/0",
93 5 => "/solo/user/0?filter=Following",
94 6 => "/inn/list",
95 _ => "/inn/0",
96 };
97
98 Ok(Redirect::to(redirect))
99}
100
101static CSS: LazyLock<String> = LazyLock::new(|| {
102 let mut css = include_str!("../../static/css/bulma.min.css").to_string();
104 css.push('\n');
105 css.push_str(include_str!("../../static/css/bulma-list.css"));
106 css.push('\n');
107 css.push_str(include_str!("../../static/css/main.css"));
108 css
109});
110
111pub(crate) async fn style() -> (HeaderMap, &'static str) {
112 let mut headers = HeaderMap::new();
113
114 headers.insert(
115 HeaderName::from_static("content-type"),
116 HeaderValue::from_static("text/css"),
117 );
118 headers.insert(
119 HeaderName::from_static("cache-control"),
120 HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
121 );
122
123 (headers, &CSS)
124}
125
126pub(crate) async fn favicon() -> (HeaderMap, &'static str) {
127 let mut headers = HeaderMap::new();
128
129 headers.insert(
130 HeaderName::from_static("content-type"),
131 HeaderValue::from_static("image/svg+xml"),
132 );
133 headers.insert(
134 HeaderName::from_static("cache-control"),
135 HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
136 );
137
138 let favicon = include_str!("../../static/favicon.svg");
139
140 (headers, favicon)
141}
142
143pub(crate) async fn encryption_js() -> (HeaderMap, &'static str) {
144 let mut headers = HeaderMap::new();
145 headers.insert(
146 HeaderName::from_static("content-type"),
147 HeaderValue::from_static("text/javascript"),
148 );
149 headers.insert(
150 HeaderName::from_static("cache-control"),
151 HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
152 );
153 let js = include_str!("../../static/js/encryption-helper.js");
154
155 (headers, js)
156}
157
158pub(crate) async fn encoding_js() -> (HeaderMap, &'static str) {
159 let mut headers = HeaderMap::new();
160 headers.insert(
161 HeaderName::from_static("content-type"),
162 HeaderValue::from_static("text/javascript"),
163 );
164 headers.insert(
165 HeaderName::from_static("cache-control"),
166 HeaderValue::from_static("public, max-age=1209600, s-maxage=86400"),
167 );
168 let js = include_str!("../../static/js/encoding-helper.js");
169
170 (headers, js)
171}
172
173pub(crate) async fn robots() -> &'static str {
174 include_str!("../../static/robots.txt")
175}
176
177pub(super) struct PageData<'a> {
178 pub(super) title: &'a str,
179 pub(super) site_name: &'a str,
180 pub(super) site_description: String,
181 pub(super) claim: Option<Claim>,
182 pub(super) has_unread: bool,
183 pub(super) lang: String,
184}
185
186impl<'a> PageData<'a> {
187 pub(super) fn new(
188 title: &'a str,
189 site_config: &'a SiteConfig,
190 claim: Option<Claim>,
191 has_unread: bool,
192 ) -> Self {
193 let site_description = md2html(&site_config.description);
194 let lang = claim
195 .as_ref()
196 .and_then(|claim| claim.lang.as_ref())
197 .map_or_else(|| site_config.lang.clone(), |lang| lang.to_owned());
198
199 Self {
200 title,
201 site_name: &site_config.site_name,
202 site_description,
203 claim,
204 has_unread,
205 lang,
206 }
207 }
208}
209
210pub(super) fn get_referer(header: Option<TypedHeader<Referer>>) -> Option<String> {
212 if let Some(TypedHeader(r)) = header {
213 let referer = format!("{r:?}");
214 let trimmed = referer
215 .trim_start_matches("Referer(\"")
216 .trim_end_matches("\")");
217 Some(trimmed.to_owned())
218 } else {
219 None
220 }
221}
222
223pub(super) struct ParamsPage {
224 pub(super) anchor: usize,
225 pub(super) n: usize,
226 pub(super) is_desc: bool,
227}
228
229#[derive(Debug, Clone, Copy, Default)]
230pub(crate) struct ValidatedForm<T>(pub T);
231
232impl<T, S> FromRequest<S> for ValidatedForm<T>
233where
234 T: DeserializeOwned + Validate,
235 S: Send + Sync,
236 Form<T>: FromRequest<S, Rejection = FormRejection>,
237{
238 type Rejection = AppError;
239
240 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
241 let Form(value) = Form::<T>::from_request(req, state).await?;
242 value.validate()?;
243 Ok(ValidatedForm(value))
244 }
245}
246
247pub(crate) fn into_response<T: ?Sized + askama::Template>(tmpl: &T) -> Response {
249 match tmpl.render() {
250 Ok(body) => Html(body).into_response(),
251 Err(e) => e.to_string().into_response(),
252 }
253}