about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/database/mod.rs150
-rw-r--r--src/database/redis/mod.rs293
-rw-r--r--src/frontend/mod.rs300
-rw-r--r--src/indieauth.rs99
-rw-r--r--src/lib.rs211
-rw-r--r--src/main.rs20
-rw-r--r--src/micropub/get.rs17
-rw-r--r--src/micropub/mod.rs2
-rw-r--r--src/micropub/post.rs624
9 files changed, 1188 insertions, 528 deletions
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 98c2cae..8579125 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -1,17 +1,17 @@
 #![warn(missing_docs)]
-use async_trait::async_trait;
-use serde::{Serialize,Deserialize};
 use crate::indieauth::User;
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
 
 mod redis;
 pub use crate::database::redis::RedisStorage;
 #[cfg(test)]
-pub use redis::tests::{RedisInstance, get_redis_instance};
+pub use redis::tests::{get_redis_instance, RedisInstance};
 
 #[derive(Serialize, Deserialize, PartialEq, Debug)]
 pub struct MicropubChannel {
     pub uid: String,
-    pub name: String
+    pub name: String,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -21,14 +21,14 @@ pub enum ErrorKind {
     JsonParsing,
     NotFound,
     BadRequest,
-    Other
+    Other,
 }
 
 #[derive(Debug)]
 pub struct StorageError {
     msg: String,
     source: Option<Box<dyn std::error::Error>>,
-    kind: ErrorKind
+    kind: ErrorKind,
 }
 unsafe impl Send for StorageError {}
 unsafe impl Sync for StorageError {}
@@ -38,14 +38,16 @@ impl From<StorageError> for tide::Response {
             ErrorKind::BadRequest => 400,
             ErrorKind::NotFound => 404,
             _ => 500,
-        }).body(serde_json::json!({
+        })
+        .body(serde_json::json!({
             "error": match err.kind() {
                 ErrorKind::BadRequest => "invalid_request",
                 ErrorKind::NotFound => "not_found",
                 _ => "database_error"
             },
             "error_description": err
-        })).build()
+        }))
+        .build()
     }
 }
 impl std::error::Error for StorageError {
@@ -58,7 +60,7 @@ impl From<serde_json::Error> for StorageError {
         Self {
             msg: format!("{}", err),
             source: Some(Box::new(err)),
-            kind: ErrorKind::JsonParsing
+            kind: ErrorKind::JsonParsing,
         }
     }
 }
@@ -70,15 +72,18 @@ impl std::fmt::Display for StorageError {
             ErrorKind::PermissionDenied => write!(f, "permission denied: "),
             ErrorKind::NotFound => write!(f, "not found: "),
             ErrorKind::BadRequest => write!(f, "bad request: "),
-            ErrorKind::Other => write!(f, "generic storage layer error: ")
+            ErrorKind::Other => write!(f, "generic storage layer error: "),
         } {
             Ok(_) => write!(f, "{}", self.msg),
-            Err(err) => Err(err)
+            Err(err) => Err(err),
         }
     }
 }
 impl serde::Serialize for StorageError {
-    fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
+    fn serialize<S: serde::Serializer>(
+        &self,
+        serializer: S,
+    ) -> std::result::Result<S::Ok, S::Error> {
         serializer.serialize_str(&self.to_string())
     }
 }
@@ -88,7 +93,7 @@ impl StorageError {
         StorageError {
             msg: msg.to_string(),
             source: None,
-            kind
+            kind,
         }
     }
     /// Get the kind of an error.
@@ -100,12 +105,11 @@ impl StorageError {
     }
 }
 
-
 /// A special Result type for the Micropub backing storage.
 pub type Result<T> = std::result::Result<T, StorageError>;
 
 /// A storage backend for the Micropub server.
-/// 
+///
 /// Implementations should note that all methods listed on this trait MUST be fully atomic
 /// or lock the database so that write conflicts or reading half-written data should not occur.
 #[async_trait]
@@ -117,21 +121,21 @@ pub trait Storage: Clone + Send + Sync {
     async fn get_post(&self, url: &str) -> Result<Option<serde_json::Value>>;
 
     /// Save a post to the database as an MF2-JSON structure.
-    /// 
+    ///
     /// Note that the `post` object MUST have `post["properties"]["uid"][0]` defined.
     async fn put_post<'a>(&self, post: &'a serde_json::Value) -> Result<()>;
 
     /*/// Save a post and add it to the relevant feeds listed in `post["properties"]["channel"]`.
-    /// 
-    /// Note that the `post` object MUST have `post["properties"]["uid"][0]` defined 
+    ///
+    /// Note that the `post` object MUST have `post["properties"]["uid"][0]` defined
     /// and `post["properties"]["channel"]` defined, even if it's empty.
     async fn put_and_index_post<'a>(&mut self, post: &'a serde_json::Value) -> Result<()>;*/
-    
+
     /// Modify a post using an update object as defined in the Micropub spec.
-    /// 
+    ///
     /// Note to implementors: the update operation MUST be atomic OR MUST lock the database
     /// to prevent two clients overwriting each other's changes.
-    /// 
+    ///
     /// You can assume concurrent updates will never contradict each other, since that will be dumb.
     /// The last update always wins.
     async fn update_post<'a>(&self, url: &'a str, update: serde_json::Value) -> Result<()>;
@@ -142,21 +146,27 @@ pub trait Storage: Clone + Send + Sync {
     /// Fetch a feed at `url` and return a an h-feed object containing
     /// `limit` posts after a post by url `after`, filtering the content
     /// in context of a user specified by `user` (or an anonymous user).
-    /// 
+    ///
     /// Specifically, private posts that don't include the user in the audience
     /// will be elided from the feed, and the posts containing location and not
     /// specifying post["properties"]["location-visibility"][0] == "public"
     /// will have their location data (but not check-in data) stripped.
-    /// 
+    ///
     /// This function is used as an optimization so the client, whatever it is,
     /// doesn't have to fetch posts, then realize some of them are private, and
     /// fetch more posts.
-    /// 
+    ///
     /// Note for implementors: if you use streams to fetch posts in parallel
     /// from the database, preferably make this method use a connection pool
     /// to reduce overhead of creating a database connection per post for
     /// parallel fetching.
-    async fn read_feed_with_limit<'a>(&self, url: &'a str, after: &'a Option<String>, limit: usize, user: &'a Option<String>) -> Result<Option<serde_json::Value>>;
+    async fn read_feed_with_limit<'a>(
+        &self,
+        url: &'a str,
+        after: &'a Option<String>,
+        limit: usize,
+        user: &'a Option<String>,
+    ) -> Result<Option<serde_json::Value>>;
 
     /// Deletes a post from the database irreversibly. 'nuff said. Must be idempotent.
     async fn delete_post<'a>(&self, url: &'a str) -> Result<()>;
