about summary refs log tree commit diff
path: root/src/micropub/post.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/micropub/post.rs')
-rw-r--r--src/micropub/post.rs624
1 files changed, 428 insertions, 196 deletions
diff --git a/src/micropub/post.rs b/src/micropub/post.rs
index 6183906..b3fe4ee 100644
--- a/src/micropub/post.rs
+++ b/src/micropub/post.rs
@@ -1,17 +1,17 @@
+use crate::database::Storage;
+use crate::indieauth::User;
+use crate::ApplicationState;
+use chrono::prelude::*;
 use core::iter::Iterator;
-use std::str::FromStr;
-use std::convert::TryInto;
-use log::{warn, error};
 use futures::stream;
 use futures::StreamExt;
-use chrono::prelude::*;
 use http_types::Mime;
+use log::{error, warn};
+use newbase60::num_to_sxg;
+use std::convert::TryInto;
+use std::str::FromStr;
 use tide::prelude::json;
 use tide::{Request, Response, Result};
-use newbase60::num_to_sxg;
-use crate::ApplicationState;
-use crate::database::{Storage};
-use crate::indieauth::User;
 
 static DEFAULT_CHANNEL_PATH: &str = "/feeds/main";
 static DEFAULT_CHANNEL_NAME: &str = "Main feed";
@@ -43,8 +43,9 @@ fn get_folder_from_type(post_type: &str) -> String {
         "h-card" => "vcards/",
         "h-event" => "events/",
         "h-food" => "food/",
-        _ => "posts/"
-    }).to_string()
+        _ => "posts/",
+    })
+    .to_string()
 }
 
 pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde_json::Value) {
@@ -63,34 +64,32 @@ pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde
                 // Do not attempt to recover the information.
                 // Do not pass GO. Do not collect $200.
                 let curtime: DateTime<Local> = Local::now();
-                body["properties"]["published"] = serde_json::Value::Array(vec![
-                    serde_json::Value::String(curtime.to_rfc3339())
-                ]);
+                body["properties"]["published"] =
+                    serde_json::Value::Array(vec![serde_json::Value::String(curtime.to_rfc3339())]);
                 published = chrono::DateTime::from(curtime);
             }
         }
     } else {
         // Set the datetime.
         let curtime: DateTime<Local> = Local::now();
-        body["properties"]["published"] = serde_json::Value::Array(vec![
-            serde_json::Value::String(curtime.to_rfc3339())
-        ]);
+        body["properties"]["published"] =
+            serde_json::Value::Array(vec![serde_json::Value::String(curtime.to_rfc3339())]);
         published = chrono::DateTime::from(curtime);
     }
     match body["properties"]["uid"][0].as_str() {
         None => {
             let uid = serde_json::Value::String(
                 me.join(
-                    &(folder.clone() + &num_to_sxg(published.timestamp_millis().try_into().unwrap()))
-                ).unwrap().to_string());
+                    &(folder.clone()
+                        + &num_to_sxg(published.timestamp_millis().try_into().unwrap())),
+                )
+                .unwrap()
+                .to_string(),
+            );
             body["properties"]["uid"] = serde_json::Value::Array(vec![uid.clone()]);
             match body["properties"]["url"].as_array_mut() {
-                Some(array) => {
-                    array.push(uid)
-                }
-                None => {
-                    body["properties"]["url"] = body["properties"]["uid"].clone()
-                }
+                Some(array) => array.push(uid),
+                None => body["properties"]["url"] = body["properties"]["uid"].clone(),
             }
         }
         Some(uid_str) => {
@@ -101,14 +100,13 @@ pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde
                         array.push(serde_json::Value::String(uid))
                     }
                 }
-                None => {
-                    body["properties"]["url"] = body["properties"]["uid"].clone()
-                }
+                None => body["properties"]["url"] = body["properties"]["uid"].clone(),
             }
         }
     }
     if let Some(slugs) = body["properties"]["mp-slug"].as_array() {
-        let new_urls = slugs.iter()
+        let new_urls = slugs
+            .iter()
             .map(|i| i.as_str().unwrap_or(""))
             .filter(|i| i != &"")
             .map(|i| me.join(&((&folder).clone() + i)).unwrap().to_string())
@@ -147,15 +145,25 @@ pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde
     }
     // TODO: maybe highlight #hashtags?
     // Find other processing to do and insert it here
