~vpzom/lotide

ff57208ead25b99135afe890188b6ebec4771b56 — Colin Reeder 9 months ago 3789d83
Support markdown for post content
M Cargo.lock => Cargo.lock +37 -0
@@ 478,6 478,15 @@ dependencies = [
]

[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
 "unicode-width",
]

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


@@ 752,6 761,7 @@ dependencies = [
 "mime",
 "openssl",
 "postgres-types",
 "pulldown-cmark",
 "serde",
 "serde_derive",
 "serde_json",


@@ 1106,6 1116,18 @@ dependencies = [
]

[[package]]
name = "pulldown-cmark"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55"
dependencies = [
 "bitflags",
 "getopts",
 "memchr",
 "unicase",
]

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


@@ 1538,6 1560,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"

[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
 "version_check",
]

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


@@ 1556,6 1587,12 @@ dependencies = [
]

[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"

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

M Cargo.toml => Cargo.toml +1 -0
@@ 36,3 36,4 @@ postgres-types = "0.1.2"
lazy_static = "1.4.0"
unic-char-range = "0.9.0"
http = "0.2.1"
pulldown-cmark = "0.7.2"

A migrations/20200718224820_markdown/down.sql => migrations/20200718224820_markdown/down.sql +4 -0
@@ 0,0 1,4 @@
BEGIN;
	ALTER TABLE post DROP COLUMN content_markdown;
	ALTER TABLE reply DROP COLUMN content_markdown;
COMMIT;

A migrations/20200718224820_markdown/up.sql => migrations/20200718224820_markdown/up.sql +4 -0
@@ 0,0 1,4 @@
BEGIN;
	ALTER TABLE post ADD COLUMN content_markdown TEXT;
	ALTER TABLE reply ADD COLUMN content_markdown TEXT;
COMMIT;

M src/apub_util.rs => src/apub_util.rs +17 -8
@@ 786,6 786,20 @@ pub fn post_to_ap(
) -> Result<activitystreams::BaseBox, crate::Error> {
    use std::convert::TryInto;

    let apply_content = |props: &mut activitystreams::object::properties::ObjectProperties| -> Result<(), crate::Error> {
        if let Some(html) = post.content_html {
            props
                .set_content_xsd_string(html)?
                .set_media_type(mime::TEXT_HTML)?;
        } else if let Some(text) = post.content_text {
            props
                .set_content_xsd_string(text)?
                .set_media_type(mime::TEXT_PLAIN)?;
        }

        Ok(())
    };

    match post.href {
        Some(href) => {
            let mut post_ap = activitystreams::object::Page::new();


@@ 804,12 818,7 @@ pub fn post_to_ap(
                .set_to_xsd_any_uri(community_ap_id)?
                .set_cc_xsd_any_uri(activitystreams::public())?;

            if let Some(content) = post.content_text {
                post_ap
                    .as_mut()
                    .set_content_xsd_string(content)?
                    .set_media_type(mime::TEXT_PLAIN)?;
            }
            apply_content(post_ap.as_mut())?;

            Ok(post_ap.try_into()?)
        }


@@ 824,13 833,13 @@ pub fn post_to_ap(
                    post.author.unwrap(),
                    &host_url_apub,
                ))?
                .set_content_xsd_string(post.content_text.unwrap_or(""))?
                .set_media_type(mime::TEXT_PLAIN)?
                .set_summary_xsd_string(post.title)?
                .set_published(post.created.clone())?
                .set_to_xsd_any_uri(community_ap_id)?
                .set_cc_xsd_any_uri(activitystreams::public())?;

            apply_content(post_ap.as_mut())?;

            Ok(post_ap.try_into()?)
        }
    }

M src/main.rs => src/main.rs +16 -0
@@ 93,11 93,15 @@ pub enum ThingLocalRef {
    Comment(i64),
}

#[derive(Debug)]
pub struct PostInfo<'a> {
    id: i64,
    author: Option<i64>,
    href: Option<&'a str>,
    content_text: Option<&'a str>,
    #[allow(dead_code)]
    content_markdown: Option<&'a str>,
    content_html: Option<&'a str>,
    title: &'a str,
    created: &'a chrono::DateTime<chrono::FixedOffset>,
    #[allow(dead_code)]


@@ 109,6 113,8 @@ pub struct PostInfoOwned {
    author: Option<i64>,
    href: Option<String>,
    content_text: Option<String>,
    content_markdown: Option<String>,
    content_html: Option<String>,
    title: String,
    created: chrono::DateTime<chrono::FixedOffset>,
    community: i64,


@@ 121,6 127,8 @@ impl<'a> Into<PostInfo<'a>> for &'a PostInfoOwned {
            author: self.author,
            href: self.href.as_deref(),
            content_text: self.content_text.as_deref(),
            content_markdown: self.content_markdown.as_deref(),
            content_html: self.content_html.as_deref(),
            title: &self.title,
            created: &self.created,
            community: self.community,


@@ 265,6 273,14 @@ pub fn spawn_task<F: std::future::Future<Output = Result<(), Error>> + Send + 's
    }));
}