@@ -170,9 +180,9 @@ pub trait Storage: Clone + Send + Sync {
 
 #[cfg(test)]
 mod tests {
-    use super::{Storage, MicropubChannel};
-    use serde_json::json;
     use super::redis::tests::get_redis_instance;
+    use super::{MicropubChannel, Storage};
+    use serde_json::json;
 
     async fn test_backend_basic_operations<Backend: Storage>(backend: Backend) {
         let post: serde_json::Value = json!({
@@ -191,23 +201,47 @@ mod tests {
         backend.put_post(&post).await.unwrap();
         if let Ok(Some(returned_post)) = backend.get_post(&key).await {
             assert!(returned_post.is_object());
-            assert_eq!(returned_post["type"].as_array().unwrap().len(), post["type"].as_array().unwrap().len());
-            assert_eq!(returned_post["type"].as_array().unwrap(), post["type"].as_array().unwrap());
-            let props: &serde_json::Map<String, serde_json::Value> = post["properties"].as_object().unwrap();
+            assert_eq!(
+                returned_post["type"].as_array().unwrap().len(),
+                post["type"].as_array().unwrap().len()
+            );
+            assert_eq!(
+                returned_post["type"].as_array().unwrap(),
+                post["type"].as_array().unwrap()
+            );
+            let props: &serde_json::Map<String, serde_json::Value> =
+                post["properties"].as_object().unwrap();
             for key in props.keys() {
-                assert_eq!(returned_post["properties"][key].as_array().unwrap(), post["properties"][key].as_array().unwrap())
+                assert_eq!(
+                    returned_post["properties"][key].as_array().unwrap(),
+                    post["properties"][key].as_array().unwrap()
+                )
             }
-        } else { panic!("For some reason the backend did not return the post.") }
+        } else {
+            panic!("For some reason the backend did not return the post.")
+        }
         // Check the alternative URL - it should return the same post
         if let Ok(Some(returned_post)) = backend.get_post(&alt_url).await {
             assert!(returned_post.is_object());
-            assert_eq!(returned_post["type"].as_array().unwrap().len(), post["type"].as_array().unwrap().len());
-            assert_eq!(returned_post["type"].as_array().unwrap(), post["type"].as_array().unwrap());
-            let props: &serde_json::Map<String, serde_json::Value> = post["properties"].as_object().unwrap();
+            assert_eq!(
+                returned_post["type"].as_array().unwrap().len(),
+                post["type"].as_array().unwrap().len()
+            );
+            assert_eq!(
+                returned_post["type"].as_array().unwrap(),
+                post["type"].as_array().unwrap()
+            );
+            let props: &serde_json::Map<String, serde_json::Value> =
+                post["properties"].as_object().unwrap();
             for key in props.keys() {
-                assert_eq!(returned_post["properties"][key].as_array().unwrap(), post["properties"][key].as_array().unwrap())
+                assert_eq!(
+                    returned_post["properties"][key].as_array().unwrap(),
+                    post["properties"][key].as_array().unwrap()
+                )
             }
-        } else { panic!("For some reason the backend did not return the post.") }
+        } else {
+            panic!("For some reason the backend did not return the post.")
+        }
     }
 
     async fn test_backend_get_channel_list<Backend: Storage>(backend: Backend) {
@@ -221,33 +255,61 @@ mod tests {
             "children": []
         });
         backend.put_post(&feed).await.unwrap();
-        let chans = backend.get_channels(&crate::indieauth::User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media")).await.unwrap();
+        let chans = backend
+            .get_channels(&crate::indieauth::User::new(
+                "https://fireburn.ru/",
+                "https://quill.p3k.io/",
+                "create update media",
+            ))
+            .await
+            .unwrap();
         assert_eq!(chans.len(), 1);
-        assert_eq!(chans[0], MicropubChannel { uid: "https://fireburn.ru/feeds/main".to_string(), name: "Main Page".to_string() });
+        assert_eq!(
+            chans[0],
+            MicropubChannel {
+                uid: "https://fireburn.ru/feeds/main".to_string(),
+                name: "Main Page".to_string()
+            }
+        );
     }
 
     async fn test_backend_settings<Backend: Storage>(backend: Backend) {
-        backend.set_setting("site_name", "https://fireburn.ru/", "Vika's Hideout").await.unwrap();
-        assert_eq!(backend.get_setting("site_name", "https://fireburn.ru/").await.unwrap(), "Vika's Hideout");
+        backend
+            .set_setting("site_name", "https://fireburn.ru/", "Vika's Hideout")
+            .await
+            .unwrap();
+        assert_eq!(
+            backend
+                .get_setting("site_name", "https://fireburn.ru/")
+                .await
+                .unwrap(),
+            "Vika's Hideout"
+        );
     }
 
     #[async_std::test]
     async fn test_redis_storage_basic_operations() {
         let redis_instance = get_redis_instance().await;
-        let backend = super::RedisStorage::new(redis_instance.uri().to_string()).await.unwrap();
+        let backend = super::RedisStorage::new(redis_instance.uri().to_string())
+            .await
+            .unwrap();
         test_backend_basic_operations(backend).await;
     }
     #[async_std::test]
     async fn test_redis_storage_channel_list() {
         let redis_instance = get_redis_instance().await;
-        let backend = super::RedisStorage::new(redis_instance.uri().to_string()).await.unwrap();
+        let backend = super::RedisStorage::new(redis_instance.uri().to_string())
+            .await
+            .unwrap();
         test_backend_get_channel_list(backend).await;
     }
 
     #[async_std::test]
     async fn test_redis_settings() {
         let redis_instance = get_redis_instance().await;
-        let backend = super::RedisStorage::new(redis_instance.uri().to_string()).await.unwrap();
+        let backend = super::RedisStorage::new(redis_instance.uri().to_string())
+            .await
+            .unwrap();
         test_backend_settings(backend).await;
     }
 }
diff --git a/src/database/redis/mod.rs b/src/database/redis/mod.rs
index 352cece..5a6b70d 100644
--- a/src/database/redis/mod.rs
+++ b/src/database/redis/mod.rs
@@ -1,20 +1,20 @@
 use async_trait::async_trait;
+use futures::stream;
 use futures_util::FutureExt;
 use futures_util::StreamExt;
-use futures::stream;
 use lazy_static::lazy_static;
 use log::error;
+use mobc::Pool;
 use mobc_redis::redis;
 use mobc_redis::redis::AsyncCommands;
-use serde_json::json;
-use mobc::Pool;
 use mobc_redis::RedisConnectionManager;
+use serde_json::json;
 
-use crate::database::{Storage, Result, StorageError, ErrorKind, MicropubChannel};
+use crate::database::{ErrorKind, MicropubChannel, Result, Storage, StorageError};
 use crate::indieauth::User;
 
 struct RedisScripts {
-    edit_post: redis::Script
+    edit_post: redis::Script,
 }
 
 impl From<mobc_redis::redis::RedisError> for StorageError {
@@ -22,7 +22,7 @@ impl From<mobc_redis::redis::RedisError> for StorageError {
         Self {
             msg: format!("{}", err),
             source: Some(Box::new(err)),
-            kind: ErrorKind::Backend
+            kind: ErrorKind::Backend,
         }
     }
 }
@@ -31,7 +31,7 @@ impl From<mobc::Error<mobc_redis::redis::RedisError>> for StorageError {
         Self {
             msg: format!("{}", err),
             source: Some(Box::new(err)),
-            kind: ErrorKind::Backend
+            kind: ErrorKind::Backend,
         }
     }
 }
@@ -64,17 +64,40 @@ fn filter_post(mut post: serde_json::Value, user: &'_ Option<String>) -> Option<
         }));
     }
     let empty_vec: Vec<serde_json::Value> = vec![];