-    return (body["properties"]["uid"][0].as_str().unwrap().to_string(), body)
+    return (
+        body["properties"]["uid"][0].as_str().unwrap().to_string(),
+        body,
+    );
 }
 
-pub async fn new_post<S: Storage>(req: Request<ApplicationState<S>>, body: serde_json::Value) -> Result {
+pub async fn new_post<S: Storage>(
+    req: Request<ApplicationState<S>>,
+    body: serde_json::Value,
+) -> Result {
     // First, check for rights.
     let user = req.ext::<User>().unwrap();
     let storage = &req.state().storage;
     if !user.check_scope("create") {
-        return error_json!(401, "invalid_scope", "Not enough privileges to post. Try a token with a \"create\" scope instead.")
+        return error_json!(
+            401,
+            "invalid_scope",
+            "Not enough privileges to post. Try a token with a \"create\" scope instead."
+        );
     }
     let (uid, post) = normalize_mf2(body, user);
 
@@ -163,29 +171,54 @@ pub async fn new_post<S: Storage>(req: Request<ApplicationState<S>>, body: serde
     // This software might also be used in a multi-user setting
     // where several users or identities share one Micropub server
     // (maybe a family website or a shitpost sideblog?)
-    if post["properties"]["url"].as_array().unwrap().iter().any(|url| !url.as_str().unwrap().starts_with(user.me.as_str()))
-        || !post["properties"]["uid"][0].as_str().unwrap().starts_with(user.me.as_str())
-        || post["properties"]["channel"].as_array().unwrap().iter().any(|url| !url.as_str().unwrap().starts_with(user.me.as_str()))
+    if post["properties"]["url"]
+        .as_array()
+        .unwrap()
+        .iter()
+        .any(|url| !url.as_str().unwrap().starts_with(user.me.as_str()))
+        || !post["properties"]["uid"][0]
+            .as_str()
+            .unwrap()
+            .starts_with(user.me.as_str())
+        || post["properties"]["channel"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|url| !url.as_str().unwrap().starts_with(user.me.as_str()))
     {
-        return error_json!(403, "forbidden", "You're trying to post to someone else's website...")
+        return error_json!(
+            403,
+            "forbidden",
+            "You're trying to post to someone else's website..."
+        );
     }
 
-
     match storage.post_exists(&uid).await {
-        Ok(exists) => if exists {
-            return error_json!(409, "already_exists", format!("A post with the exact same UID already exists in the database: {}", uid))
-        },
-        Err(err) => return Ok(err.into())
+        Ok(exists) => {
+            if exists {
+                return error_json!(
+                    409,
+                    "already_exists",
+                    format!(
+                        "A post with the exact same UID already exists in the database: {}",
+                        uid
+                    )
+                );
+            }
+        }
+        Err(err) => return Ok(err.into()),
     }
 
     if let Err(err) = storage.put_post(&post).await {
-        return error_json!(500, "database_error", format!("{}", err))
+        return error_json!(500, "database_error", format!("{}", err));
     }
 
     // It makes sense to use a loop here, because you wouldn't post to a hundred channels at once
     // Mostly one or two, and even those ones will be the ones picked for you by software
     for channel in post["properties"]["channel"]
-        .as_array().unwrap().iter()
+        .as_array()
+        .unwrap()
+        .iter()
         .map(|i| i.as_str().unwrap_or("").to_string())
         .filter(|i| !i.is_empty())
         .collect::<Vec<_>>()
@@ -193,22 +226,44 @@ pub async fn new_post<S: Storage>(req: Request<ApplicationState<S>>, body: serde
         let default_channel = user.me.join(DEFAULT_CHANNEL_PATH).unwrap().to_string();
         let vcards_channel = user.me.join(CONTACTS_CHANNEL_PATH).unwrap().to_string();
         match storage.post_exists(&channel).await {
-            Ok(exists) => if exists {
-                if let Err(err) = storage.update_post(&channel, json!({
-                    "add": {
-                        "children": [uid]
+            Ok(exists) => {
+                if exists {
+                    if let Err(err) = storage
+                        .update_post(
+                            &channel,
+                            json!({
+                                "add": {
+                                    "children": [uid]
+                                }
+                            }),
+                        )
+                        .await
+                    {
+                        return error_json!(
+                            500,
+                            "database_error",
+                            format!(
+                                "Couldn't insert post into the channel due to a database error: {}",
+                                err
+                            )
+                        );
                     }
-                })).await {
-                    return error_json!(500, "database_error", format!("Couldn't insert post into the channel due to a database error: {}", err))
-                }
-            } else if channel == default_channel || channel == vcards_channel {
-                if let Err(err) = create_feed(storage, &uid, &channel, &user).await {
-                    return error_json!(500, "database_error", format!("Couldn't save feed: {}", err))
+                } else if channel == default_channel || channel == vcards_channel {
+                    if let Err(err) = create_feed(storage, &uid, &channel, &user).await {
+                        return error_json!(
+                            500,
+                            "database_error",
+                            format!("Couldn't save feed: {}", err)
+                        );
+                    }
+                } else {
+                    warn!(
+                        "Ignoring request to post to a non-existent feed: {}",
+                        channel
+                    );
                 }
-            } else {
-                warn!("Ignoring request to post to a non-existent feed: {}", channel);
-            },
-            Err(err) => return error_json!(500, "database_error", err)
+            }
+            Err(err) => return error_json!(500, "database_error", err),
         }
     }
     // END WRITE BOUNDARY
@@ -222,26 +277,39 @@ pub async fn new_post<S: Storage>(req: Request<ApplicationState<S>>, body: serde
         .build());
 }
 
-async fn create_feed(storage: &impl Storage, uid: &str, channel: &str, user: &User) -> crate::database::Result<()> {
+async fn create_feed(
+    storage: &impl Storage,
+    uid: &str,
+    channel: &str,
+    user: &User,
+) -> crate::database::Result<()> {
     let path = url::Url::parse(channel).unwrap().path().to_string();
     let (name, slug) = if path == DEFAULT_CHANNEL_PATH {
         (DEFAULT_CHANNEL_NAME, "main")
     } else if path == CONTACTS_CHANNEL_PATH {
         (CONTACTS_CHANNEL_NAME, "vcards")
-    } else { panic!("Tried to create an unknown default feed!"); };
-
-    let (_, feed) = normalize_mf2(json!({
-        "type": ["h-feed"],
-        "properties": {
-            "name": [name],
-            "mp-slug": [slug],
-        },
-        "children": [uid]
-    }), &user);
+    } else {
+        panic!("Tried to create an unknown default feed!");
+    };
+
+    let (_, feed) = normalize_mf2(
+        json!({
+            "type": ["h-feed"],
+            "properties": {
+                "name": [name],
+                "mp-slug": [slug],
+            },
+            "children": [uid]
+        }),
+        &user,
+    );
     storage.put_post(&feed).await
 }
 
-async fn post_process_new_post<S: Storage>(req: Request<ApplicationState<S>>, post: serde_json::Value) {
+async fn post_process_new_post<S: Storage>(
+    req: Request<ApplicationState<S>>,
+    post: serde_json::Value,
+) {
     // TODO: Post-processing the post (aka second write pass)
     // - [-] Download rich reply contexts
     // - [-] Syndicate the post if requested, add links to the syndicated copies
@@ -262,11 +330,9 @@ async fn post_process_new_post<S: Storage>(req: Request<ApplicationState<S>>, po
     for prop in &["in-reply-to", "like-of", "repost-of", "bookmark-of"] {
         if let Some(array) = post["properties"][prop].as_array() {
             contextually_significant_posts.extend(
-                array.iter()
-                    .filter_map(|v| v.as_str()
-                        .and_then(|v| surf::Url::parse(v).ok()
-                    )
-                )
+                array
+                    .iter()
+                    .filter_map(|v| v.as_str().and_then(|v| surf::Url::parse(v).ok())),
             );
         }
     }
@@ -275,26 +341,28 @@ async fn post_process_new_post<S: Storage>(req: Request<ApplicationState<S>>, po
     contextually_significant_posts.dedup();
 
     // 1.3. Fetch the posts with their bodies and save them in a new Vec<(surf::Url, String)>
-    let posts_with_bodies: Vec<(surf::Url, String)> = stream::iter(contextually_significant_posts.into_iter())
-        .filter_map(|v: surf::Url| async move {
-            if let Ok(res) = http.get(&v).send().await {
-                if res.status() != 200 {
-                    return None
+    let posts_with_bodies: Vec<(surf::Url, String)> =
+        stream::iter(contextually_significant_posts.into_iter())
+            .filter_map(|v: surf::Url| async move {
+                if let Ok(res) = http.get(&v).send().await {
+                    if res.status() != 200 {
+                        return None;
+                    } else {
+                        return Some((v, res));
+                    }
                 } else {
-                    return Some((v, res))
+                    return None;
                 }
-            } else {
-                return None
-            }
-        })
-        .filter_map(|(v, mut res): (surf::Url, surf::Response)| async move {
-            if let Ok(body) = res.body_string().await {
-                return Some((v, body))
-            } else {
-                return None
-            }
-        })
-        .collect().await;
+            })
+            .filter_map(|(v, mut res): (surf::Url, surf::Response)| async move {
+                if let Ok(body) = res.body_string().await {
+                    return Some((v, body));
+                } else {
+                    return None;
+                }
+            })
+            .collect()
+            .await;
     // 1.4. Parse the bodies and include them in relevant places on the MF2 struct
     //      This requires an MF2 parser, and there are none for Rust at the moment.
     //
@@ -303,24 +371,32 @@ async fn post_process_new_post<S: Storage>(req: Request<ApplicationState<S>>, po
     // 2. Syndicate the post
     let syndicated_copies: Vec<serde_json::Value>;
     if let Some(syndication_targets) = post["properties"]["syndicate-to"].as_array() {
-        syndicated_copies = stream::iter(syndication_targets.into_iter()
-            .filter_map(|v| v.as_str())
-            .filter_map(|t| surf::Url::parse(t).ok())
-            .collect::<Vec<_>>().into_iter()
-            .map(|_t: surf::Url| async move {
-                // TODO: Define supported syndication methods
-                // and syndicate the endpoint there
-                // Possible ideas:
-                //  - indieweb.xyz (might need a lot of space for the buttons though, investigate proposing grouping syndication targets)
-                //  - news.indieweb.org (IndieNews - needs a category linking to #indienews)
-                //  - Twitter via brid.gy (do I really need Twitter syndication tho?)
-                if false {
-                    Some("")
-                } else {
-                    None
-                }
-            })
-        ).buffer_unordered(3).filter_map(|v| async move { v }).map(|v| serde_json::Value::String(v.to_string())).collect::<Vec<_>>().await;
+        syndicated_copies = stream::iter(
+            syndication_targets
+                .into_iter()
+                .filter_map(|v| v.as_str())
+                .filter_map(|t| surf::Url::parse(t).ok())
+                .collect::<Vec<_>>()
+                .into_iter()
+                .map(|_t: surf::Url| async move {
+                    // TODO: Define supported syndication methods
+                    // and syndicate the endpoint there
+                    // Possible ideas:
+                    //  - indieweb.xyz (might need a lot of space for the buttons though, investigate proposing grouping syndication targets)
+                    //  - news.indieweb.org (IndieNews - needs a category linking to #indienews)
+                    //  - Twitter via brid.gy (do I really need Twitter syndication tho?)
+                    if false {
+                        Some("")
+                    } else {
+                        None
+                    }
+                }),
+        )
+        .buffer_unordered(3)
+        .filter_map(|v| async move { v })
+        .map(|v| serde_json::Value::String(v.to_string()))
+        .collect::<Vec<_>>()
+        .await;
     } else {
         syndicated_copies = vec![]
     }
@@ -363,35 +439,67 @@ async fn post_process_new_post<S: Storage>(req: Request<ApplicationState<S>>, po
             // TODO: Replace this function once the MF2 parser is ready
             // A compliant parser's output format includes rels,
             // we could just find a Webmention one in there
-            let pattern = easy_scraper::Pattern::new(r#"<link href="{url}" rel="webmention">"#).expect("Pattern for webmentions couldn't be parsed");
+            let pattern = easy_scraper::Pattern::new(r#"<link href="{url}" rel="webmention">"#)
+                .expect("Pattern for webmentions couldn't be parsed");
             let matches = pattern.matches(&body);
-            if matches.is_empty() { return None }
+            if matches.is_empty() {
+                return None;
+            }
             let endpoint = &matches[0]["url"];
-            if let Ok(endpoint) = url.join(endpoint) { Some((url, endpoint)) } else { None }
+            if let Ok(endpoint) = url.join(endpoint) {
+                Some((url, endpoint))
+            } else {
+                None
+            }
         })
         .map(|(target, endpoint)| async move {
-            let response = http.post(&endpoint)
+            let response = http
+                .post(&endpoint)
                 .content_type("application/x-www-form-urlencoded")
                 .body(
-                    serde_urlencoded::to_string(vec![("source", source), ("target", &target.to_string())])
-                        .expect("Couldn't construct webmention form")
-                ).send().await;
+                    serde_urlencoded::to_string(vec![
+                        ("source", source),
+                        ("target", &target.to_string()),
+                    ])
+                    .expect("Couldn't construct webmention form"),
+                )
+                .send()
+                .await;
             match response {
-                Ok(response) => if response.status() == 200 || response.status() == 201 || response.status() == 202 {
-                    Ok(())
-                } else {
-                    error!("Sending webmention for {} to {} failed: Endpoint replied with HTTP {}", target, endpoint, response.status());
-                    Err(())
+                Ok(response) => {
+                    if response.status() == 200
+                        || response.status() == 201
+                        || response.status() == 202
+                    {
+                        Ok(())
+                    } else {
+                        error!(
+                            "Sending webmention for {} to {} failed: Endpoint replied with HTTP {}",
+                            target,
+                            endpoint,
+                            response.status()
+                        );
+                        Err(())
+                    }
                 }
                 Err(err) => {
-                    error!("Sending webmention for {} to {} failed: {}", target, endpoint, err);
+                    error!(
+                        "Sending webmention for {} to {} failed: {}",
+                        target, endpoint, err
+                    );
                     Err(())
                 }
             }
-        }).buffer_unordered(3).collect::<Vec<_>>().await;
+        })
+        .buffer_unordered(3)
+        .collect::<Vec<_>>()
+        .await;
 }
 
-async fn process_json<S: Storage>(req: Request<ApplicationState<S>>, body: serde_json::Value) -> Result {
+async fn process_json<S: Storage>(
+    req: Request<ApplicationState<S>>,
+    body: serde_json::Value,
+) -> Result {
     let is_action = body["action"].is_string() && body["url"].is_string();
     if is_action {
         // This could be an update, a deletion or an undeletion request.
@@ -402,37 +510,51 @@ async fn process_json<S: Storage>(req: Request<ApplicationState<S>>, body: serde
         match action {
             "delete" => {
                 if !user.check_scope("delete") {
-                    return error_json!(401, "insufficient_scope", "You need a `delete` scope to delete posts.")
+                    return error_json!(
+                        401,
+                        "insufficient_scope",
+                        "You need a `delete` scope to delete posts."
+                    );
                 }
                 if let Err(error) = req.state().storage.delete_post(&url).await {
-                    return Ok(error.into())
+                    return Ok(error.into());
                 }
                 return Ok(Response::builder(200).build());
-            },
+            }
             "update" => {
                 if !user.check_scope("update") {
-                    return error_json!(401, "insufficient_scope", "You need an `update` scope to update posts.")
+                    return error_json!(
+                        401,
+                        "insufficient_scope",
+                        "You need an `update` scope to update posts."
+                    );
                 }
                 if let Err(error) = req.state().storage.update_post(&url, body.clone()).await {
-                    return Ok(error.into())
+                    return Ok(error.into());
                 } else {
-                    return Ok(Response::builder(204).build())
+                    return Ok(Response::builder(204).build());
                 }
-            },
-            _ => {
-                return error_json!(400, "invalid_request", "This action is not supported.")
             }
+            _ => return error_json!(400, "invalid_request", "This action is not supported."),
         }
     } else if body["type"][0].is_string() {
         // This is definitely an h-entry or something similar. Check if it has properties?
         if body["properties"].is_object() {
             // Ok, this is definitely a new h-entry. Let's save it.
-            return new_post(req, body).await
+            return new_post(req, body).await;
         } else {
-            return error_json!(400, "invalid_request", "This MF2-JSON object has a type, but not properties. This makes no sense to post.")
+            return error_json!(
+                400,
+                "invalid_request",
+                "This MF2-JSON object has a type, but not properties. This makes no sense to post."
+            );
         }
     } else {
-        return error_json!(400, "invalid_request", "Try sending MF2-structured data or an object with an \"action\" and \"url\" keys.")
+        return error_json!(
+            400,
+            "invalid_request",
+            "Try sending MF2-structured data or an object with an \"action\" and \"url\" keys."
+        );
     }
 }
 
@@ -440,12 +562,15 @@ fn convert_form_to_mf2_json(form: Vec<(String, String)>) -> serde_json::Value {
     let mut mf2 = json!({"type": [], "properties": {}});
     for (k, v) in form {
         if k == "h" {
-            mf2["type"].as_array_mut().unwrap().push(json!("h-".to_string() + &v));
+            mf2["type"]
+                .as_array_mut()
+                .unwrap()
+                .push(json!("h-".to_string() + &v));
         } else if k != "access_token" {
             let key = k.strip_suffix("[]").unwrap_or(&k);
             match mf2["properties"][key].as_array_mut() {
                 Some(prop) => prop.push(json!(v)),
-                None => mf2["properties"][key] = json!([v])
+                None => mf2["properties"][key] = json!([v]),
             }
         }
     }
@@ -455,33 +580,50 @@ fn convert_form_to_mf2_json(form: Vec<(String, String)>) -> serde_json::Value {
     mf2
 }
 
-async fn process_form<S: Storage>(req: Request<ApplicationState<S>>, form: Vec<(String, String)>) -> Result {
+async fn process_form<S: Storage>(
+    req: Request<ApplicationState<S>>,
+    form: Vec<(String, String)>,
+) -> Result {
     if let Some((_, v)) = form.iter().find(|(k, _)| k == "action") {
         if v == "delete" {
             let user = req.ext::<User>().unwrap();
             if !user.check_scope("delete") {
-                return error_json!(401, "insufficient_scope", "You cannot delete posts without a `delete` scope.")
+                return error_json!(
+                    401,
+                    "insufficient_scope",
+                    "You cannot delete posts without a `delete` scope."
+                );
             }
             match form.iter().find(|(k, _)| k == "url") {
                 Some((_, url)) => {
                     if let Err(error) = req.state().storage.delete_post(&url).await {
-                        return error_json!(500, "database_error", error)
+                        return error_json!(500, "database_error", error);
                     }
-                    return Ok(Response::builder(200).build())
-                },
-                None => return error_json!(400, "invalid_request", "Please provide an `url` to delete.")
+                    return Ok(Response::builder(200).build());
+                }
+                None => {
+                    return error_json!(
+                        400,
+                        "invalid_request",
+                        "Please provide an `url` to delete."
+                    )
+                }
             }
         } else {
-            return error_json!(400, "invalid_request", "This action is not supported in form-encoded mode. (JSON requests support more actions, use JSON!)")
+            return error_json!(400, "invalid_request", "This action is not supported in form-encoded mode. (JSON requests support more actions, use JSON!)");
         }
     }
-    
+
     let mf2 = convert_form_to_mf2_json(form);
 
     if mf2["properties"].as_object().unwrap().keys().len() > 0 {
         return new_post(req, mf2).await;
     }
-    return error_json!(400, "invalid_request", "Try sending h=entry&content=something%20interesting");
+    return error_json!(
+        400,
+        "invalid_request",
+        "Try sending h=entry&content=something%20interesting"
+    );
 }
 
 pub async fn post_handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
@@ -489,29 +631,31 @@ pub async fn post_handler<S: Storage>(mut req: Request<ApplicationState<S>>) ->
         Some(value) => {
             if value == Mime::from_str("application/json").unwrap() {
                 match req.body_json::<serde_json::Value>().await {
-                    Ok(parsed) => {
-                        return process_json(req, parsed).await
-                    },
-                    Err(err) => return error_json!(
-                        400, "invalid_request",
-                        format!("Parsing JSON failed: {:?}", err)
-                    )
+                    Ok(parsed) => return process_json(req, parsed).await,
+                    Err(err) => {
+                        return error_json!(
+                            400,
+                            "invalid_request",
+                            format!("Parsing JSON failed: {:?}", err)
+                        )
+                    }
                 }
             } else if value == Mime::from_str("application/x-www-form-urlencoded").unwrap() {
                 match req.body_form::<Vec<(String, String)>>().await {
-                    Ok(parsed) => {
-                        return process_form(req, parsed).await
-                    },
-                    Err(err) => return error_json!(
-                        400, "invalid_request",
-                        format!("Parsing form failed: {:?}", err)
-                    )
+                    Ok(parsed) => return process_form(req, parsed).await,
+                    Err(err) => {
+                        return error_json!(
+                            400,
+                            "invalid_request",
+                            format!("Parsing form failed: {:?}", err)
+                        )
+                    }
                 }
             } else {
                 return error_json!(
                     415, "unsupported_media_type",
                     "What's this? Try sending JSON instead. (urlencoded form also works but is less cute)"
-                )
+                );
             }
         }
         _ => {
@@ -538,9 +682,22 @@ mod tests {
             }
         });
 
-        let (uid, normalized) = normalize_mf2(mf2.clone(), &User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"));
-        assert_eq!(normalized["properties"]["uid"][0], mf2["properties"]["uid"][0], "UID was replaced");
-        assert_eq!(normalized["properties"]["uid"][0], uid, "Returned post location doesn't match UID");
+        let (uid, normalized) = normalize_mf2(
+            mf2.clone(),
+            &User::new(
+                "https://fireburn.ru/",
+                "https://quill.p3k.io/",
+                "create update media",
+            ),
+        );
+        assert_eq!(
+            normalized["properties"]["uid"][0], mf2["properties"]["uid"][0],
+            "UID was replaced"
+        );
+        assert_eq!(
+            normalized["properties"]["uid"][0], uid,
+            "Returned post location doesn't match UID"
+        );
     }
 
     #[test]
@@ -548,7 +705,7 @@ mod tests {
         use serde_urlencoded::from_str;
 
         assert_eq!(
-            convert_form_to_mf2_json(from_str("h=entry&content=something%20interesting").unwrap()), 
+            convert_form_to_mf2_json(from_str("h=entry&content=something%20interesting").unwrap()),
             json!({
                 "type": ["h-entry"],
                 "properties": {
@@ -567,16 +724,64 @@ mod tests {
             }
         });
 
-        let (uid, post) = normalize_mf2(mf2, &User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"));
-        assert_eq!(post["properties"]["published"].as_array().expect("post['published'] is undefined").len(), 1, "Post doesn't have a published time");
-        DateTime::parse_from_rfc3339(post["properties"]["published"][0].as_str().unwrap()).expect("Couldn't parse date from rfc3339");
-        assert!(post["properties"]["url"].as_array().expect("post['url'] is undefined").len() > 0, "Post doesn't have any URLs");
-        assert_eq!(post["properties"]["uid"].as_array().expect("post['uid'] is undefined").len(), 1, "Post doesn't have a single UID");
-        assert_eq!(post["properties"]["uid"][0], uid, "UID of a post and its supposed location don't match");
-        assert!(uid.starts_with("https://fireburn.ru/posts/"), "The post namespace is incorrect");
-        assert_eq!(post["properties"]["content"][0]["html"].as_str().expect("Post doesn't have a rich content object").trim(), "<p>This is content!</p>", "Parsed Markdown content doesn't match expected HTML");
-        assert_eq!(post["properties"]["channel"][0], "https://fireburn.ru/feeds/main", "Post isn't posted to the main channel");
-        assert_eq!(post["properties"]["author"][0], "https://fireburn.ru/", "Post author is unknown");
+        let (uid, post) = normalize_mf2(
+            mf2,
+            &User::new(
+                "https://fireburn.ru/",
+                "https://quill.p3k.io/",
+                "create update media",
+            ),
+        );
+        assert_eq!(
+            post["properties"]["published"]
+                .as_array()
+                .expect("post['published'] is undefined")
+                .len(),
+            1,
+            "Post doesn't have a published time"
+        );
+        DateTime::parse_from_rfc3339(post["properties"]["published"][0].as_str().unwrap())
+            .expect("Couldn't parse date from rfc3339");
+        assert!(
+            post["properties"]["url"]
+                .as_array()
+                .expect("post['url'] is undefined")
+                .len()
+                > 0,
+            "Post doesn't have any URLs"
+        );
+        assert_eq!(
+            post["properties"]["uid"]
+                .as_array()
+                .expect("post['uid'] is undefined")
+                .len(),
+            1,
+            "Post doesn't have a single UID"
+        );
+        assert_eq!(
+            post["properties"]["uid"][0], uid,
+            "UID of a post and its supposed location don't match"
+        );
+        assert!(
+            uid.starts_with("https://fireburn.ru/posts/"),
+            "The post namespace is incorrect"
+        );
+        assert_eq!(
+            post["properties"]["content"][0]["html"]
+                .as_str()
+                .expect("Post doesn't have a rich content object")
+                .trim(),
+            "<p>This is content!</p>",
+            "Parsed Markdown content doesn't match expected HTML"
+        );
+        assert_eq!(
+            post["properties"]["channel"][0], "https://fireburn.ru/feeds/main",
+            "Post isn't posted to the main channel"
+        );
+        assert_eq!(
+            post["properties"]["author"][0], "https://fireburn.ru/",
+            "Post author is unknown"
+        );
     }
 
     #[test]
@@ -589,15 +794,27 @@ mod tests {
             },
         });
 
-        let (_, post) = normalize_mf2(mf2, &User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"));
-        assert!(post["properties"]["url"]
-            .as_array()
-            .unwrap()
-            .iter()
-            .map(|i| i.as_str().unwrap())
-            .any(|i| i == "https://fireburn.ru/posts/hello-post"),
-            "Didn't found an URL pointing to the location expected by the mp-slug semantics");
-        assert!(post["properties"]["mp-slug"].as_array().is_none(), "mp-slug wasn't deleted from the array!")
+        let (_, post) = normalize_mf2(
+            mf2,
+            &User::new(
+                "https://fireburn.ru/",
+                "https://quill.p3k.io/",
+                "create update media",
+            ),
+        );
+        assert!(
+            post["properties"]["url"]
+                .as_array()
+                .unwrap()
+                .iter()
+                .map(|i| i.as_str().unwrap())
+                .any(|i| i == "https://fireburn.ru/posts/hello-post"),
+            "Didn't found an URL pointing to the location expected by the mp-slug semantics"
+        );
+        assert!(
+            post["properties"]["mp-slug"].as_array().is_none(),
+            "mp-slug wasn't deleted from the array!"
+        )
     }
 
     #[test]
@@ -610,16 +827,31 @@ mod tests {
             }
         });
 
-        let (uid, post) = normalize_mf2(mf2, &User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"));
-        assert_eq!(post["properties"]["uid"][0], uid, "UID of a post and its supposed location don't match");
+        let (uid, post) = normalize_mf2(
+            mf2,
+            &User::new(
+                "https://fireburn.ru/",
+                "https://quill.p3k.io/",
+                "create update media",
+            ),
+        );
+        assert_eq!(
+            post["properties"]["uid"][0], uid,
+            "UID of a post and its supposed location don't match"
+        );
         assert_eq!(post["properties"]["author"][0], "https://fireburn.ru/");
-        assert!(post["properties"]["url"]
-            .as_array()
-            .unwrap()
-            .iter()
-            .map(|i| i.as_str().unwrap())
-            .any(|i| i == "https://fireburn.ru/feeds/main"),
-        "Didn't found an URL pointing to the location expected by the mp-slug semantics");
-        assert!(post["properties"]["mp-slug"].as_array().is_none(), "mp-slug wasn't deleted from the array!")
+        assert!(
+            post["properties"]["url"]
+                .as_array()
+                .unwrap()
+                .iter()
+                .map(|i| i.as_str().unwrap())
+                .any(|i| i == "https://fireburn.ru/feeds/main"),
+            "Didn't found an URL pointing to the location expected by the mp-slug semantics"
+        );
+        assert!(
+            post["properties"]["mp-slug"].as_array().is_none(),
+            "mp-slug wasn't deleted from the array!"
+        )
     }
-}
\ No newline at end of file
+}