about summary refs log tree commit diff
path: root/kittybox-rs/src/micropub/post.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-07-07 00:32:33 +0300
committerVika <vika@fireburn.ru>2022-07-07 00:36:39 +0300
commit7f23ec84bc05c236c1bf40c2f0d72412af711516 (patch)
treef0ba64804fffce29a8f04e5b6c76f9863de81dd2 /kittybox-rs/src/micropub/post.rs
parent5cfac54aa4fb3c207ea2cbbeccd4571fa204a09b (diff)
treewide: rewrite using Axum
Axum has streaming bodies and allows to write simpler code. It also
helps enforce stronger types and looks much more neat.

This allows me to progress on the media endpoint and add streaming
reads and writes to the MediaStore trait.

Metrics are temporarily not implemented. Everything else was
preserved, and the tests still pass, after adjusting for new calling
conventions.

TODO: create method routers for protocol endpoints
Diffstat (limited to 'kittybox-rs/src/micropub/post.rs')
-rw-r--r--kittybox-rs/src/micropub/post.rs944
1 files changed, 0 insertions, 944 deletions
diff --git a/kittybox-rs/src/micropub/post.rs b/kittybox-rs/src/micropub/post.rs
deleted file mode 100644
index cf9f3d9..0000000
--- a/kittybox-rs/src/micropub/post.rs
+++ /dev/null
@@ -1,944 +0,0 @@
-use crate::database::Storage;
-use crate::indieauth::User;
-use chrono::prelude::*;
-use core::iter::Iterator;
-use newbase60::num_to_sxg;
-use std::convert::TryInto;
-use serde_json::json;
-
-pub(crate) static DEFAULT_CHANNEL_PATH: &str = "/feeds/main";
-static DEFAULT_CHANNEL_NAME: &str = "Main feed";
-pub(crate) static CONTACTS_CHANNEL_PATH: &str = "/feeds/vcards";
-static CONTACTS_CHANNEL_NAME: &str = "My address book";
-pub(crate) static FOOD_CHANNEL_PATH: &str = "/feeds/food";
-static FOOD_CHANNEL_NAME: &str = "My recipe book";
-
-fn get_folder_from_type(post_type: &str) -> String {
-    (match post_type {
-        "h-feed" => "feeds/",
-        "h-card" => "vcards/",
-        "h-event" => "events/",
-        "h-food" => "food/",
-        _ => "posts/",
-    })
-    .to_string()
-}
-
-/// Reset the datetime to a proper datetime.
-/// Do not attempt to recover the information.
-/// Do not pass GO. Do not collect $200.                
-fn reset_dt(post: &mut serde_json::Value) -> DateTime<FixedOffset> {
-    let curtime: DateTime<Local> = Local::now();
-    post["properties"]["published"] = json!([curtime.to_rfc3339()]);
-    chrono::DateTime::from(curtime)
-}
-
-pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde_json::Value) {
-    // Normalize the MF2 object here.
-    let me = &user.me;
-    let folder = get_folder_from_type(body["type"][0].as_str().unwrap());
-    let published: DateTime<FixedOffset> = if let Some(dt) = body["properties"]["published"][0].as_str() {
-        // Check if the datetime is parsable.
-        match DateTime::parse_from_rfc3339(dt) {
-            Ok(dt) => dt,
-            Err(_) => reset_dt(&mut body)
-        }
-    } else {
-        // Set the datetime.
-        // Note: this code block duplicates functionality with the above failsafe.
-        // Consider refactoring it to a helper function?
-        reset_dt(&mut body)
-    };
-    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(),
-            );
-            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(uid_str) => {
-            let uid = uid_str.to_string();
-            match body["properties"]["url"].as_array_mut() {
-                Some(array) => {
-                    if !array.iter().any(|i| i.as_str().unwrap_or("") == uid) {
-                        array.push(serde_json::Value::String(uid))
-                    }
-                }
-                None => body["properties"]["url"] = body["properties"]["uid"].clone(),
-            }
-        }
-    }
-    if let Some(slugs) = body["properties"]["mp-slug"].as_array() {
-        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())
-            .collect::<Vec<String>>();
-        let urls = body["properties"]["url"].as_array_mut().unwrap();
-        new_urls.iter().for_each(|i| urls.push(json!(i)));
-    }
-    let props = body["properties"].as_object_mut().unwrap();
-    props.remove("mp-slug");
-
-    if body["properties"]["content"][0].is_string() {
-        // Convert the content to HTML using the `markdown` crate
-        body["properties"]["content"] = json!([{
-            "html": markdown::to_html(body["properties"]["content"][0].as_str().unwrap()),
-            "value": body["properties"]["content"][0]
-        }])
-    }
-    // TODO: apply this normalization to editing too
-    if body["properties"]["mp-channel"].is_array() {
-        let mut additional_channels = body["properties"]["mp-channel"].as_array().unwrap().clone();
-        if let Some(array) = body["properties"]["channel"].as_array_mut() {
-            array.append(&mut additional_channels);
-        } else {
-            body["properties"]["channel"] = json!(additional_channels)
-        }
-        body["properties"].as_object_mut().unwrap().remove("mp-channel");
-    } else if body["properties"]["mp-channel"].is_string() {
-        let chan = body["properties"]["mp-channel"].as_str().unwrap().to_owned();
-        if let Some(array) = body["properties"]["channel"].as_array_mut() {
-            array.push(json!(chan))
-        } else {
-            body["properties"]["channel"] = json!([chan]);
-        }
-        body["properties"].as_object_mut().unwrap().remove("mp-channel");
-    }
-    if body["properties"]["channel"][0].as_str().is_none() {
-        match body["type"][0].as_str() {
-            Some("h-entry") => {
-                // Set the channel to the main channel...
-                let default_channel = me.join(DEFAULT_CHANNEL_PATH).unwrap().to_string();
-
-                body["properties"]["channel"] = json!([default_channel]);
-            }
-            Some("h-card") => {
-                let default_channel = me.join(CONTACTS_CHANNEL_PATH).unwrap().to_string();
-
-                body["properties"]["channel"] = json!([default_channel]);
-            }
-            Some("h-food") => {
-                let default_channel = me.join(FOOD_CHANNEL_PATH).unwrap().to_string();
-
-                body["properties"]["channel"] = json!([default_channel]);
-            }
-            // TODO h-event
-            /*"h-event" => {
-                let default_channel
-            },*/
-            _ => {
-                body["properties"]["channel"] = json!([]);
-            }
-        }
-    }
-    body["properties"]["posted-with"] = json!([user.client_id]);
-    if body["properties"]["author"][0].as_str().is_none() {
-        body["properties"]["author"] = json!([me.as_str()])
-    }
-    // TODO: maybe highlight #hashtags?
-    // Find other processing to do and insert it here
-    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 {
-    // 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."
-        );
-    }
-    let (uid, post) = normalize_mf2(body, user);
-
-    // Security check!
-    // 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"]["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..."
-        );
-    }
-
-    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()),
-    }
-
-    if let Err(err) = storage.put_post(&post, user.me.as_str()).await {
-        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()
-        .map(|i| i.as_str().unwrap_or("").to_string())
-        .filter(|i| !i.is_empty())
-        .collect::<Vec<_>>()
-    {
-        let default_channel = user.me.join(DEFAULT_CHANNEL_PATH).unwrap().to_string();
-        let vcards_channel = user.me.join(CONTACTS_CHANNEL_PATH).unwrap().to_string();
-        let food_channel = user.me.join(FOOD_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]}}))
-                        .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
-                    || channel == food_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
-                    );
-                }
-            }
-            Err(err) => return error_json!(500, "database_error", err),
-        }
-    }
-    // END WRITE BOUNDARY
-
-    // do background processing on the post
-    async_std::task::spawn(post_process_new_post(req, post));
-
-    Ok(Response::builder(202)
-        .header("Location", &uid)
-        .body(json!({"status": "accepted", "location": &uid}))
-        .build())
-}*/
-
-pub(crate) 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();
-
-    // Note to Future Vika: DO NOT CONVERT THIS TO A MATCH BLOCK
-    // It will get treated as a binding instead of a const
-    // See `rustc --explain E0530` for more info
-    let name = if path == DEFAULT_CHANNEL_PATH {
-        DEFAULT_CHANNEL_NAME
-    } else if path == CONTACTS_CHANNEL_PATH {
-        CONTACTS_CHANNEL_NAME
-    } else if path == FOOD_CHANNEL_PATH {
-        FOOD_CHANNEL_NAME
-    } else {
-        panic!("Tried to create an unknown default feed!")
-    };
-
-    let (_, feed) = normalize_mf2(
-        json!({
-            "type": ["h-feed"],
-            "properties": {
-                "name": [name],
-                "uid": [channel]
-            },
-            "children": [uid]
-        }),
-        user,
-    );
-    storage.put_post(&feed, user.me.as_str()).await
-}
-
-/*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
-    // - [ ] Send WebSub notifications to the hub (if we happen to have one)
-    // - [x] Send webmentions
-    let http = &req.state().http_client;
-    let uid = post["properties"]["uid"][0].as_str().unwrap().to_string();
-    // 1. Download rich reply contexts
-    //    This needs to be done first, because at this step we can also determine webmention endpoints
-    //    and save them for later use. Additionally, the richer our content is, the better.
-    //    This needs to be done asynchronously, so the posting experience for the author will be as fast
-    //    as possible without making them wait for potentially slow downstream websites to load
-    // 1.1. Collect the list of contextually-significant post to load context from.
-    //      This will include reply-tos, liked, reposted and bookmarked content
-    //
-    //      TODO: Fetch links mentioned in a post, since we need to send webmentions to those as mentions
-    let mut contextually_significant_posts: Vec<surf::Url> = vec![];
-    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())),
-            );
-        }
-    }
-    // 1.2. Deduplicate the list
-    contextually_significant_posts.sort_unstable();
-    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, surf::Response, 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 {
-                        None
-                    } else {
-                        Some((v, res))
-                    }
-                } else {
-                    None
-                }
-            })
-            .filter_map(|(v, mut res): (surf::Url, surf::Response)| async move {
-                if let Ok(body) = res.body_string().await {
-                    Some((v, res, body))
-                } else {
-                    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.
-    //
-    // TODO: integrate https://gitlab.com/vikanezrimaya/mf2-parser when it's ready
-
-    // 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
-                .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![]
-    }
-    // Save the post a second time here after syndication
-    // We use update_post here to prevent race conditions since its required to be atomic
-    let mut update = json!({
-        "action": "update",
-        "url": &uid
-    });
-    if !syndicated_copies.is_empty() {
-        update["add"] = json!({});
-        update["add"]["syndication"] = serde_json::Value::Array(syndicated_copies);
-    }
-    if !posts_with_bodies.is_empty() {
-        error!("Replacing context links with parsed MF2-JSON data is not yet implemented (but it's ok! it'll just be less pretty)")
-        /* TODO: Replace context links with parsed MF2-JSON data * /
-        update["replace"] = {}
-        update["replace"]["like-of"] = []
-        update["replace"]["in-reply-to"] = []
-        update["replace"]["bookmark-of"] = []
-        update["replace"]["repost-of"] = []
-        // */
-    }
-    // We don't need the original copy of the post anymore... I hope!
-    // This will act as a safeguard so I can't read stale data by accident anymore...
-    drop(post);
-    if let Err(err) = req.state().storage.update_post(&uid, update).await {
-        error!("Encountered error while post-processing a post: {}", err)
-        // At this point, we can still continue, we just won't have rich data for the post
-        // I wonder why could it even happen except in case of a database disconnection?
-    }
-    // 3. Send WebSub notifications
-    // TODO WebSub support
-
-    // 4. Send webmentions
-    //    We'll need the bodies here to get their endpoints
-    let source = &uid;
-    stream::iter(posts_with_bodies.into_iter())
-        .filter_map(
-            |(url, response, body): (surf::Url, surf::Response, String)| async move {
-                // Check Link headers first
-                // the first webmention endpoint will be returned
-                if let Some(values) = response.header("Link") {
-                    let iter = values.iter().flat_map(|i| i.as_str().split(','));
-
-                    // Honestly I don't like this parser. It's very crude.
-                    // But it should do the job. But I don't like it.
-                    for link in iter {
-                        let mut split = link.split(';');
-
-                        match split.next() {
-                            Some(uri) => {
-                                if let Some(uri) = uri.strip_prefix('<') {
-                                    if let Some(uri) = uri.strip_suffix('>') {
-                                        for prop in split {
-                                            let lowercased = prop.to_ascii_lowercase();
-                                            if &lowercased == "rel=\"webmention\""
-                                                || &lowercased == "rel=webmention"
-                                            {
-                                                if let Ok(endpoint) = url.join(uri) {
-                                                    return Some((url, endpoint));
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                            None => continue,
-                        }
-                    }
-                }
-                // 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 matches = pattern.matches(&body);
-                if matches.is_empty() {
-                    return None;
-                }
-                let endpoint = &matches[0]["url"];
-                if let Ok(endpoint) = url.join(endpoint) {
-                    Some((url, endpoint))
-                } else {
-                    None
-                }
-            },
-        )
-        .map(|(target, endpoint)| async move {
-            info!(
-                "Sending webmention to {} about {}",
-                source,
-                &target.to_string()
-            );
-            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;
-            match response {
-                Ok(response) => {
-                    if response.status() == 200
-                        || response.status() == 201
-                        || response.status() == 202
-                    {
-                        info!("Sent webmention for {} to {}", target, endpoint);
-                        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
-                    );
-                    Err(())
-                }
-            }
-        })
-        .buffer_unordered(3)
-        .collect::<Vec<_>>()
-        .await;
-}*/
-
-/*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.
-        // Process it separately.
-        let action = body["action"].as_str().unwrap();
-        let url = body["url"].as_str().unwrap();
-        let user = req.ext::<User>().unwrap();
-        match action {
-            "delete" => {
-                if !user.check_scope("delete") {
-                    return error_json!(
-                        401,
-                        "insufficient_scope",
-                        "You need a `delete` scope to delete posts."
-                    );
-                }
-                // This special scope is not available through a token endpoint, since the
-                // authorization endpoint is supposed to reject any auth request trying to get this
-                // scope. It is intended for TRUSTED external services that need to modify the
-                // database while ignoring any access controls
-                if (url::Url::parse(url)?.origin().ascii_serialization() + "/") != user.me.as_str()
-                    && !user.check_scope("kittybox_internal:do_what_thou_wilt")
-                {
-                    return error_json!(
-                        403,
-                        "forbidden",
-                        "You're not allowed to delete someone else's posts."
-                    );
-                }
-                if let Err(error) = req.state().storage.delete_post(url).await {
-                    return Ok(error.into());
-                }
-                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."
-                    );
-                }
-                if (url::Url::parse(url)?.origin().ascii_serialization() + "/") != user.me.as_str()
-                    && !user.check_scope("kittybox_internal:do_what_thou_wilt")
-                {
-                    return error_json!(
-                        403,
-                        "forbidden",
-                        "You're not allowed to delete someone else's posts."
-                    );
-                }
-                if let Err(error) = req.state().storage.update_post(url, body.clone()).await {
-                    Ok(error.into())
-                } else {
-                    Ok(Response::builder(204).build())
-                }
-            }
-            _ => 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;
-        } else {
-            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."
-        );
-    }
-}*/
-
-/*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."
-                );
-            }
-            match form.iter().find(|(k, _)| k == "url") {
-                Some((_, url)) => {
-                    if (url::Url::parse(url)?.origin().ascii_serialization() + "/")
-                        != user.me.as_str()
-                        && !user.check_scope("kittybox_internal:do_what_thou_wilt")
-                    {
-                        return error_json!(
-                            403,
-                            "forbidden",
-                            "You're not allowed to delete someone else's posts."
-                        );
-                    }
-                    if let Err(error) = req.state().storage.delete_post(url).await {
-                        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."
-                    )
-                }
-            }
-        } else {
-            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"
-    );
-}*/
-
-/*pub async fn post_handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
-    match req.content_type() {
-        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)
-                        )
-                    }
-                }
-            } 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)
-                        )
-                    }
-                }
-            } else {
-                return error_json!(
-                    415, "unsupported_media_type",
-                    "What's this? Try sending JSON instead. (urlencoded form also works but is less cute)"
-                );
-            }
-        }
-        _ => {
-            return error_json!(
-                415, "unsupported_media_type",
-                "You didn't send a Content-Type header, so we don't know how to parse your request."
-            );
-        }
-    }
-}*/
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_no_replace_uid() {
-        let mf2 = json!({
-            "type": ["h-card"],
-            "properties": {
-                "uid": ["https://fireburn.ru/"],
-                "name": ["Vika Nezrimaya"],
-                "note": ["A crazy programmer girl who wants some hugs"]
-            }
-        });
-
-        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]
-    fn test_mp_channel() {
-        let mf2 = json!({
-            "type": ["h-entry"],
-            "properties": {
-                "uid": ["https://fireburn.ru/posts/test"],
-                "content": [{"html": "<p>Hello world!</p>"}],
-                "mp-channel": ["https://fireburn.ru/feeds/test"]
-            }
-        });
-
-        let (_, normalized) = normalize_mf2(
-            mf2.clone(),
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            )
-        );
-
-        assert_eq!(
-            normalized["properties"]["channel"],
-            mf2["properties"]["mp-channel"]
-        );
-    }
-
-    #[test]
-    fn test_mp_channel_as_string() {
-        let mf2 = json!({
-            "type": ["h-entry"],
-            "properties": {
-                "uid": ["https://fireburn.ru/posts/test"],
-                "content": [{"html": "<p>Hello world!</p>"}],
-                "mp-channel": "https://fireburn.ru/feeds/test"
-            }
-        });
-
-        let (_, normalized) = normalize_mf2(
-            mf2.clone(),
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            )
-        );
-
-        assert_eq!(
-            normalized["properties"]["channel"][0],
-            mf2["properties"]["mp-channel"]
-        );
-    }
-
-    #[test]
-    fn test_normalize_mf2() {
-        let mf2 = json!({
-            "type": ["h-entry"],
-            "properties": {
-                "content": ["This is content!"]
-            }
-        });
-
-        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")
-                .is_empty(),
-            "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]
-    fn test_mp_slug() {
-        let mf2 = json!({
-            "type": ["h-entry"],
-            "properties": {
-                "content": ["This is content!"],
-                "mp-slug": ["hello-post"]
-            },
-        });
-
-        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]
-    fn test_normalize_feed() {
-        let mf2 = json!({
-            "type": ["h-feed"],
-            "properties": {
-                "name": "Main feed",
-                "mp-slug": ["main"]
-            }
-        });
-
-        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!"
-        )
-    }
-}