~vpzom/lotide

cbaa0b1f13de9832ca6a4aaafc6624455b64b1f2 — Colin Reeder 19 days ago 9c4466b
Add API for making posts sticky, and include in featured collection
A migrations/20210709224628_sticky/down.sql => migrations/20210709224628_sticky/down.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	ALTER TABLE post DROP COLUMN sticky;
COMMIT;

A migrations/20210709224628_sticky/up.sql => migrations/20210709224628_sticky/up.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	ALTER TABLE post ADD COLUMN sticky BOOLEAN NOT NULL DEFAULT (FALSE);
COMMIT;

M src/apub_util/mod.rs => src/apub_util/mod.rs +9 -0
@@ 190,6 190,15 @@ pub fn get_local_community_apub_id(
    res
}

pub fn get_local_community_featured_apub_id(
    community: CommunityLocalID,
    host_url_apub: &BaseURL,
) -> BaseURL {
    let mut res = get_local_community_apub_id(community, host_url_apub);
    res.path_segments_mut().push("featured");
    res
}

pub fn get_local_community_outbox_apub_id(
    community: CommunityLocalID,
    host_url_apub: &BaseURL,

M src/routes/api/communities.rs => src/routes/api/communities.rs +61 -24
@@ 5,6 5,7 @@ use crate::routes::api::{
use crate::{CommunityLocalID, PostLocalID, UserLocalID};
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use std::ops::Deref;
use std::sync::Arc;

#[derive(Serialize)]


@@ 747,7 748,7 @@ async fn route_unstable_communities_posts_list(

    let mut values: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = vec![&community_id, &limit];
    let sql: &str = &format!(
        "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, person.username, person.local, person.ap_id, person.avatar, (SELECT COUNT(*) FROM post_like WHERE post_like.post = post.id), (SELECT COUNT(*) FROM reply WHERE reply.post = post.id){} FROM post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = $1 AND post.approved=TRUE AND post.deleted=FALSE ORDER BY {} LIMIT $2",
        "SELECT post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, person.username, person.local, person.ap_id, person.avatar, (SELECT COUNT(*) FROM post_like WHERE post_like.post = post.id), (SELECT COUNT(*) FROM reply WHERE reply.post = post.id), post.sticky{} FROM post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = $1 AND post.approved=TRUE AND post.deleted=FALSE ORDER BY {} LIMIT $2",
        if let Some(user) = &include_your_for {
            values.push(user);
            ", EXISTS(SELECT 1 FROM post_like WHERE post=post.id AND person=$3)"


@@ 805,8 806,9 @@ async fn route_unstable_communities_posts_list(
                community: &community,
                replies_count_total: Some(row.get(12)),
                score: row.get(11),
                sticky: row.get(13),
                your_vote: if include_your_for.is_some() {
                    Some(if row.get(13) {
                    Some(if row.get(14) {
                        Some(crate::Empty {})
                    } else {
                        None


@@ 829,6 831,8 @@ async fn route_unstable_communities_posts_patch(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    use std::fmt::Write;

    let (community_id, post_id) = params;

    let lang = crate::get_lang_for_req(&req);


@@ 839,6 843,7 @@ async fn route_unstable_communities_posts_patch(
    #[derive(Deserialize)]
    struct CommunityPostEditBody {
        approved: Option<bool>,
        sticky: Option<bool>,
    }

    let body = hyper::body::to_bytes(req.into_body()).await?;


@@ 862,7 867,7 @@ async fn route_unstable_communities_posts_patch(

    let old_row = db
        .query_opt(
            "SELECT community, approved, local, ap_id FROM post WHERE id=$1",
            "SELECT community, approved, local, ap_id, sticky FROM post WHERE id=$1",
            &[&post_id],
        )
        .await?


@@ 881,6 886,7 @@ async fn route_unstable_communities_posts_patch(
    }

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

    let post_ap_id = if old_row.get(2) {
        crate::apub_util::get_local_post_apub_id(post_id, &ctx.host_url_apub).into()


@@ 888,28 894,59 @@ async fn route_unstable_communities_posts_patch(
        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?;
    let mut sql = "UPDATE post SET ".to_owned();
    let mut values: Vec<&(dyn postgres_types::ToSql + Sync)> = vec![&post_id];
    let mut any_changes = false;

        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,
                );
    if let Some(approved) = &body.approved {
        if !any_changes {
            any_changes = true;
        } else {
            sql.push(',');
        }
        values.push(approved);
        write!(sql, "approved=${}", values.len()).unwrap();
    }
    if let Some(sticky) = &body.sticky {
        if !any_changes {
            any_changes = true;
        } else {
            sql.push(',');
        }
        values.push(sticky);
        write!(sql, "sticky=${}", values.len()).unwrap();
    }

    if any_changes {
        sql.push_str(" WHERE id=$1");

        log::debug!("sql = {}", sql);

        db.execute(sql.deref(), &values).await?;

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

        if let Some(sticky) = body.sticky {
            if sticky != old_sticky {
                crate::apub_util::spawn_enqueue_send_new_community_update(community_id, ctx);
            }
        }
    }

M src/routes/api/mod.rs => src/routes/api/mod.rs +4 -2
@@ 245,6 245,7 @@ struct RespPostListPost<'a> {
    #[serde(skip_serializing_if = "Option::is_none")]
    replies_count_total: Option<i64>,
    score: i64,
    sticky: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    your_vote: Option<Option<crate::Empty>>,
}


@@ 1024,7 1025,7 @@ async fn route_unstable_misc_render_markdown(
}

fn common_posts_list_query(include_your_idx: Option<usize>) -> Cow<'static, str> {
    const BASE: &str = "post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, person.avatar, (SELECT COUNT(*) FROM post_like WHERE post_like.post = post.id), (SELECT COUNT(*) FROM reply WHERE reply.post = post.id)";
    const BASE: &str = "post.id, post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, person.avatar, (SELECT COUNT(*) FROM post_like WHERE post_like.post = post.id), (SELECT COUNT(*) FROM reply WHERE reply.post = post.id), post.sticky";
    match include_your_idx {
        None => BASE.into(),
        Some(idx) => format!(


@@ 1101,9 1102,10 @@ async fn handle_common_posts_list(
                created: &created.to_rfc3339(),
                community: &community,
                score: row.get(15),
                sticky: row.get(17),
                replies_count_total: Some(row.get(16)),
                your_vote: if include_your {
                    Some(if row.get(17) {
                    Some(if row.get(18) {
                        Some(crate::Empty {})
                    } else {
                        None

M src/routes/api/posts.rs => src/routes/api/posts.rs +2 -1
@@ 452,7 452,7 @@ async fn route_unstable_posts_get(

    let (row, your_vote) = futures::future::try_join(
        db.query_opt(
            "SELECT post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, (SELECT COUNT(*) FROM post_like WHERE post_like.post = $1), post.approved, person.avatar, post.local FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.id = $1",
            "SELECT post.author, post.href, post.content_text, post.title, post.created, post.content_html, community.id, community.name, community.local, community.ap_id, person.username, person.local, person.ap_id, (SELECT COUNT(*) FROM post_like WHERE post_like.post = $1), post.approved, person.avatar, post.local, post.sticky FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND post.id = $1",
            &[&post_id],
        )
        .map_err(crate::Error::from),


@@ 533,6 533,7 @@ async fn route_unstable_posts_get(
                community: &community,
                replies_count_total: None,
                score: row.get(13),
                sticky: row.get(17),
                your_vote,
            };


M src/routes/apub/communities.rs => src/routes/apub/communities.rs +76 -0
@@ 1,8 1,25 @@
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use activitystreams::prelude::*;
use serde_derive::{Deserialize, Serialize};
use std::ops::Deref;
use std::sync::Arc;

lazy_static::lazy_static! {
    static ref FEATURED_CONTEXT: activitystreams::base::AnyBase = activitystreams::base::AnyBase::from_arbitrary_json(serde_json::json!({
        "toot": "http://joinmastodon.org/ns#",
        "featured": {
            "@id": "toot:featured",
            "@type": "@id"
        }
    })).unwrap();
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FeaturedExtension {
    #[serde(skip_serializing_if = "Option::is_none")]
    featured: Option<url::Url>,
}

pub fn route_communities() -> crate::RouteNode<()> {
    crate::RouteNode::new().with_child_parse::<CommunityLocalID, _>(
        crate::RouteNode::new()


@@ 18,6 35,11 @@ pub fn route_communities() -> crate::RouteNode<()> {
                ),
            )
            .with_child(
                "featured",
                crate::RouteNode::new()
                    .with_handler_async("GET", handler_communities_featured_list),
            )
            .with_child(
                "followers",
                crate::RouteNode::new()
                    .with_handler_async("GET", handler_communities_followers_list)


@@ 129,6 151,7 @@ async fn handler_communities_get(
                activitystreams::context(),
                activitystreams::security(),
            ])
            .add_context(FEATURED_CONTEXT.clone())
            .set_id(community_ap_id.deref().clone())
            .set_name(name.as_ref())
            .set_summary(description);


@@ 155,6 178,12 @@ async fn handler_communities_get(
            })
            .set_preferred_username(name);

            let featured_ext = FeaturedExtension {
                featured: Some(crate::apub_util::get_local_community_featured_apub_id(community_id, &ctx.host_url_apub).into()),
            };

            let info = activitystreams_ext::Ext1::new(info, featured_ext);

            let key_id = format!(
                "{}/communities/{}#main-key",
                ctx.host_url_apub, community_id


@@ 233,6 262,53 @@ async fn handler_communities_comments_announce_get(
    }
}

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

    let rows = db
        .query(
            "SELECT id, local, ap_id FROM post WHERE community=$1 AND sticky",
            &[&community_id],
        )
        .await?;

    let items: Result<Vec<_>, _> = rows
        .into_iter()
        .map(|row| {
            use std::str::FromStr;

            if row.get(1) {
                Ok(crate::apub_util::get_local_post_apub_id(
                    PostLocalID(row.get(0)),
                    &ctx.host_url_apub,
                )
                .into())
            } else {
                url::Url::from_str(row.get(2))
            }
        })
        .collect();
    let items = items?;

    let mut body = activitystreams::collection::Collection::<
        activitystreams::collection::kind::CollectionType,
    >::new();
    body.set_context(activitystreams::context());
    body.set_total_items(items.len() as u64);
    body.set_many_items(items);

    let body = serde_json::to_vec(&body)?;

    Ok(hyper::Response::builder()
        .header(hyper::header::CONTENT_TYPE, crate::apub_util::ACTIVITY_TYPE)
        .body(body.into())?)
}

async fn handler_communities_followers_list(
    params: (CommunityLocalID,),
    ctx: Arc<crate::RouteContext>,