pub fn render_markdown(src: &str) -> String {
    let parser = pulldown_cmark::Parser::new(src);
    let mut output = String::new();
    pulldown_cmark::html::push_html(&mut output, parser);

    output
}

pub fn on_community_add_post<'a>(
    community: i64,
    post_local_id: i64,

M src/routes/api/mod.rs => src/routes/api/mod.rs +37 -15
@@ 487,42 487,64 @@ async fn route_unstable_posts_create(
    struct PostsCreateBody {
        community: i64,
        href: Option<String>,
        content_markdown: Option<String>,
        content_text: Option<String>,
        title: String,
    }

    let body: PostsCreateBody = serde_json::from_slice(&body)?;

    if body.href.is_none() && body.content_text.is_none() {
    if body.href.is_none() && body.content_text.is_none() && body.content_markdown.is_none() {
        return Err(crate::Error::UserError(crate::simple_response(
            hyper::StatusCode::BAD_REQUEST,
            "Post must contain either href or content_text",
            "Post must contain one of href, content_text, or content_markdown",
        )));
    }

    if body.content_markdown.is_some() && body.content_text.is_some() {
        return Err(crate::Error::UserError(crate::simple_response(
            hyper::StatusCode::BAD_REQUEST,
            "content_markdown and content_text are mutually exclusive",
        )));
    }

    // TODO validate permissions to post

    let (content_text, content_markdown, content_html) = match body.content_markdown {
        Some(md) => {
            let (html, md) =
                tokio::task::spawn_blocking(move || (crate::render_markdown(&md), md)).await?;
            (None, Some(md), Some(html))
        }
        None => match body.content_text {
            Some(text) => (Some(text), None, None),
            None => (None, None, None),
        },
    };

    let res_row = db.query_one(
        "INSERT INTO post (author, href, content_text, title, created, community, local) VALUES ($1, $2, $3, $4, current_timestamp, $5, TRUE) RETURNING id, created, (SELECT local FROM community WHERE id=post.community)",
        &[&user, &body.href, &body.content_text, &body.title, &body.community],
        "INSERT INTO post (author, href, title, created, community, local, content_text, content_markdown, content_html) VALUES ($1, $2, $3, current_timestamp, $4, TRUE, $5, $6, $7) RETURNING id, created, (SELECT local FROM community WHERE id=post.community)",
        &[&user, &body.href, &body.title, &body.community, &content_text, &content_markdown, &content_html],
    ).await?;

    let id = res_row.get(0);
    let created = res_row.get(1);

    let post = crate::PostInfoOwned {
        id,
        author: Some(user),
        content_text,
        content_markdown,
        content_html,
        href: body.href,
        title: body.title,
        created,
        community: body.community,
    };

    crate::spawn_task(async move {
        let created = res_row.get(1);
        let community_local: Option<bool> = res_row.get(2);

        let post = crate::PostInfoOwned {
            id,
            author: Some(user),
            content_text: body.content_text,
            href: body.href,
            title: body.title,
            created,
            community: body.community,
        };

        if let Some(community_local) = community_local {
            if community_local {
                crate::on_community_add_post(

M src/routes/apub/mod.rs => src/routes/apub/mod.rs +20 -16
@@ 907,7 907,7 @@ async fn handler_posts_get(

    match db
        .query_opt(
            "SELECT author, href, content_text, title, created, community, local, deleted, had_href, (SELECT ap_id FROM community WHERE id=post.community) AS community_ap_id FROM post WHERE id=$1",
            "SELECT author, href, title, created, community, local, deleted, had_href, content_text, content_markdown, content_html, (SELECT ap_id FROM community WHERE id=post.community) AS community_ap_id FROM post WHERE id=$1",
            &[&post_id],
        )
        .await?


@@ 917,7 917,7 @@ async fn handler_posts_get(
            "No such post",
        )),
        Some(row) => {
            let local: bool = row.get(6);
            let local: bool = row.get(5);

            if !local {
                return Err(crate::Error::UserError(crate::simple_response(


@@ 926,8 926,8 @@ async fn handler_posts_get(
                )));
            }

            if row.get(7) {
                let had_href: Option<bool> = row.get(8);
            if row.get(6) {
                let had_href: Option<bool> = row.get(7);

                let mut body = activitystreams::object::Tombstone::new();
                body.tombstone_props.set_former_type_xsd_string(if had_href == Some(true) { "Page" } else { "Note" })?;


@@ 946,9 946,9 @@ async fn handler_posts_get(
                return Ok(resp);
            }

            let community_local_id = row.get(5);
            let community_local_id = row.get(4);

            let community_ap_id = match row.get(9) {
            let community_ap_id = match row.get(11) {
                Some(ap_id) => ap_id,
                None => {
                    // assume local (might be a problem?)


@@ 959,11 959,13 @@ async fn handler_posts_get(
            let post_info = crate::PostInfo {
                author: row.get(0),
                community: community_local_id,
                created: &row.get(4),
                created: &row.get(3),
                href: row.get(1),
                content_text: row.get(2),
                content_text: row.get(8),
                content_markdown: row.get(9),
                content_html: row.get(10),
                id: post_id,
                title: row.get(3),
                title: row.get(2),
            };

            let body = crate::apub_util::post_to_ap(&post_info, &community_ap_id, &ctx.host_url_apub)?;


@@ 991,7 993,7 @@ async fn handler_posts_create_get(

    match db
        .query_opt(
            "SELECT author, href, content_text, title, created, community, local, deleted, (SELECT ap_id FROM community WHERE id=post.community) AS community_ap_id FROM post WHERE id=$1",
            "SELECT author, href, title, created, community, local, deleted, content_text, content_markdown, content_html, (SELECT ap_id FROM community WHERE id=post.community) AS community_ap_id FROM post WHERE id=$1",
            &[&post_id],
        )
        .await?


@@ 1001,7 1003,7 @@ async fn handler_posts_create_get(
            "No such post",
        )),
        Some(row) => {
            let local: bool = row.get(6);
            let local: bool = row.get(5);

            if !local {
                return Err(crate::Error::UserError(crate::simple_response(


@@ 1010,16 1012,16 @@ async fn handler_posts_create_get(
                )));
            }

            if row.get(7) {
            if row.get(6) {
                return Err(crate::Error::UserError(crate::simple_response(
                    hyper::StatusCode::GONE,
                    "Post has been deleted",
                )));
            }

            let community_local_id = row.get(5);
            let community_local_id = row.get(4);

            let community_ap_id = match row.get(8) {
            let community_ap_id = match row.get(10) {
                Some(ap_id) => ap_id,
                None => {
                    // assume local (might be a problem?)


@@ 1030,9 1032,11 @@ async fn handler_posts_create_get(
            let post_info = crate::PostInfo {
                author: row.get(0),
                community: community_local_id,
                created: &row.get(4),
                created: &row.get(3),
                href: row.get(1),
                content_text: row.get(2),
                content_text: row.get(7),
                content_markdown: row.get(8),
                content_html: row.get(9),
                id: post_id,
                title: row.get(3),
            };