~vpzom/lotide

a82a487f135f59db1f05e1a24ef5a55679739e53 — Colin Reeder 4 months ago 3a46189
Add API for modifying post approval
M res/lang/en.ftl => res/lang/en.ftl +1 -0
@@ 14,6 14,7 @@ password_incorrect = Incorrect password
post_content_conflict = content_markdown and content_text are mutually exclusive
post_href_invalid = Specified URL is not valid
post_needs_content = Post must contain one of href, content_text, or content_markdown
post_not_in_community = That post is not in this community
post_not_yours = That's not your post
root = lotide is running. Note that lotide itself does not include a frontend, and you'll need to install one separately.
user_name_disallowed_chars = Username contains disallowed characters

M res/lang/eo.ftl => res/lang/eo.ftl +1 -0
@@ 14,6 14,7 @@ password_incorrect = Pasvorto malĝustas
post_content_conflict = content_markdown kaj content_text konfliktas
post_href_invalid = URL nevalidas.
post_needs_content = Poŝto devas enhavi unu el href, content_text, kaj content_markdown
post_not_in_community = Tiu poŝto ne estas en ĉi tiu komunumo
post_not_yours = Tio ne estas via poŝto
root = lotide funkcias. Notu: lotide ne enhavas klienton, kaj vi devos instali tiun aparte.
user_name_disallowed_chars = Uzantnomo enhavas malpermesitajn signojn

M src/apub_util.rs => src/apub_util.rs +61 -2
@@ 721,6 721,43 @@ pub fn local_community_post_announce_ap(
    Ok(announce)
}

pub fn local_community_post_announce_undo_ap(
    community_id: CommunityLocalID,
    post_local_id: PostLocalID,
    post_ap_id: url::Url,
    uuid: &uuid::Uuid,
    host_url_apub: &BaseURL,
) -> Result<activitystreams::activity::Undo, crate::Error> {
    let community_ap_id = get_local_community_apub_id(community_id, host_url_apub);

    let announce =
        local_community_post_announce_ap(community_id, post_local_id, post_ap_id, host_url_apub)?;

    let mut undo =
        activitystreams::activity::Undo::new(community_ap_id.clone(), announce.into_any_base()?);

    undo.set_context(activitystreams::context())
        .set_id({
            let mut res = community_ap_id.clone();
            res.path_segments_mut().extend(&[
                "posts",
                &post_local_id.to_string(),
                "announce",
                "undos",
                &uuid.to_string(),
            ]);
            res.into()
        })
        .set_to({
            let mut res = community_ap_id;
            res.path_segments_mut().push("followers");
            res
        })
        .set_cc(activitystreams::public());

    Ok(undo)
}

pub fn local_community_comment_announce_ap(
    community_id: CommunityLocalID,
    comment_local_id: CommentLocalID,


@@ 748,8 785,6 @@ pub fn spawn_announce_community_post(
    post_ap_id: url::Url,
    ctx: Arc<crate::RouteContext>,
) {
    // since post is borrowed, we can't move it
    // so we convert it to AP form before spawning
    match local_community_post_announce_ap(community, post_local_id, post_ap_id, &ctx.host_url_apub)
    {
        Err(err) => {


@@ 763,6 798,25 @@ pub fn spawn_announce_community_post(
    }
}

pub fn spawn_enqueue_send_community_post_announce_undo(
    community: CommunityLocalID,
    post: PostLocalID,
    post_ap_id: url::Url,
    ctx: Arc<crate::RouteContext>,
) {
    crate::spawn_task(async move {
        let undo = local_community_post_announce_undo_ap(
            community,
            post,
            post_ap_id,
            &uuid::Uuid::new_v4(),
            &ctx.host_url_apub,
        )?;

        enqueue_send_to_community_followers(community, undo, ctx).await
    });
}

pub fn spawn_announce_community_comment(
    community: CommunityLocalID,
    comment_local_id: CommentLocalID,


@@ 1863,6 1917,11 @@ pub async fn handle_undo(
        .await?;
    db.execute("DELETE FROM community_follow WHERE ap_id=$1", &[&object_id])
        .await?;
    db.execute(
        "UPDATE post SET approved=FALSE, approved_ap_id=NULL WHERE approved_ap_id=$1",
        &[&object_id],
    )
    .await?;

    Ok(())
}

M src/routes/api/communities.rs => src/routes/api/communities.rs +110 -1
@@ 504,6 504,109 @@ async fn route_unstable_communities_posts_list(
        .body(body.into())?)
}

async fn route_unstable_communities_posts_patch(
    params: (CommunityLocalID, PostLocalID),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id, post_id) = params;

    let lang = crate::get_lang_for_req(&req);
    let db = ctx.db_pool.get().await?;

    let user = crate::require_login(&req, &db).await?;

    #[derive(Deserialize)]
    struct CommunityPostEditBody {
        approved: Option<bool>,
    }

    let body = hyper::body::to_bytes(req.into_body()).await?;
    let body: CommunityPostEditBody = serde_json::from_slice(&body)?;

    ({
        let row = db
            .query_opt(
                "SELECT created_by FROM community WHERE id=$1",
                &[&community_id],
            )
            .await?;
        match row {
            None => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::NOT_FOUND,
                lang.tr("no_such_community", None).into_owned(),
            ))),
            Some(row) => {
                let created_by = row.get::<_, Option<_>>(0).map(UserLocalID);
                if created_by == Some(user) {
                    Ok(())
                } else {
                    Err(crate::Error::UserError(crate::simple_response(
                        hyper::StatusCode::FORBIDDEN,
                        lang.tr("community_edit_denied", None).into_owned(),
                    )))
                }
            }
        }
    })?;

    let old_row = db
        .query_opt(
            "SELECT community, approved, local, ap_id FROM post WHERE id=$1",
            &[&post_id],
        )
        .await?
        .ok_or_else(|| {
            crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::NOT_FOUND,
                lang.tr("no_such_post", None).into_owned(),
            ))
        })?;

    if community_id != CommunityLocalID(old_row.get(0)) {
        return Err(crate::Error::UserError(crate::simple_response(
            hyper::StatusCode::NOT_FOUND,
            lang.tr("post_not_in_community", None).into_owned(),
        )));
    }

    let old_approved: bool = old_row.get(1);

    let post_ap_id = if old_row.get(2) {
        crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub).into()
    } else {
        std::str::FromStr::from_str(old_row.get(3))?
    };

    if let Some(approved) = body.approved {
        db.execute(
            "UPDATE post SET approved=$1 WHERE id=$2",
            &[&approved, &post_id],
        )
        .await?;

        if approved != old_approved {
            if approved {
                crate::apub_util::spawn_announce_community_post(
                    community_id,
                    post_id,
                    post_ap_id,
                    ctx,
                );
            } else {
                crate::apub_util::spawn_enqueue_send_community_post_announce_undo(
                    community_id,
                    post_id,
                    post_ap_id,
                    ctx,
                );
            }
        }
    }

    Ok(crate::empty_response())
}

