use crate::database::{Storage, StorageError};
use axum::{
extract::{Host, Path, Query},
http::{StatusCode, Uri},
response::IntoResponse,
Extension,
};
use futures_util::FutureExt;
use serde::Deserialize;
use std::convert::TryInto;
use tracing::{debug, error};
//pub mod login;
pub mod onboarding;
use kittybox_frontend_renderer::{
Entry, Feed, VCard,
ErrorPage, Template, MainPage,
POSTS_PER_PAGE
};
pub use kittybox_frontend_renderer::assets::statics;
#[derive(Debug, Deserialize)]
pub struct QueryParams {
after: Option<String>,
}
#[derive(Debug)]
struct FrontendError {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
code: StatusCode,
}
impl FrontendError {
pub fn with_code<C>(code: C, msg: &str) -> Self
where
C: TryInto<StatusCode>,
{
Self {
msg: msg.to_string(),
source: None,
code: code.try_into().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub fn msg(&self) -> &str {
&self.msg
}
pub fn code(&self) -> StatusCode {
self.code
}
}
impl From<StorageError> for FrontendError {
fn from(err: StorageError) -> Self {
Self {
msg: "Database error".to_string(),
source: Some(Box::new(err)),
code: StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl std::error::Error for FrontendError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
}
}
impl std::fmt::Display for FrontendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg)
}
}
async fn get_post_from_database<S: Storage>(
db: &S,
url: &str,
after: Option<String>,
user: &Option<String>,
) -> std::result::Result<serde_json::Value, FrontendError> {
match db
.read_feed_with_limit(url, &after, POSTS_PER_PAGE, user)
.await
{
Ok(result) => match result {
Some(post) => Ok(post),
None => Err(FrontendError::with_code(
StatusCode::NOT_FOUND,
"Post not found in the database",
)),
},
Err(err) => match err.kind() {
crate::database::ErrorKind::PermissionDenied => {
// TODO: Authentication
if user.is_some() {
Err(FrontendError::with_code(
StatusCode::FORBIDDEN,
"User authenticated AND forbidden to access this resource",
))
} else {
Err(FrontendError::with_code(
StatusCode::UNAUTHORIZED,
"User needs to authenticate themselves",
))
}
}
_ => Err(err.into()),
},
}
}
#[tracing::instrument(skip(db))]
pub async fn homepage<D: Storage>(
Host(host): Host,
Query(query): Query<QueryParams>,
Extension(db): Extension<D>,
) -> impl IntoResponse {
let user = None; // TODO authentication
let path = format!("https://{}/", host);
let feed_path = format!("https://{}/feeds/main", host);
match tokio::try_join!(
get_post_from_database(&db, &path, None, &user),
get_post_from_database(&db, &feed_path, query.after, &user)
) {
Ok((hcard, hfeed)) => {
// Here, we know those operations can't really fail
// (or it'll be a transient failure that will show up on
// other requests anyway if it's serious...)
//
// btw is it more efficient to fetch these in parallel?
let (blogname, channels) = tokio::join!(
db.get_setting(crate::database::Settings::SiteName, &path)
.map(|i| i.unwrap_or_else(|_| "Kittybox".to_owned())),
db.get_channels(&path).map(|i| i.unwrap_or_default())
);
// Render the homepage
(
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
r#"text/html; charset="utf-8""#,
)],
Template {
title: &blogname,
blog_name: &blogname,
feeds: channels,
user,
content: MainPage {
feed: &hfeed,
card: &hcard,
}
.to_string(),
}
.to_string(),
)
}
Err(err) => {
if err.code == StatusCode::NOT_FOUND {
debug!("Transferring to onboarding...");
// Transfer to onboarding
(
StatusCode::FOUND,
[(axum::http::header::LOCATION, "/.kittybox/onboarding")],
String::default(),
)
} else {
error!("Error while fetching h-card and/or h-feed: {}", err);
// Return the error
let (blogname, channels) = tokio::join!(
db.get_setting(crate::database::Settings::SiteName, &path)
.map(|i| i.unwrap_or_else(|_| "Kittybox".to_owned())),
db.get_channels(&path).map(|i| i.unwrap_or_default())
);
(
err.code(),
[(
axum::http::header::CONTENT_TYPE,
r#"text/html; charset="utf-8""#,
)],
Template {
title: &blogname,
blog_name: &blogname,
feeds: channels,
user,
content: ErrorPage {
code: err.code(),
msg: Some(err.msg().to_string()),
}
.to_string(),
}
.to_string(),
)
}
}
}
}
#[tracing::instrument(skip(db))]
pub async fn catchall<D: Storage>(
Extension(db): Extension<D>,
Host(host): Host,
Query(query): Query<QueryParams>,
uri: Uri,
) -> impl IntoResponse {
let user = None; // TODO authentication
let path = url::Url::parse(&format!("https://{}/", host))
.unwrap()
.join(uri.path())
.unwrap();
match get_post_from_database(&db, path.as_str(), query.after, &user).await {
Ok(post) => {
let (blogname, channels) = tokio::join!(
db.get_setting(crate::database::Settings::SiteName, &host)
.map(|i| i.unwrap_or_else(|_| "Kittybox".to_owned())),
db.get_channels(&host).map(|i| i.unwrap_or_default())
);
// Render the homepage
(
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
r#"text/html; charset="utf-8""#,
)],
Template {
title: &blogname,
blog_name: &blogname,
feeds: channels,
user,
content: match post.pointer("/type/0").and_then(|i| i.as_str()) {
Some("h-entry") => Entry { post: &post }.to_string(),
Some("h-feed") => Feed { feed: &post }.to_string(),
Some("h-card") => VCard { card: &post }.to_string(),
unknown => {
unimplemented!("Template for MF2-JSON type {:?}", unknown)
}
},
}
.to_string(),
)
}
Err(err) => {
let (blogname, channels) = tokio::join!(
db.get_setting(crate::database::Settings::SiteName, &host)
.map(|i| i.unwrap_or_else(|_| "Kittybox".to_owned())),
db.get_channels(&host).map(|i| i.unwrap_or_default())
);
(
err.code(),
[(
axum::http::header::CONTENT_TYPE,
r#"text/html; charset="utf-8""#,
)],
Template {
title: &blogname,
blog_name: &blogname,
feeds: channels,
user,
content: ErrorPage {
code: err.code(),
msg: Some(err.msg().to_owned()),
}
.to_string(),
}
.to_string(),
)
}
}
}