about summary refs log tree commit diff
path: root/src/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/mod.rs300
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"),
     }?)
 }