pub fn route_communities() -> crate::RouteNode<()> {
    crate::RouteNode::new()
        .with_handler_async("GET", route_unstable_communities_list)


@@ 525,7 628,13 @@ pub fn route_communities() -> crate::RouteNode<()> {
                .with_child(
                    "posts",
                    crate::RouteNode::new()
                        .with_handler_async("GET", route_unstable_communities_posts_list),
                        .with_handler_async("GET", route_unstable_communities_posts_list)
                        .with_child_parse::<PostLocalID, _>(
                            crate::RouteNode::new().with_handler_async(
                                "PATCH",
                                route_unstable_communities_posts_patch,
                            ),
                        ),
                ),
        )
}

M src/routes/apub/communities.rs => src/routes/apub/communities.rs +53 -2
@@ 56,7 56,16 @@ pub fn route_communities() -> crate::RouteNode<()> {
                    crate::RouteNode::new().with_child(
                        "announce",
                        crate::RouteNode::new()
                            .with_handler_async("GET", handler_communities_posts_announce_get),
                            .with_handler_async("GET", handler_communities_posts_announce_get)
                            .with_child(
                                "undos",
                                crate::RouteNode::new().with_child_parse::<uuid::Uuid, _>(
                                    crate::RouteNode::new().with_handler_async(
                                        "GET",
                                        handler_communities_posts_announce_undos_get,
                                    ),
                                ),
                            ),
                    ),
                ),
            )


@@ 581,7 590,7 @@ async fn handler_communities_posts_announce_get(
    let db = ctx.db_pool.get().await?;

    match db.query_opt(
        "SELECT post.id, post.local, post.ap_id, community.local FROM post, community WHERE post.community = community.id AND id=$1 AND community=$2",
        "SELECT post.id, post.local, post.ap_id, community.local FROM post, community WHERE post.community = community.id AND id=$1 AND community=$2 AND approved",
        &[&post_id, &community_id],
    ).await? {
        None => {


@@ 626,6 635,48 @@ async fn handler_communities_posts_announce_get(
    }
}

async fn handler_communities_posts_announce_undos_get(
    params: (CommunityLocalID, PostLocalID, uuid::Uuid),
    ctx: Arc<crate::RouteContext>,
    _req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id, post_id, undo_id) = params;
    let db = ctx.db_pool.get().await?;

    match db.query_opt(
        "SELECT post.local, post.ap_id, community.local FROM post, community WHERE post.community = community.id AND post.id = $1 AND community.id = $2 AND NOT post.approved",
        &[&post_id, &community_id],
    ).await? {
        None => {
            Ok(crate::simple_response(
                    hyper::StatusCode::NOT_FOUND,
                    "No such undo",
            ))
        },
        Some(row) => {
            let community_local = row.get(2);
            if community_local {
                let post_ap_id = if row.get(0) {
                    crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub).into()
                } else {
                    std::str::FromStr::from_str(row.get(1))?
                };
                let body = crate::apub_util::local_community_post_announce_undo_ap(community_id, post_id, post_ap_id, &undo_id, &ctx.host_url_apub)?;
                let body = serde_json::to_vec(&body)?;

                Ok(hyper::Response::builder()
                   .header(hyper::header::CONTENT_TYPE, crate::apub_util::ACTIVITY_TYPE)
                   .body(body.into())?)
            } else {
                Ok(crate::simple_response(
                    hyper::StatusCode::BAD_REQUEST,
                    "Requested community is not owned by this instance",
                ))
            }
        }
    }
}

async fn handler_communities_updates_get(
    params: (CommunityLocalID, uuid::Uuid),
    ctx: Arc<crate::RouteContext>,