~vpzom/hitide

3d0f120023526c507e4d97f70fb633288401f260 — Colin Reeder 6 months ago 1c2477e
Add image upload to New Post UI
5 files changed, 271 insertions(+), 17 deletions(-)

M Cargo.lock
M Cargo.toml
M res/lang/en.ftl
M src/resp_types.rs
M src/routes/communities.rs
M Cargo.lock => Cargo.lock +135 -0
@@ 1,6 1,15 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86"
dependencies = [
 "memchr",
]

[[package]]
name = "ammonia"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 79,12 88,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"

[[package]]
name = "derive_more"
version = "0.99.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "dtoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3"

[[package]]
name = "encoding_rs"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2"
dependencies = [
 "cfg-if",
]

[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 178,12 207,28 @@ dependencies = [
]

[[package]]
name = "futures"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-executor",
 "futures-io",
 "futures-sink",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-channel"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
dependencies = [
 "futures-core",
 "futures-sink",
]

[[package]]


@@ 193,6 238,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"

[[package]]
name = "futures-executor"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
dependencies = [
 "futures-core",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-io"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc94b64bb39543b4e432f1790b6bf18e3ee3b74653c5449f63310e9a74b123c"

[[package]]
name = "futures-macro"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 225,9 287,13 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-macro",
 "futures-sink",
 "futures-task",
 "memchr",
 "pin-project",
 "pin-utils",
 "proc-macro-hack",


@@ 299,6 365,7 @@ dependencies = [
 "hyper",
 "hyper-tls",
 "lazy_static",
 "multer",
 "render",
 "serde",
 "serde_derive",


@@ 535,6 602,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"

[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"

[[package]]
name = "mio"
version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 566,6 639,25 @@ dependencies = [
]

[[package]]
name = "multer"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99851e6ad01b0fbe086dda2dea00d68bb84fc7d7eae2c39ca7313da9197f4d31"
dependencies = [
 "bytes",
 "derive_more",
 "encoding_rs",
 "futures",
 "http",
 "httparse",
 "lazy_static",
 "log",
 "mime",
 "regex",
 "twoway",
]

[[package]]
name = "native-tls"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 882,6 974,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"

[[package]]
name = "regex"
version = "1.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
dependencies = [
 "aho-corasick",
 "memchr",
 "regex-syntax",
 "thread_local",
]

[[package]]
name = "regex-syntax"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"

[[package]]
name = "remove_dir_all"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1138,6 1248,15 @@ dependencies = [
]

[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
dependencies = [
 "lazy_static",
]

[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1229,6 1348,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"

[[package]]
name = "twoway"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
dependencies = [
 "memchr",
 "unchecked-index",
]

[[package]]
name = "type-map"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1238,6 1367,12 @@ dependencies = [
]

[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"

[[package]]
name = "unic-langid"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +1 -0
@@ 32,3 32,4 @@ lazy_static = "1.4.0"
unic-langid = { version = "0.9.0", features = ["macros"] }
futures-util = "0.3.5"
hitide_icons = { path = "./icons" }
multer = "1.2.2"

M res/lang/en.ftl => res/lang/en.ftl +3 -0
@@ 62,6 62,9 @@ post_delete_question = Delete this post?
post_delete_title = Delete Post
post_likes_nothing = Looks like nobody has liked this post yet.
post_new = New Post
post_new_href_conflict = Cannot specify both URL and Image
post_new_missing_content_type = Missing Content-Type for image upload
post_new_image_prompt = Image:
post_not_approved = This post has not been approved by the community.
preview = Preview
register = Register

M src/resp_types.rs => src/resp_types.rs +5 -0
@@ 166,6 166,11 @@ pub struct JustID {
}

#[derive(Deserialize, Debug)]
pub struct JustStringID<'a> {
    pub id: &'a str,
}

#[derive(Deserialize, Debug)]
pub struct RespYourFollow {
    pub accepted: bool,
}

M src/routes/communities.rs => src/routes/communities.rs +127 -17
@@ 1,13 1,14 @@
use crate::components::{CommunityLink, HTPage, MaybeFillInput, MaybeFillTextArea, PostItem};
use crate::resp_types::{
    JustContentHTML, RespCommunityInfoMaybeYour, RespMinimalAuthorInfo, RespMinimalCommunityInfo,
    RespPostListPost, RespYourFollow,
    JustContentHTML, JustStringID, RespCommunityInfoMaybeYour, RespMinimalAuthorInfo,
    RespMinimalCommunityInfo, RespPostListPost, RespYourFollow,
};
use crate::routes::{
    fetch_base_data, for_client, get_cookie_map_for_headers, get_cookie_map_for_req, html_response,
    res_to_error, CookieMap,
};
use serde_derive::Deserialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;



@@ 722,7 723,7 @@ async fn page_community_new_post_inner(
    cookies: &CookieMap<'_>,
    ctx: Arc<crate::RouteContext>,
    display_error: Option<String>,
    prev_values: Option<&HashMap<&str, serde_json::Value>>,
    prev_values: Option<&HashMap<Cow<'_, str>, serde_json::Value>>,
    display_preview: Option<&str>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;


@@ 744,7 745,7 @@ async fn page_community_new_post_inner(
                    }
                })
            }
            <form method={"POST"} action={&submit_url}>
            <form method={"POST"} action={&submit_url} enctype={"multipart/form-data"}>
                <table>
                    <tr>
                        <td>


@@ 762,6 763,14 @@ async fn page_community_new_post_inner(
                            <MaybeFillInput values={&prev_values} r#type={"text"} name={"href"} required={false} id={"input_url"} />
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <label for={"input_image"}>{lang.tr("post_new_image_prompt", None)}</label>
                        </td>
                        <td>
                            <input id={"input_image"} type={"file"} accept={"image/*"} name={"href_media"} />
                        </td>
                    </tr>
                </table>
                <label>
                    {lang.tr("text_with_markdown", None)}{":"}


@@ 792,12 801,113 @@ async fn handler_communities_new_post_submit(
    let (community_id,) = params;

    let (req_parts, body) = req.into_parts();
    let lang = crate::get_lang_for_headers(&req_parts.headers);
    let cookies = get_cookie_map_for_headers(&req_parts.headers)?;

    let body = hyper::body::to_bytes(body).await?;
    let mut body: HashMap<&str, serde_json::Value> = serde_urlencoded::from_bytes(&body)?;
    if body.contains_key("preview") {
        let md = body
    let content_type = req_parts
        .headers
        .get(hyper::header::CONTENT_TYPE)
        .ok_or_else(|| {
            crate::Error::InternalStr("missing content-type header in form submission".to_owned())
        })?;
    let content_type = std::str::from_utf8(content_type.as_ref())?;

    let boundary = multer::parse_boundary(&content_type)?;

    let mut multipart = multer::Multipart::new(body, boundary);

    let mut body_values: HashMap<Cow<'_, str>, serde_json::Value> = HashMap::new();
    {
        let mut error = None;

        loop {
            let field = multipart.next_field().await?;
            let field = match field {
                None => break,
                Some(field) => field,
            };

            if field.name().is_none() {
                continue;
            }

            if field.name().unwrap() == "href_media" {
                if body_values.contains_key("href") && body_values["href"] != "" {
                    error = Some(lang.tr("post_new_href_conflict", None).into_owned());
                } else {
                    match field.content_type() {
                        None => {
                            error =
                                Some(lang.tr("post_new_missing_content_type", None).into_owned());
                        }
                        Some(mime) => {
                            println!("will upload media");
                            let res = res_to_error(
                                ctx.http_client
                                    .request(for_client(
                                        hyper::Request::post(format!(
                                            "{}/api/unstable/media",
                                            ctx.backend_host,
                                        ))
                                        .header(hyper::header::CONTENT_TYPE, mime.as_ref())
                                        .body(hyper::Body::wrap_stream(field))?,
                                        &req_parts.headers,
                                        &cookies,
                                    )?)
                                    .await?,
                            )
                            .await;

                            match res {
                                Err(crate::Error::RemoteError((_, message))) => {
                                    error = Some(message);
                                }
                                Err(other) => {
                                    return Err(other);
                                }
                                Ok(res) => {
                                    let res = hyper::body::to_bytes(res.into_body()).await?;
                                    let res: JustStringID = serde_json::from_slice(&res)?;

                                    body_values.insert(
                                        "href".into(),
                                        format!("local-media://{}", res.id).into(),
                                    );
                                }
                            }

                            println!("finished media upload");
                        }
                    }
                }
            } else {
                let name = field.name().unwrap();
                if name == "href" && body_values.contains_key("href") && body_values["href"] != "" {
                    error = Some(lang.tr("post_new_href_conflict", None).into_owned());
                } else {
                    let name = name.to_owned();
                    let value = field.text().await?;
                    body_values.insert(name.into(), value.into());
                }
            }
        }

        if let Some(error) = error {
            return page_community_new_post_inner(
                community_id,
                &req_parts.headers,
                &cookies,
                ctx,
                Some(error),
                Some(&body_values),
                None,
            )
            .await;
        }
    }

    if body_values.contains_key("preview") {
        let md = body_values
            .get("content_markdown")
            .and_then(|x| x.as_str())
            .unwrap_or("");


@@ 828,7 938,7 @@ async fn handler_communities_new_post_submit(
                    &cookies,
                    ctx,
                    None,
                    Some(&body),
                    Some(&body_values),
                    Some(&preview_res.content_html),
                )
                .await


@@ 840,7 950,7 @@ async fn handler_communities_new_post_submit(
                    &cookies,
                    ctx,
                    Some(message),
                    Some(&body),
                    Some(&body_values),
                    None,
                )
                .await


@@ 849,19 959,19 @@ async fn handler_communities_new_post_submit(
        };
    }

    body.insert("community", community_id.into());
    if body.get("content_markdown").and_then(|x| x.as_str()) == Some("") {
        body.remove("content_markdown");
    body_values.insert("community".into(), community_id.into());
    if body_values.get("content_markdown").and_then(|x| x.as_str()) == Some("") {
        body_values.remove("content_markdown");
    }
    if body.get("href").and_then(|x| x.as_str()) == Some("") {
        body.remove("href");
    if body_values.get("href").and_then(|x| x.as_str()) == Some("") {
        body_values.remove("href");
    }

    let api_res = res_to_error(
        ctx.http_client
            .request(for_client(
                hyper::Request::post(format!("{}/api/unstable/posts", ctx.backend_host))
                    .body(serde_json::to_vec(&body)?.into())?,
                    .body(serde_json::to_vec(&body_values)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)


@@ 891,7 1001,7 @@ async fn handler_communities_new_post_submit(
                &cookies,
                ctx,
                Some(message),
                Some(&body),
                Some(&body_values),
                None,
            )
            .await