From 0617663b249f9ca488e5de652108b17d67fbaf45 Mon Sep 17 00:00:00 2001 From: Vika Date: Sat, 29 Jul 2023 21:59:56 +0300 Subject: Moved the entire Kittybox tree into the root --- kittybox-rs/src/frontend/mod.rs | 404 ---------------------------------------- 1 file changed, 404 deletions(-) delete mode 100644 kittybox-rs/src/frontend/mod.rs (limited to 'kittybox-rs/src/frontend/mod.rs') diff --git a/kittybox-rs/src/frontend/mod.rs b/kittybox-rs/src/frontend/mod.rs deleted file mode 100644 index 7a43532..0000000 --- a/kittybox-rs/src/frontend/mod.rs +++ /dev/null @@ -1,404 +0,0 @@ -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, -} - -#[derive(Debug)] -struct FrontendError { - msg: String, - source: Option>, - code: StatusCode, -} - -impl FrontendError { - pub fn with_code(code: C, msg: &str) -> Self - where - C: TryInto, - { - 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 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)?; - if let Some(err) = std::error::Error::source(&self) { - write!(f, ": {}", err)?; - } - - Ok(()) - } -} - -/// Filter the post according to the value of `user`. -/// -/// Anonymous users cannot view private posts and protected locations; -/// Logged-in users can only view private posts targeted at them; -/// Logged-in users can't view private location data -#[tracing::instrument(skip(post), fields(post = %post))] -pub fn filter_post( - mut post: serde_json::Value, - user: Option<&str>, -) -> Option { - if post["properties"]["deleted"][0].is_string() { - tracing::debug!("Deleted post; returning tombstone instead"); - return Some(serde_json::json!({ - "type": post["type"], - "properties": { - "deleted": post["properties"]["deleted"] - } - })); - } - let empty_vec: Vec = vec![]; - let author_list = post["properties"]["author"] - .as_array() - .unwrap_or(&empty_vec) - .iter() - .map(|i| -> &str { - match i { - serde_json::Value::String(ref author) => author.as_str(), - mf2 => mf2["properties"]["uid"][0].as_str().unwrap() - } - }).collect::>(); - let visibility = post["properties"]["visibility"][0] - .as_str() - .unwrap_or("public"); - let audience = { - let mut audience = author_list.clone(); - audience.extend(post["properties"]["audience"] - .as_array() - .unwrap_or(&empty_vec) - .iter() - .map(|i| i.as_str().unwrap())); - - audience - }; - tracing::debug!("post audience = {:?}", audience); - if (visibility == "private" && !audience.iter().any(|i| Some(*i) == user)) - || (visibility == "protected" && user.is_none()) - { - return None; - } - if post["properties"]["location"].is_array() { - let location_visibility = post["properties"]["location-visibility"][0] - .as_str() - .unwrap_or("private"); - tracing::debug!("Post contains location, location privacy = {}", location_visibility); - let mut author = post["properties"]["author"] - .as_array() - .unwrap_or(&empty_vec) - .iter() - .map(|i| i.as_str().unwrap()); - if (location_visibility == "private" && !author.any(|i| Some(i) == user)) - || (location_visibility == "protected" && user.is_none()) - { - post["properties"] - .as_object_mut() - .unwrap() - .remove("location"); - } - } - - match post["properties"]["author"].take() { - serde_json::Value::Array(children) => { - post["properties"]["author"] = serde_json::Value::Array( - children - .into_iter() - .filter_map(|post| if post.is_string() { - Some(post) - } else { - filter_post(post, user) - }) - .collect::>() - ); - }, - serde_json::Value::Null => {}, - other => post["properties"]["author"] = other - } - - match post["children"].take() { - serde_json::Value::Array(children) => { - post["children"] = serde_json::Value::Array( - children - .into_iter() - .filter_map(|post| filter_post(post, user)) - .collect::>() - ); - }, - serde_json::Value::Null => {}, - other => post["children"] = other - } - Some(post) -} - -async fn get_post_from_database( - db: &S, - url: &str, - after: Option, - user: &Option, -) -> std::result::Result<(serde_json::Value, Option), FrontendError> { - match db - .read_feed_with_cursor(url, after.as_deref(), POSTS_PER_PAGE, user.as_deref()) - .await - { - Ok(result) => match result { - Some((post, cursor)) => match filter_post(post, user.as_deref()) { - Some(post) => Ok((post, cursor)), - None => { - // 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", - )) - } - } - } - 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( - Host(host): Host, - Query(query): Query, - Extension(db): Extension, -) -> 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, cursor))) => { - // 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, webring, channels) = tokio::join!( - db.get_setting::(&host) - .map(Result::unwrap_or_default), - - db.get_setting::(&host) - .map(Result::unwrap_or_default), - - 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.as_ref(), - blog_name: blogname.as_ref(), - feeds: channels, - user, - content: MainPage { - feed: &hfeed, - card: &hcard, - cursor: cursor.as_deref(), - webring: crate::database::settings::Setting::into_inner(webring) - } - .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::(&host) - .map(Result::unwrap_or_default), - - 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.as_ref(), - blog_name: blogname.as_ref(), - 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( - Extension(db): Extension, - Host(host): Host, - Query(query): Query, - 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, cursor)) => { - let (blogname, channels) = tokio::join!( - db.get_setting::(&host) - .map(Result::unwrap_or_default), - - 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.as_ref(), - blog_name: blogname.as_ref(), - 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, cursor: cursor.as_deref() }.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::(&host) - .map(Result::unwrap_or_default), - - 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.as_ref(), - blog_name: blogname.as_ref(), - feeds: channels, - user, - content: ErrorPage { - code: err.code(), - msg: Some(err.msg().to_owned()), - } - .to_string(), - } - .to_string(), - ) - } - } -} -- cgit 1.4.1