diff options
Diffstat (limited to 'src/frontend/mod.rs')
-rw-r--r-- | src/frontend/mod.rs | 300 |
1 files changed, 199 insertions, 101 deletions
diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index c92619b..eefc257 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,17 +1,17 @@ -use serde::{Serialize, Deserialize}; -use tide::{Request, Response, Result, StatusCode, Next}; -use log::{info,error}; -use crate::ApplicationState; use crate::database::Storage; +use crate::ApplicationState; +use log::{error, info}; +use serde::{Deserialize, Serialize}; +use tide::{Next, Request, Response, Result, StatusCode}; static POSTS_PER_PAGE: usize = 20; mod templates { - use log::error; - use http_types::StatusCode; - use ellipse::Ellipse; - use chrono; use super::IndiewebEndpoints; + use chrono; + use ellipse::Ellipse; + use http_types::StatusCode; + use log::error; /// Return a pretty location specifier from a geo: URI. fn decode_geo_uri(uri: &str) -> String { @@ -21,12 +21,12 @@ mod templates { let lat = parts.next().unwrap(); let lon = parts.next().unwrap(); // TODO - format them as proper latitude and longitude - return format!("{}, {}", lat, lon) + return format!("{}, {}", lat, lon); } else { - return uri.to_string() + return uri.to_string(); } } else { - return uri.to_string() + return uri.to_string(); } } @@ -124,7 +124,7 @@ mod templates { div.form_group { label[for="hcard_name"] { "Your name" } input#hcard_name[name="hcard_name", placeholder="Your name"]; - small { + small { "No need to write the name as in your passport, this is not a legal document " "- just write how you want to be called on the network. This name will be also " "shown whenever you leave a comment on someone else's post using your website." @@ -165,7 +165,7 @@ mod templates { small { "A little bit of introduction. Just one paragraph, and note, you can't use HTML here (yet)." } // TODO: HTML e-note instead of p-note } - + // TODO: u-photo upload - needs media endpoint cooperation div.switch_card_buttons { @@ -438,7 +438,7 @@ mod templates { @if card["properties"]["photo"][0].is_string() { img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()]; } - h1 { + h1 { a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] { @card["properties"]["name"][0].as_str().unwrap() } @@ -508,7 +508,7 @@ mod templates { } } @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE { - a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string() + a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string() + "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] { "Older posts" } @@ -521,8 +521,8 @@ mod templates { #dynamicstuff { p { "This section will provide interesting statistics or tidbits about my life in this exact moment (with maybe a small delay)." } p { "It will probably require JavaScript to self-update, but I promise to keep this widget lightweight and open-source!" } - p { small { - "JavaScript isn't a menace, stop fearing it or I will switch to WebAssembly " + p { small { + "JavaScript isn't a menace, stop fearing it or I will switch to WebAssembly " "and knock your nico-nico-kneecaps so fast with its speed you won't even notice that... " small { "omae ha mou shindeiru" } // NANI?!!! @@ -557,7 +557,7 @@ mod templates { StatusCode::ImATeapot => { p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" } - p { + p { small { "I could brew you some coffee tho if we meet one day... " small { i { "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" } } } } @@ -565,51 +565,61 @@ mod templates { _ => { p { "It seems like you have found an error. Not to worry, it has already been logged." } } } P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" } - + } } } -use templates::{Template,ErrorPage,MainPage,OnboardingPage}; +use templates::{ErrorPage, MainPage, OnboardingPage, Template}; #[derive(Clone, Serialize, Deserialize)] pub struct IndiewebEndpoints { authorization_endpoint: String, token_endpoint: String, webmention: Option<String>, - microsub: Option<String> + microsub: Option<String>, } #[derive(Deserialize)] struct QueryParams { - after: Option<String> + after: Option<String>, } #[derive(Debug)] struct FrontendError { msg: String, source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>, - code: StatusCode + code: StatusCode, } impl FrontendError { pub fn with_code(code: StatusCode, msg: &str) -> Self { - Self { msg: msg.to_string(), source: None, code } + Self { + msg: msg.to_string(), + source: None, + code, + } + } + pub fn msg(&self) -> &str { + &self.msg + } + pub fn code(&self) -> StatusCode { + self.code } - pub fn msg(&self) -> &str { &self.msg } - pub fn code(&self) -> StatusCode { self.code } } impl From<crate::database::StorageError> for FrontendError { fn from(err: crate::database::StorageError) -> Self { Self { msg: "Database error".to_string(), source: Some(Box::new(err)), - code: StatusCode::InternalServerError + code: StatusCode::InternalServerError, } } } 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)) + self.source + .as_ref() + .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)) } } impl std::fmt::Display for FrontendError { @@ -618,30 +628,47 @@ impl std::fmt::Display for FrontendError { } } -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 { +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::NotFound, "Post not found in the database")) + None => Err(FrontendError::with_code( + StatusCode::NotFound, + "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")) + 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(FrontendError::with_code( + StatusCode::Unauthorized, + "User needs to authenticate themselves", + )) } } - _ => Err(err.into()) - } + _ => Err(err.into()), + }, } } #[derive(Deserialize)] struct OnboardingFeed { slug: String, - name: String + name: String, } #[derive(Deserialize)] @@ -649,7 +676,7 @@ struct OnboardingData { user: serde_json::Value, first_post: serde_json::Value, blog_name: String, - feeds: Vec<OnboardingFeed> + feeds: Vec<OnboardingFeed>, } pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result { @@ -663,16 +690,24 @@ pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S let me = url::Url::parse("http://localhost:8080/").unwrap(); if let Ok(_) = get_post_from_database(backend, me.as_str(), None, &None).await { - Err(FrontendError::with_code(StatusCode::Forbidden, "Onboarding is over. Are you trying to take over somebody's website?!"))? + Err(FrontendError::with_code( + StatusCode::Forbidden, + "Onboarding is over. Are you trying to take over somebody's website?!", + ))? } info!("Onboarding new user: {}", me); let user = crate::indieauth::User::new(me.as_str(), "https://kittybox.fireburn.ru/", "create"); - backend.set_setting("site_name", user.me.as_str(), &body.blog_name).await?; + backend + .set_setting("site_name", user.me.as_str(), &body.blog_name) + .await?; if body.user["type"][0] != "h-card" || body.first_post["type"][0] != "h-entry" { - Err(FrontendError::with_code(StatusCode::BadRequest, "user and first_post should be h-card and h-entry"))? + Err(FrontendError::with_code( + StatusCode::BadRequest, + "user and first_post should be h-card and h-entry", + ))? } info!("Validated body.user and body.first_post as microformats2"); @@ -680,7 +715,7 @@ pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S let hentry = body.first_post; // Ensure the h-card's UID is set to the main page, so it will be fetchable. - hcard["properties"]["uid"] = json!([ me.as_str() ]); + hcard["properties"]["uid"] = json!([me.as_str()]); // Normalize the h-card - note that it should preserve the UID we set here. let (_, hcard) = crate::micropub::normalize_mf2(hcard, &user); // The h-card is written directly - all the stuff in the Micropub's @@ -690,10 +725,13 @@ pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S backend.put_post(&hcard).await?; for feed in body.feeds { - let (_, feed) = crate::micropub::normalize_mf2(json!({ - "type": ["h-feed"], - "properties": {"name": [feed.name], "mp-slug": [feed.slug]} - }), &user); + let (_, feed) = crate::micropub::normalize_mf2( + json!({ + "type": ["h-feed"], + "properties": {"name": [feed.name], "mp-slug": [feed.slug]} + }), + &user, + ); backend.put_post(&feed).await?; } @@ -707,8 +745,11 @@ pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S } pub async fn coffee<S: Storage>(_: Request<ApplicationState<S>>) -> Result { - Err(FrontendError::with_code(StatusCode::ImATeapot, "Someone asked this website to brew them some coffee..."))?; - return Ok(Response::builder(500).build()) // unreachable + Err(FrontendError::with_code( + StatusCode::ImATeapot, + "Someone asked this website to brew them some coffee...", + ))?; + return Ok(Response::builder(500).build()); // unreachable } pub async fn mainpage<S: Storage>(req: Request<ApplicationState<S>>) -> Result { @@ -726,7 +767,7 @@ pub async fn mainpage<S: Storage>(req: Request<ApplicationState<S>>) -> Result { info!("Request at {}", url); let hcard_url = url.as_str(); let feed_url = url.join("feeds/main").unwrap().to_string(); - + let card = get_post_from_database(backend, hcard_url, None, &user).await; let feed = get_post_from_database(backend, &feed_url, query.after, &user).await; @@ -737,34 +778,51 @@ pub async fn mainpage<S: Storage>(req: Request<ApplicationState<S>>) -> Result { let feed_err = feed.unwrap_err(); if card_err.code == 404 { // Yes, we definitely need some onboarding here. - Ok(Response::builder(200).content_type("text/html; charset=utf-8").body(Template { - title: "Kittybox - Onboarding", - blog_name: "Kitty Box!", - endpoints: IndiewebEndpoints { - authorization_endpoint, token_endpoint, - webmention: None, microsub: None - }, - content: OnboardingPage {}.to_string() - }.to_string()).build()) + Ok(Response::builder(200) + .content_type("text/html; charset=utf-8") + .body( + Template { + title: "Kittybox - Onboarding", + blog_name: "Kitty Box!", + endpoints: IndiewebEndpoints { + authorization_endpoint, + token_endpoint, + webmention: None, + microsub: None, + }, + content: OnboardingPage {}.to_string(), + } + .to_string(), + ) + .build()) } else { Err(feed_err)? } } else { Ok(Response::builder(200) .content_type("text/html; charset=utf-8") - .body(Template { - title: &format!("{} - Main page", url.host().unwrap().to_string()), - blog_name: &backend.get_setting("site_name", &url.host().unwrap().to_string()).await.unwrap_or_else(|_| "Kitty Box!".to_string()), - endpoints: IndiewebEndpoints { - authorization_endpoint, token_endpoint, - webmention: None, microsub: None - }, - content: MainPage { - feed: &feed?, - card: &card? - }.to_string() - }.to_string() - ).build()) + .body( + Template { + title: &format!("{} - Main page", url.host().unwrap().to_string()), + blog_name: &backend + .get_setting("site_name", &url.host().unwrap().to_string()) + .await + .unwrap_or_else(|_| "Kitty Box!".to_string()), + endpoints: IndiewebEndpoints { + authorization_endpoint, + token_endpoint, + webmention: None, + microsub: None, + }, + content: MainPage { + feed: &feed?, + card: &card?, + } + .to_string(), + } + .to_string(), + ) + .build()) } } @@ -777,41 +835,73 @@ pub async fn render_post<S: Storage>(req: Request<ApplicationState<S>>) -> Resul #[cfg(any(not(debug_assertions), test))] let url = req.url(); #[cfg(all(debug_assertions, not(test)))] - let url = url::Url::parse("http://localhost:8080/").unwrap().join(req.url().path()).unwrap(); - - let post = get_post_from_database(&req.state().storage, url.as_str(), query.after, &user).await?; - - let template: String = match post["type"][0].as_str().expect("Empty type array or invalid type") { + let url = url::Url::parse("http://localhost:8080/") + .unwrap() + .join(req.url().path()) + .unwrap(); + + let post = + get_post_from_database(&req.state().storage, url.as_str(), query.after, &user).await?; + + let template: String = match post["type"][0] + .as_str() + .expect("Empty type array or invalid type") + { "h-entry" => templates::Entry { post: &post }.to_string(), "h-card" => templates::VCard { card: &post }.to_string(), "h-feed" => templates::Feed { feed: &post }.to_string(), - _ => Err(FrontendError::with_code(StatusCode::InternalServerError, "Couldn't render an unknown type"))? + _ => Err(FrontendError::with_code( + StatusCode::InternalServerError, + "Couldn't render an unknown type", + ))?, }; Ok(Response::builder(200) .content_type("text/html; charset=utf-8") - .body(Template { - title: post["properties"]["name"][0].as_str().unwrap_or(&format!("Note at {}", url.host().unwrap().to_string())), - blog_name: &req.state().storage.get_setting("site_name", &url.host().unwrap().to_string()).await.unwrap_or_else(|_| "Kitty Box!".to_string()), - endpoints: IndiewebEndpoints { - authorization_endpoint, token_endpoint, - webmention: None, microsub: None - }, - content: template - }.to_string() - ).build()) + .body( + Template { + title: post["properties"]["name"][0] + .as_str() + .unwrap_or(&format!("Note at {}", url.host().unwrap().to_string())), + blog_name: &req + .state() + .storage + .get_setting("site_name", &url.host().unwrap().to_string()) + .await + .unwrap_or_else(|_| "Kitty Box!".to_string()), + endpoints: IndiewebEndpoints { + authorization_endpoint, + token_endpoint, + webmention: None, + microsub: None, + }, + content: template, + } + .to_string(), + ) + .build()) } pub struct ErrorHandlerMiddleware {} #[async_trait::async_trait] -impl<S> tide::Middleware<ApplicationState<S>> for ErrorHandlerMiddleware where - S: crate::database::Storage +impl<S> tide::Middleware<ApplicationState<S>> for ErrorHandlerMiddleware +where + S: crate::database::Storage, { - async fn handle(&self, request: Request<ApplicationState<S>>, next: Next<'_, ApplicationState<S>>) -> Result { + async fn handle( + &self, + request: Request<ApplicationState<S>>, + next: Next<'_, ApplicationState<S>>, + ) -> Result { let authorization_endpoint = request.state().authorization_endpoint.to_string(); let token_endpoint = request.state().token_endpoint.to_string(); - let site_name = &request.state().storage.get_setting("site_name", &request.url().host().unwrap().to_string()).await.unwrap_or_else(|_| "Kitty Box!".to_string()); + let site_name = &request + .state() + .storage + .get_setting("site_name", &request.url().host().unwrap().to_string()) + .await + .unwrap_or_else(|_| "Kitty Box!".to_string()); let mut res = next.run(request).await; let mut code: Option<StatusCode> = None; if let Some(err) = res.downcast_error::<FrontendError>() { @@ -826,15 +916,20 @@ impl<S> tide::Middleware<ApplicationState<S>> for ErrorHandlerMiddleware where if let Some(code) = code { res.set_status(code); res.set_content_type("text/html; charset=utf-8"); - res.set_body(Template { - title: "Error", - blog_name: site_name, - endpoints: IndiewebEndpoints { - authorization_endpoint, token_endpoint, - webmention: None, microsub: None - }, - content: ErrorPage { code }.to_string() - }.to_string()); + res.set_body( + Template { + title: "Error", + blog_name: site_name, + endpoints: IndiewebEndpoints { + authorization_endpoint, + token_endpoint, + webmention: None, + microsub: None, + }, + content: ErrorPage { code }.to_string(), + } + .to_string(), + ); } Ok(res) } @@ -858,7 +953,10 @@ pub async fn handle_static<S: Storage>(req: Request<ApplicationState<S>>) -> Res .content_type("text/css; charset=utf-8") .body(ONBOARDING_CSS) .build()), - Ok(_) => Err(FrontendError::with_code(StatusCode::NotFound, "Static file not found")), - Err(_) => panic!("Invalid usage of the frontend::handle_static() function") + Ok(_) => Err(FrontendError::with_code( + StatusCode::NotFound, + "Static file not found", + )), + Err(_) => panic!("Invalid usage of the frontend::handle_static() function"), }?) } |