-    let author = post["properties"]["author"].as_array().unwrap_or(&empty_vec).iter().map(|i| i.as_str().unwrap().to_string());
-    let visibility = post["properties"]["visibility"][0].as_str().unwrap_or("public");
-    let mut audience = author.chain(post["properties"]["audience"].as_array().unwrap_or(&empty_vec).iter().map(|i| i.as_str().unwrap().to_string()));
-    if (visibility == "private" && !audience.any(|i| Some(i) == *user)) || (visibility == "protected" && user.is_none()) {
-        return None
+    let author = post["properties"]["author"]
+        .as_array()
+        .unwrap_or(&empty_vec)
+        .iter()
+        .map(|i| i.as_str().unwrap().to_string());
+    let visibility = post["properties"]["visibility"][0]
+        .as_str()
+        .unwrap_or("public");
+    let mut audience = author.chain(
+        post["properties"]["audience"]
+            .as_array()
+            .unwrap_or(&empty_vec)
+            .iter()
+            .map(|i| i.as_str().unwrap().to_string()),
+    );
+    if (visibility == "private" && !audience.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");
-        let mut author = post["properties"]["author"].as_array().unwrap_or(&empty_vec).iter().map(|i| i.as_str().unwrap().to_string());
+        let location_visibility = post["properties"]["location-visibility"][0]
+            .as_str()
+            .unwrap_or("private");
+        let mut author = post["properties"]["author"]
+            .as_array()
+            .unwrap_or(&empty_vec)
+            .iter()
+            .map(|i| i.as_str().unwrap().to_string());
         if location_visibility == "private" && !author.any(|i| Some(i) == *user) {
-            post["properties"].as_object_mut().unwrap().remove("location");
+            post["properties"]
+                .as_object_mut()
+                .unwrap()
+                .remove("location");
         }
     }
     Some(post)
@@ -84,12 +107,16 @@ fn filter_post(mut post: serde_json::Value, user: &'_ Option<String>) -> Option<
 impl Storage for RedisStorage {
     async fn get_setting<'a>(&self, setting: &'a str, user: &'a str) -> Result<String> {
         let mut conn = self.redis.get().await?;
-        Ok(conn.hget::<String, &str, String>(format!("settings_{}", user), setting).await?)
+        Ok(conn
+            .hget::<String, &str, String>(format!("settings_{}", user), setting)
+            .await?)
     }
 
     async fn set_setting<'a>(&self, setting: &'a str, user: &'a str, value: &'a str) -> Result<()> {
         let mut conn = self.redis.get().await?;
-        Ok(conn.hset::<String, &str, &str, ()>(format!("settings_{}", user), setting, value).await?)
+        Ok(conn
+            .hset::<String, &str, &str, ()>(format!("settings_{}", user), setting, value)
+            .await?)
     }
 
     async fn delete_post<'a>(&self, url: &'a str) -> Result<()> {
@@ -101,41 +128,63 @@ impl Storage for RedisStorage {
         let mut conn = self.redis.get().await?;
         Ok(conn.hexists::<&str, &str, bool>(&"posts", url).await?)
     }
-    
+
     async fn get_post(&self, url: &str) -> Result<Option<serde_json::Value>> {
         let mut conn = self.redis.get().await?;
-        match conn.hget::<&str, &str, Option<String>>(&"posts", url).await? {
+        match conn
+            .hget::<&str, &str, Option<String>>(&"posts", url)
+            .await?
+        {
             Some(val) => {
                 let parsed = serde_json::from_str::<serde_json::Value>(&val)?;
                 if let Some(new_url) = parsed["see_other"].as_str() {
-                    match conn.hget::<&str, &str, Option<String>>(&"posts", new_url).await? {
+                    match conn
+                        .hget::<&str, &str, Option<String>>(&"posts", new_url)
+                        .await?
+                    {
                         Some(val) => Ok(Some(serde_json::from_str::<serde_json::Value>(&val)?)),
-                        None => Ok(None)
+                        None => Ok(None),
                     }
                 } else {
                     Ok(Some(parsed))
                 }
-            },
-            None => Ok(None)
+            }
+            None => Ok(None),
         }
     }
 
     async fn get_channels(&self, user: &User) -> Result<Vec<MicropubChannel>> {
         let mut conn = self.redis.get().await?;
-        let channels = conn.smembers::<String, Vec<String>>("channels_".to_string() + user.me.as_str()).await?;
+        let channels = conn
+            .smembers::<String, Vec<String>>("channels_".to_string() + user.me.as_str())
+            .await?;
         // TODO: use streams here instead of this weird thing... how did I even write this?!
-        Ok(futures_util::future::join_all(channels.iter()
-            .map(|channel| self.get_post(channel)
-                .map(|result| result.unwrap())
-                .map(|post: Option<serde_json::Value>| {
-                    if let Some(post) = post {
-                        Some(MicropubChannel {
-                            uid: post["properties"]["uid"][0].as_str().unwrap().to_string(),
-                            name: post["properties"]["name"][0].as_str().unwrap().to_string()
-                        })
-                    } else { None }
+        Ok(futures_util::future::join_all(
+            channels
+                .iter()
+                .map(|channel| {
+                    self.get_post(channel).map(|result| result.unwrap()).map(
+                        |post: Option<serde_json::Value>| {
+                            if let Some(post) = post {
+                                Some(MicropubChannel {
+                                    uid: post["properties"]["uid"][0].as_str().unwrap().to_string(),
+                                    name: post["properties"]["name"][0]
+                                        .as_str()
+                                        .unwrap()
+                                        .to_string(),
+                                })
+                            } else {
+                                None
+                            }
+                        },
+                    )
                 })
-            ).collect::<Vec<_>>()).await.into_iter().filter_map(|chan| chan).collect::<Vec<_>>())
+                .collect::<Vec<_>>(),
+        )
+        .await
+        .into_iter()
+        .filter_map(|chan| chan)
+        .collect::<Vec<_>>())
     }
 
     async fn put_post<'a>(&self, post: &'a serde_json::Value) -> Result<()> {
@@ -143,72 +192,122 @@ impl Storage for RedisStorage {
         let key: &str;
         match post["properties"]["uid"][0].as_str() {
             Some(uid) => key = uid,
-            None => return Err(StorageError::new(ErrorKind::BadRequest, "post doesn't have a UID"))
+            None => {
+                return Err(StorageError::new(
+                    ErrorKind::BadRequest,
+                    "post doesn't have a UID",
+                ))
+            }
         }
-        conn.hset::<&str, &str, String, ()>(&"posts", key, post.to_string()).await?;
+        conn.hset::<&str, &str, String, ()>(&"posts", key, post.to_string())
+            .await?;
         if post["properties"]["url"].is_array() {
-            for url in post["properties"]["url"].as_array().unwrap().iter().map(|i| i.as_str().unwrap().to_string()) {
+            for url in post["properties"]["url"]
+                .as_array()
+                .unwrap()
+                .iter()
+                .map(|i| i.as_str().unwrap().to_string())
+            {
                 if url != key {
-                    conn.hset::<&str, &str, String, ()>(&"posts", &url, json!({"see_other": key}).to_string()).await?;
+                    conn.hset::<&str, &str, String, ()>(
+                        &"posts",
+                        &url,
+                        json!({ "see_other": key }).to_string(),
+                    )
+                    .await?;
                 }
             }
         }
-        if post["type"].as_array().unwrap().iter().any(|i| i == "h-feed") {
+        if post["type"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|i| i == "h-feed")
+        {
             // This is a feed. Add it to the channels array if it's not already there.
-            conn.sadd::<String, &str, ()>("channels_".to_string() + post["properties"]["author"][0].as_str().unwrap(), key).await?
+            conn.sadd::<String, &str, ()>(
+                "channels_".to_string() + post["properties"]["author"][0].as_str().unwrap(),
+                key,
+            )
+            .await?
         }
         Ok(())
     }
 
-    async fn read_feed_with_limit<'a>(&self, url: &'a str, after: &'a Option<String>, limit: usize, user: &'a Option<String>) -> Result<Option<serde_json::Value>> {
+    async fn read_feed_with_limit<'a>(
+        &self,
+        url: &'a str,
+        after: &'a Option<String>,
+        limit: usize,
+        user: &'a Option<String>,
+    ) -> Result<Option<serde_json::Value>> {
         let mut conn = self.redis.get().await?;
         let mut feed;
-        match conn.hget::<&str, &str, Option<String>>(&"posts", url).await? {
+        match conn
+            .hget::<&str, &str, Option<String>>(&"posts", url)
+            .await?
+        {
             Some(post) => feed = serde_json::from_str::<serde_json::Value>(&post)?,
-            None => return Ok(None)
+            None => return Ok(None),
         }
         if feed["see_other"].is_string() {
-            match conn.hget::<&str, &str, Option<String>>(&"posts", feed["see_other"].as_str().unwrap()).await? {
+            match conn
+                .hget::<&str, &str, Option<String>>(&"posts", feed["see_other"].as_str().unwrap())
+                .await?
+            {
                 Some(post) => feed = serde_json::from_str::<serde_json::Value>(&post)?,
-                None => return Ok(None)
+                None => return Ok(None),
             }
         }
         if let Some(post) = filter_post(feed, user) {
             feed = post
         } else {
-            return Err(StorageError::new(ErrorKind::PermissionDenied, "specified user cannot access this post"))
+            return Err(StorageError::new(
+                ErrorKind::PermissionDenied,
+                "specified user cannot access this post",
+            ));
         }
         if feed["children"].is_array() {
             let children = feed["children"].as_array().unwrap();
             let posts_iter: Box<dyn std::iter::Iterator<Item = String> + Send>;
             // TODO: refactor this to apply the skip on the &mut iterator
             if let Some(after) = after {
-                posts_iter = Box::new(children.iter().map(|i| i.as_str().unwrap().to_string()).skip_while(move |i| i != after).skip(1));
+                posts_iter = Box::new(
+                    children
+                        .iter()
+                        .map(|i| i.as_str().unwrap().to_string())
+                        .skip_while(move |i| i != after)
+                        .skip(1),
+                );
             } else {
                 posts_iter = Box::new(children.iter().map(|i| i.as_str().unwrap().to_string()));
             }
             let posts = stream::iter(posts_iter)
                 .map(|url| async move {
                     match self.redis.get().await {
-                        Ok(mut conn) => match conn.hget::<&str, &str, Option<String>>("posts", &url).await {
-                            Ok(post) => match post {
-                                Some(post) => match serde_json::from_str::<serde_json::Value>(&post) {
-                                    Ok(post) => Some(post),
-                                    Err(err) => {
-                                        let err = StorageError::from(err);
-                                        error!("{}", err);
-                                        panic!("{}", err)
+                        Ok(mut conn) => {
+                            match conn.hget::<&str, &str, Option<String>>("posts", &url).await {
+                                Ok(post) => match post {
+                                    Some(post) => {
+                                        match serde_json::from_str::<serde_json::Value>(&post) {
+                                            Ok(post) => Some(post),
+                                            Err(err) => {
+                                                let err = StorageError::from(err);
+                                                error!("{}", err);
+                                                panic!("{}", err)
+                                            }
+                                        }
                                     }
+                                    // Happens because of a broken link (result of an improper deletion?)
+                                    None => None,
                                 },
-                                // Happens because of a broken link (result of an improper deletion?)
-                                None => None,
-                            },
-                            Err(err) => {
-                                let err = StorageError::from(err);
-                                error!("{}", err);
-                                panic!("{}", err)
+                                Err(err) => {
+                                    let err = StorageError::from(err);
+                                    error!("{}", err);
+                                    panic!("{}", err)
+                                }
                             }
-                        },
+                        }
                         // TODO: Instead of causing a panic, investigate how can you fail the whole stream
                         // Somehow fuse it maybe?
                         Err(err) => {
@@ -227,14 +326,20 @@ impl Storage for RedisStorage {
                 // Hack to unwrap the Option and sieve out broken links
                 // Broken links return None, and Stream::filter_map skips all Nones.
                 .filter_map(|post: Option<serde_json::Value>| async move { post })
-                .filter_map(|post| async move {
-                    filter_post(post, user)
-                })
+                .filter_map(|post| async move { filter_post(post, user) })
                 .take(limit);
             // TODO: Instead of catching panics, find a way to make the whole stream fail with Result<Vec<serde_json::Value>>
-            match std::panic::AssertUnwindSafe(posts.collect::<Vec<serde_json::Value>>()).catch_unwind().await {
+            match std::panic::AssertUnwindSafe(posts.collect::<Vec<serde_json::Value>>())
+                .catch_unwind()
+                .await
+            {
                 Ok(posts) => feed["children"] = json!(posts),
-                Err(_) => return Err(StorageError::new(ErrorKind::Other, "Unknown error encountered while assembling feed, see logs for more info"))
+                Err(_) => {
+                    return Err(StorageError::new(
+                        ErrorKind::Other,
+                        "Unknown error encountered while assembling feed, see logs for more info",
+                    ))
+                }
             }
         }
         return Ok(Some(feed));
@@ -242,39 +347,56 @@ impl Storage for RedisStorage {
 
     async fn update_post<'a>(&self, mut url: &'a str, update: serde_json::Value) -> Result<()> {
         let mut conn = self.redis.get().await?;
-        if !conn.hexists::<&str, &str, bool>("posts", url).await.unwrap() {
-            return Err(StorageError::new(ErrorKind::NotFound, "can't edit a non-existent post"))
+        if !conn
+            .hexists::<&str, &str, bool>("posts", url)
+            .await
+            .unwrap()
+        {
+            return Err(StorageError::new(
+                ErrorKind::NotFound,
+                "can't edit a non-existent post",
+            ));
         }
-        let post: serde_json::Value = serde_json::from_str(&conn.hget::<&str, &str, String>("posts", url).await?)?;
+        let post: serde_json::Value =
+            serde_json::from_str(&conn.hget::<&str, &str, String>("posts", url).await?)?;
         if let Some(new_url) = post["see_other"].as_str() {
             url = new_url
         }
-        Ok(SCRIPTS.edit_post.key("posts").arg(url).arg(update.to_string()).invoke_async::<_, ()>(&mut conn as &mut redis::aio::Connection).await?)
+        Ok(SCRIPTS
+            .edit_post
+            .key("posts")
+            .arg(url)
+            .arg(update.to_string())
+            .invoke_async::<_, ()>(&mut conn as &mut redis::aio::Connection)
+            .await?)
     }
 }
 
-
 impl RedisStorage {
     /// Create a new RedisDatabase that will connect to Redis at `redis_uri` to store data.
     pub async fn new(redis_uri: String) -> Result<Self> {
         match redis::Client::open(redis_uri) {
-            Ok(client) => Ok(Self { redis: Pool::builder().max_open(20).build(RedisConnectionManager::new(client)) }),
-            Err(e) => Err(e.into())
+            Ok(client) => Ok(Self {
+                redis: Pool::builder()
+                    .max_open(20)
+                    .build(RedisConnectionManager::new(client)),
+            }),
+            Err(e) => Err(e.into()),
         }
     }
 }
 
 #[cfg(test)]
 pub mod tests {
+    use mobc_redis::redis;
     use std::process;
     use std::time::Duration;
-    use mobc_redis::redis;
 
     pub struct RedisInstance {
         // We just need to hold on to it so it won't get dropped and remove the socket
         _tempdir: tempdir::TempDir,
         uri: String,
-        child: std::process::Child
+        child: std::process::Child,
     }
     impl Drop for RedisInstance {
         fn drop(&mut self) {
@@ -292,11 +414,14 @@ pub mod tests {
         let socket = tempdir.path().join("redis.sock");
         let redis_child = process::Command::new("redis-server")
             .current_dir(&tempdir)
-            .arg("--port").arg("0")
-            .arg("--unixsocket").arg(&socket)
+            .arg("--port")
+            .arg("0")
+            .arg("--unixsocket")
+            .arg(&socket)
             .stdout(process::Stdio::null())
             .stderr(process::Stdio::null())
-            .spawn().expect("Failed to spawn Redis");
+            .spawn()
+            .expect("Failed to spawn Redis");
         println!("redis+unix:///{}", socket.to_str().unwrap());
         let uri = format!("redis+unix:///{}", socket.to_str().unwrap());
         // There should be a slight delay, we need to wait for Redis to spin up
@@ -317,7 +442,9 @@ pub mod tests {
         }
 
         return RedisInstance {
-            uri, child: redis_child, _tempdir: tempdir
-        }
+            uri,
+            child: redis_child,
+            _tempdir: tempdir,
+        };
     }
 }
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"),
     }?)
 }
diff --git a/src/indieauth.rs b/src/indieauth.rs
index 27a70a1..f6bac04 100644
--- a/src/indieauth.rs
+++ b/src/indieauth.rs
@@ -1,11 +1,11 @@
 use async_trait::async_trait;
 #[allow(unused_imports)]
-use log::{error,info};
-use url::Url;
+use log::{error, info};
+use std::sync::Arc;
 use tide::prelude::*;
 #[allow(unused_imports)]
-use tide::{Request, Response, Next, Result};
-use std::sync::Arc;
+use tide::{Next, Request, Response, Result};
+use url::Url;
 
 use crate::database;
 use crate::ApplicationState;
@@ -14,7 +14,7 @@ use crate::ApplicationState;
 pub struct User {
     pub me: Url,
     pub client_id: Url,
-    scope: String
+    scope: String,
 }
 
 impl User {
@@ -28,23 +28,40 @@ impl User {
         Self {
             me: Url::parse(me).unwrap(),
             client_id: Url::parse(client_id).unwrap(),
-            scope: scope.to_string()
+            scope: scope.to_string(),
         }
     }
 }
 
 #[cfg(any(not(debug_assertions), test))]
-async fn get_token_data(token: String, token_endpoint: &http_types::Url, http_client: &surf::Client) -> (http_types::StatusCode, Option<User>) {
-    match http_client.get(token_endpoint).header("Authorization", token).header("Accept", "application/json").send().await {
+async fn get_token_data(
+    token: String,
+    token_endpoint: &http_types::Url,
+    http_client: &surf::Client,
+) -> (http_types::StatusCode, Option<User>) {
+    match http_client
+        .get(token_endpoint)
+        .header("Authorization", token)
+        .header("Accept", "application/json")
+        .send()
+        .await
+    {
         Ok(mut resp) => {
             if resp.status() == 200 {
                 match resp.body_json::<User>().await {
                     Ok(user) => {
-                        info!("Token endpoint request successful. Validated user: {}", user.me);
+                        info!(
+                            "Token endpoint request successful. Validated user: {}",
+                            user.me
+                        );
                         (resp.status(), Some(user))
-                    },
+                    }
                     Err(err) => {
-                        error!("Token endpoint parsing error (HTTP status {}): {}", resp.status(), err);
+                        error!(
+                            "Token endpoint parsing error (HTTP status {}): {}",
+                            resp.status(),
+                            err
+                        );
                         (http_types::StatusCode::InternalServerError, None)
                     }
                 }
@@ -63,7 +80,7 @@ async fn get_token_data(token: String, token_endpoint: &http_types::Url, http_cl
 pub struct IndieAuthMiddleware {
     #[allow(dead_code)] // it's not really dead since it's only dead in debug scope
     cache: Arc<retainer::Cache<String, User>>,
-    monitor_task: Option<async_std::task::JoinHandle<()>>
+    monitor_task: Option<async_std::task::JoinHandle<()>>,
 }
 impl IndieAuthMiddleware {
     /// Create a new instance of IndieAuthMiddleware.
@@ -74,12 +91,19 @@ impl IndieAuthMiddleware {
     pub fn new() -> Self {
         let cache: Arc<retainer::Cache<String, User>> = Arc::new(retainer::Cache::new());
         let cache_clone = cache.clone();
-        let task = async_std::task::spawn(async move { cache_clone.monitor(4, 0.1, std::time::Duration::from_secs(30)).await });
+        let task = async_std::task::spawn(async move {
+            cache_clone
+                .monitor(4, 0.1, std::time::Duration::from_secs(30))
+                .await
+        });
 
         #[cfg(all(debug_assertions, not(test)))]
         error!("ATTENTION: You are running in debug mode. NO REQUESTS TO TOKEN ENDPOINT WILL BE MADE. YOU WILL BE PROCEEDING WITH DEBUG USER CREDENTIALS. DO NOT RUN LIKE THIS IN PRODUCTION.");
 
-        Self { cache, monitor_task: Some(task) }
+        Self {
+            cache,
+            monitor_task: Some(task),
+        }
     }
 }
 impl Drop for IndieAuthMiddleware {
@@ -96,7 +120,8 @@ impl Drop for IndieAuthMiddleware {
         // (it is safe tho cuz None is no nullptr and dereferencing it doesn't cause unsafety)
         // (could cause a VERY FUNNY race condition to occur though
         //  if you tried to refer to the value in another thread!)
-        let task = std::mem::take(&mut self.monitor_task).expect("Dropped IndieAuthMiddleware TWICE? Impossible!");
+        let task = std::mem::take(&mut self.monitor_task)
+            .expect("Dropped IndieAuthMiddleware TWICE? Impossible!");
         // Then cancel the task, using another task to request cancellation.
         // Because apparently you can't run async code from Drop...
         // This should drop the last reference for the [`cache`],
@@ -105,27 +130,41 @@ impl Drop for IndieAuthMiddleware {
     }
 }
 #[async_trait]
-impl<B> tide::Middleware<ApplicationState<B>> for IndieAuthMiddleware where
-    B: database::Storage + Send + Sync + Clone
+impl<B> tide::Middleware<ApplicationState<B>> for IndieAuthMiddleware
+where
+    B: database::Storage + Send + Sync + Clone,
 {
-
     #[cfg(all(not(test), debug_assertions))]
-    async fn handle(&self, mut req: Request<ApplicationState<B>>, next: Next<'_, ApplicationState<B>>) -> Result {
-        req.set_ext(User::new("http://localhost:8080/", "https://curl.haxx.se/","create update delete undelete media"));
+    async fn handle(
+        &self,
+        mut req: Request<ApplicationState<B>>,
+        next: Next<'_, ApplicationState<B>>,
+    ) -> Result {
+        req.set_ext(User::new(
+            "http://localhost:8080/",
+            "https://curl.haxx.se/",
+            "create update delete undelete media",
+        ));
         Ok(next.run(req).await)
     }
     #[cfg(any(not(debug_assertions), test))]
-    async fn handle(&self, mut req: Request<ApplicationState<B>>, next: Next<'_, ApplicationState<B>>) -> Result {
+    async fn handle(
+        &self,
+        mut req: Request<ApplicationState<B>>,
+        next: Next<'_, ApplicationState<B>>,
+    ) -> Result {
         let header = req.header("Authorization");
         match header {
             None => {
                 // TODO: move that to the request handling functions
                 // or make a middleware that refuses to accept unauthenticated requests
-                Ok(Response::builder(401).body(json!({
-                    "error": "unauthorized",
-                    "error_description": "Please provide an access token."
-                })).build())
-            },
+                Ok(Response::builder(401)
+                    .body(json!({
+                        "error": "unauthorized",
+                        "error_description": "Please provide an access token."
+                    }))
+                    .build())
+            }
             Some(value) => {
                 let endpoint = &req.state().token_endpoint;
                 let http_client = &req.state().http_client;
@@ -177,9 +216,13 @@ mod tests {
     use super::*;
     #[test]
     fn user_scopes_are_checkable() {
-        let user = User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media");
+        let user = User::new(
+            "https://fireburn.ru/",
+            "https://quill.p3k.io/",
+            "create update media",
+        );
 
         assert!(user.check_scope("create"));
         assert!(!user.check_scope("delete"));
     }
-}
\ No newline at end of file
+}
diff --git a/src/lib.rs b/src/lib.rs
index 27adc1a..d1bff68 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,22 +1,22 @@
 use tide::{Request, Response};
 
 mod database;
+mod frontend;
 mod indieauth;
 mod micropub;
-mod frontend;
 
 use crate::indieauth::IndieAuthMiddleware;
 
 #[derive(Clone)]
 pub struct ApplicationState<StorageBackend>
 where
-    StorageBackend: database::Storage + Send + Sync + 'static
+    StorageBackend: database::Storage + Send + Sync + 'static,
 {
     token_endpoint: surf::Url,
     authorization_endpoint: surf::Url,
     media_endpoint: Option<String>,
     http_client: surf::Client,
-    storage: StorageBackend
+    storage: StorageBackend,
 }
 
 type App<Storage> = tide::Server<ApplicationState<Storage>>;
@@ -25,70 +25,105 @@ static MICROPUB_CLIENT: &[u8] = include_bytes!("./index.html");
 
 fn equip_app<Storage>(mut app: App<Storage>) -> App<Storage>
 where
-    Storage: database::Storage + Send + Sync + Clone
+    Storage: database::Storage + Send + Sync + Clone,
 {
-    app.at("/micropub").with(IndieAuthMiddleware::new())
+    app.at("/micropub")
+        .with(IndieAuthMiddleware::new())
         .get(micropub::get_handler)
         .post(micropub::post_handler);
     // The Micropub client. It'll start small, but could grow into something full-featured!
     app.at("/micropub/client").get(|_: Request<_>| async move {
-        Ok(Response::builder(200).body(MICROPUB_CLIENT).content_type("text/html").build())
+        Ok(Response::builder(200)
+            .body(MICROPUB_CLIENT)
+            .content_type("text/html")
+            .build())
     });
-    app.at("/").with(frontend::ErrorHandlerMiddleware {})
+    app.at("/")
+        .with(frontend::ErrorHandlerMiddleware {})
         .get(frontend::mainpage)
         .post(frontend::onboarding_receiver);
-    app.at("/static/*path").with(frontend::ErrorHandlerMiddleware {}).get(frontend::handle_static);
-    app.at("/*path").with(frontend::ErrorHandlerMiddleware {}).get(frontend::render_post);
-    app.at("/coffee").with(frontend::ErrorHandlerMiddleware {}).get(frontend::coffee);
+    app.at("/static/*path")
+        .with(frontend::ErrorHandlerMiddleware {})
+        .get(frontend::handle_static);
+    app.at("/*path")
+        .with(frontend::ErrorHandlerMiddleware {})
+        .get(frontend::render_post);
+    app.at("/coffee")
+        .with(frontend::ErrorHandlerMiddleware {})
+        .get(frontend::coffee);
     app.at("/health").get(|_| async { Ok("OK") });
 
     app
 }
 
-pub async fn get_app_with_redis(token_endpoint: surf::Url, authorization_endpoint: surf::Url, redis_uri: String, media_endpoint: Option<String>) -> App<database::RedisStorage> {
-    let app = tide::with_state(ApplicationState { 
-        token_endpoint, media_endpoint,
+pub async fn get_app_with_redis(
+    token_endpoint: surf::Url,
+    authorization_endpoint: surf::Url,
+    redis_uri: String,
+    media_endpoint: Option<String>,
+) -> App<database::RedisStorage> {
+    let app = tide::with_state(ApplicationState {
+        token_endpoint,
+        media_endpoint,
         authorization_endpoint,
         storage: database::RedisStorage::new(redis_uri).await.unwrap(),
         http_client: surf::Client::new(),
     });
-    
+
     equip_app(app)
 }
 
 #[cfg(test)]
-pub async fn get_app_with_test_redis(token_endpoint: surf::Url) -> (database::RedisInstance, database::RedisStorage, App<database::RedisStorage>) {
+pub async fn get_app_with_test_redis(
+    token_endpoint: surf::Url,
+) -> (
+    database::RedisInstance,
+    database::RedisStorage,
+    App<database::RedisStorage>,
+) {
     use surf::Url;
 
     let redis_instance = database::get_redis_instance().await;
-    let backend = database::RedisStorage::new(redis_instance.uri().to_string()).await.unwrap();
+    let backend = database::RedisStorage::new(redis_instance.uri().to_string())
+        .await
+        .unwrap();
     let app = tide::with_state(ApplicationState {
-        token_endpoint, media_endpoint: None,
+        token_endpoint,
+        media_endpoint: None,
         authorization_endpoint: Url::parse("https://indieauth.com/auth").unwrap(),
         storage: backend.clone(),
         http_client: surf::Client::new(),
     });
-    return (redis_instance, backend, equip_app(app))
+    return (redis_instance, backend, equip_app(app));
 }
 
 #[cfg(test)]
 #[allow(unused_variables)]
 mod tests {
     use super::*;
+    use database::Storage;
+    use mockito::mock;
     use serde_json::json;
     use tide_testing::TideTestingExt;
-    use mockito::mock;
-    use database::Storage;
 
     // Helpers
-    async fn create_app() -> (database::RedisStorage, App<database::RedisStorage>, database::RedisInstance) {
+    async fn create_app() -> (
+        database::RedisStorage,
+        App<database::RedisStorage>,
+        database::RedisInstance,
+    ) {
         //get_app_with_memory_for_testing(surf::Url::parse(&*mockito::server_url()).unwrap()).await
-        let (r, b, a) = get_app_with_test_redis(surf::Url::parse(&*mockito::server_url()).unwrap()).await;
+        let (r, b, a) =
+            get_app_with_test_redis(surf::Url::parse(&*mockito::server_url()).unwrap()).await;
         (b, a, r)
     }
 
-    async fn post_json(app: &App<database::RedisStorage>, json: serde_json::Value) -> surf::Response {
-        let request = app.post("/micropub")
+    async fn post_json(
+        app: &App<database::RedisStorage>,
+        json: serde_json::Value,
+    ) -> surf::Response {
+        let request = app
+            .post("/micropub")
             .header("Authorization", "Bearer test")
             .header("Content-Type", "application/json")
             .body(json);
@@ -105,22 +140,30 @@ mod tests {
 
         let (db, app, _r) = create_app().await;
 
-        let response = post_json(&app, json!({
-            "type": ["h-entry"],
-            "properties": {
-                "content": ["Fake news about Aaron Parecki!"],
-                "uid": ["https://aaronparecki.com/posts/fake-news"]
-            }
-        })).await;
+        let response = post_json(
+            &app,
+            json!({
+                "type": ["h-entry"],
+                "properties": {
+                    "content": ["Fake news about Aaron Parecki!"],
+                    "uid": ["https://aaronparecki.com/posts/fake-news"]
+                }
+            }),
+        )
+        .await;
         assert_eq!(response.status(), 403);
 
-        let response = post_json(&app, json!({
-            "type": ["h-entry"],
-            "properties": {
-                "content": ["More fake news about Aaron Parecki!"],
-                "url": ["https://aaronparecki.com/posts/more-fake-news"]
-            }
-        })).await;
+        let response = post_json(
+            &app,
+            json!({
+                "type": ["h-entry"],
+                "properties": {
+                    "content": ["More fake news about Aaron Parecki!"],
+                    "url": ["https://aaronparecki.com/posts/more-fake-news"]
+                }
+            }),
+        )
+        .await;
         assert_eq!(response.status(), 403);
 
         let response = post_json(&app, json!({
@@ -143,9 +186,12 @@ mod tests {
 
         let (db, app, _r) = create_app().await;
 
-        let response: serde_json::Value = app.get("/micropub?q=config")
+        let response: serde_json::Value = app
+            .get("/micropub?q=config")
             .header("Authorization", "test")
-            .recv_json().await.unwrap();
+            .recv_json()
+            .await
+            .unwrap();
         assert!(!response["q"].as_array().unwrap().is_empty());
     }
 
@@ -159,9 +205,12 @@ mod tests {
 
         let (db, app, _r) = create_app().await;
 
-        let response: surf::Response = app.get("/micropub?q=config")
+        let response: surf::Response = app
+            .get("/micropub?q=config")
             .header("Authorization", "test")
-            .send().await.unwrap();
+            .send()
+            .await
+            .unwrap();
         assert_eq!(response.status(), 401);
     }
 
@@ -184,17 +233,27 @@ mod tests {
 
         let (storage, app, _r) = create_app().await;
 
-        let request: surf::RequestBuilder = app.post("/micropub")
+        let request: surf::RequestBuilder = app
+            .post("/micropub")
             .header("Authorization", "Bearer test")
             .header("Content-Type", "application/x-www-form-urlencoded")
             .body("h=entry&content=something%20interesting&category[]=test&category[]=stuff");
         let mut response: surf::Response = request.send().await.unwrap();
-        println!("{:#}", response.body_json::<serde_json::Value>().await.unwrap());
+        println!(
+            "{:#}",
+            response.body_json::<serde_json::Value>().await.unwrap()
+        );
         assert!(response.status() == 201 || response.status() == 202);
         let uid = response.header("Location").unwrap().last().to_string();
         // Assume the post is in the database at this point.
         let post = storage.get_post(&uid).await.unwrap().unwrap();
-        assert_eq!(post["properties"]["content"][0]["html"].as_str().unwrap().trim(), "<p>something interesting</p>");
+        assert_eq!(
+            post["properties"]["content"][0]["html"]
+                .as_str()
+                .unwrap()
+                .trim(),
+            "<p>something interesting</p>"
+        );
     }
 
     #[async_std::test]
@@ -207,35 +266,63 @@ mod tests {
 
         let (storage, app, _r) = create_app().await;
 
-        let mut response = post_json(&app, json!({
-            "type": ["h-entry"],
-            "properties": {
-                "content": ["This is content!"]
-            }
-        })).await;
-        println!("{:#}", response.body_json::<serde_json::Value>().await.unwrap());
+        let mut response = post_json(
+            &app,
+            json!({
+                "type": ["h-entry"],
+                "properties": {
+                    "content": ["This is content!"]
+                }
+            }),
+        )
+        .await;
+        println!(
+            "{:#}",
+            response.body_json::<serde_json::Value>().await.unwrap()
+        );
         assert!(response.status() == 201 || response.status() == 202);
         let uid = response.header("Location").unwrap().last().to_string();
         // Assume the post is in the database at this point.
         let post = storage.get_post(&uid).await.unwrap().unwrap();
-        assert_eq!(post["properties"]["content"][0]["html"].as_str().unwrap().trim(), "<p>This is content!</p>");
-        let feed = storage.get_post("https://fireburn.ru/feeds/main").await.unwrap().unwrap();
+        assert_eq!(
+            post["properties"]["content"][0]["html"]
+                .as_str()
+                .unwrap()
+                .trim(),
+            "<p>This is content!</p>"
+        );
+        let feed = storage
+            .get_post("https://fireburn.ru/feeds/main")
+            .await
+            .unwrap()
+            .unwrap();
         assert_eq!(feed["children"].as_array().unwrap().len(), 1);
         assert_eq!(feed["children"][0].as_str().unwrap(), uid);
         let first_uid = uid;
         // Test creation of a second post
-        let mut response = post_json(&app, json!({
-            "type": ["h-entry"],
-            "properties": {
-                "content": ["#moar content for you!"]
-            }
-        })).await;
-        println!("{:#}", response.body_json::<serde_json::Value>().await.unwrap());
+        let mut response = post_json(
+            &app,
+            json!({
+                "type": ["h-entry"],
+                "properties": {
+                    "content": ["#moar content for you!"]
+                }
+            }),
+        )
+        .await;
+        println!(
+            "{:#}",
+            response.body_json::<serde_json::Value>().await.unwrap()
+        );
         assert!(response.status() == 201 || response.status() == 202);
         let uid = response.header("Location").unwrap().last().to_string();
         // Assume the post is in the database at this point.
         //println!("Keys in database: {:?}", storage.mapping.read().await.keys());
-        let new_feed = storage.get_post("https://fireburn.ru/feeds/main").await.unwrap().unwrap();
+        let new_feed = storage
+            .get_post("https://fireburn.ru/feeds/main")
+            .await
+            .unwrap()
+            .unwrap();
         println!("{}", new_feed["children"]);
         assert_eq!(new_feed["children"].as_array().unwrap().len(), 2);
         assert_eq!(new_feed["children"][0].as_str().unwrap(), uid);
diff --git a/src/main.rs b/src/main.rs
index ce654df..1b333a2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
+use kittybox_micropub as micropub;
+use log::{debug, error, info};
 use std::env;
-use log::{error,info,debug};
 use surf::Url;
-use kittybox_micropub as micropub;
 
 #[async_std::main]
 async fn main() -> Result<(), std::io::Error> {
@@ -16,7 +16,7 @@ async fn main() -> Result<(), std::io::Error> {
         Ok(val) => {
             debug!("Redis connection: {}", val);
             redis_uri = val
-        },
+        }
         Err(_) => {
             error!("REDIS_URI is not set, cannot find a database");
             std::process::exit(1);
@@ -50,7 +50,7 @@ async fn main() -> Result<(), std::io::Error> {
                     std::process::exit(1)
                 }
             }
-        },
+        }
         Err(_) => {
             error!("AUTHORIZATION_ENDPOINT is not set, will not be able to confirm token and ID requests using IndieAuth!");
             std::process::exit(1)
@@ -59,7 +59,15 @@ async fn main() -> Result<(), std::io::Error> {
 
     let media_endpoint: Option<String> = env::var("MEDIA_ENDPOINT").ok();
 
-    let host = env::var("SERVE_AT").ok().unwrap_or_else(|| "0.0.0.0:8080".to_string());
-    let app = micropub::get_app_with_redis(token_endpoint, authorization_endpoint, redis_uri, media_endpoint).await;
+    let host = env::var("SERVE_AT")
+        .ok()
+        .unwrap_or_else(|| "0.0.0.0:8080".to_string());
+    let app = micropub::get_app_with_redis(
+        token_endpoint,
+        authorization_endpoint,
+        redis_uri,
+        media_endpoint,
+    )
+    .await;
     app.listen(host).await
 }
diff --git a/src/micropub/get.rs b/src/micropub/get.rs
index e106883..525bf12 100644
--- a/src/micropub/get.rs
+++ b/src/micropub/get.rs
@@ -1,23 +1,26 @@
-use tide::prelude::{Deserialize, json};
-use tide::{Request, Response, Result};
-use crate::ApplicationState;
-use crate::database::{MicropubChannel,Storage};
+use crate::database::{MicropubChannel, Storage};
 use crate::indieauth::User;
+use crate::ApplicationState;
+use tide::prelude::{json, Deserialize};
+use tide::{Request, Response, Result};
 
 #[derive(Deserialize)]
 struct QueryOptions {
     q: String,
-    url: Option<String>
+    url: Option<String>,
 }
 
 pub async fn get_handler<Backend>(req: Request<ApplicationState<Backend>>) -> Result
 where
-    Backend: Storage + Send + Sync
+    Backend: Storage + Send + Sync,
 {
     let user = req.ext::<User>().unwrap();
     let backend = &req.state().storage;
     let media_endpoint = &req.state().media_endpoint;
-    let query = req.query::<QueryOptions>().unwrap_or(QueryOptions { q: "".to_string(), url: None });
+    let query = req.query::<QueryOptions>().unwrap_or(QueryOptions {
+        q: "".to_string(),
+        url: None,
+    });
     match &*query.q {
         "config" => {
             let channels: Vec<MicropubChannel>;
diff --git a/src/micropub/mod.rs b/src/micropub/mod.rs
index 9bc553c..68a3134 100644
--- a/src/micropub/mod.rs
+++ b/src/micropub/mod.rs
@@ -2,5 +2,5 @@ pub mod get;
 pub mod post;
 
 pub use get::get_handler;
+pub use post::normalize_mf2;
 pub use post::post_handler;
-pub use post::normalize_mf2;
\ No newline at end of file
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
+}