~vpzom/lotide

958099d68435a5b3a3116d43bc285b29da392c05 — Colin Reeder 10 days ago a1ff72a
Only allow removing moderators newer than you
A migrations/20210112062722_more-timestamps/down.sql => migrations/20210112062722_more-timestamps/down.sql +4 -0
@@ 0,0 1,4 @@
BEGIN;
	ALTER TABLE community DROP COLUMN created_local;
	ALTER TABLE community_moderator DROP COLUMN created_local;
COMMIT;

A migrations/20210112062722_more-timestamps/up.sql => migrations/20210112062722_more-timestamps/up.sql +4 -0
@@ 0,0 1,4 @@
BEGIN;
	ALTER TABLE community ADD COLUMN created_local TIMESTAMPTZ;
	ALTER TABLE community_moderator ADD COLUMN created_local TIMESTAMPTZ;
COMMIT;

M res/lang/en.ftl => res/lang/en.ftl +1 -0
@@ 3,6 3,7 @@ comment_empty = Comment may not be empty
comment_not_yours = That's not your comment
community_edit_denied = You are not authorized to modify this community
community_moderators_not_local = Community moderators can only be listed for local communities
community_moderators_remove_must_be_older = You can only remove moderators that are newer than you
community_name_disallowed_chars = Community name contains disallowed characters
email_content_forgot_password = Hi { $username }, if you requested a password reset from lotide, use this code: { $key }
email_not_configured = Email is not configured on this server

M src/apub_util/ingest.rs => src/apub_util/ingest.rs +2 -2
@@ 184,7 184,7 @@ pub async fn ingest_object(
                        if let Some(row) = row {
                            let local: bool = row.get(0);
                            if local {
                                db.execute("INSERT INTO community_follow (community, follower, local, ap_id, accepted) VALUES ($1, $2, FALSE, $3, TRUE) ON CONFLICT (community, follower) DO NOTHING", &[&community_id, &follower_local_id, &activity_ap_id.as_str()]).await?;
                                db.execute("INSERT INTO community_follow (community, follower, local, ap_id, accepted, created_local) VALUES ($1, $2, FALSE, $3, TRUE, current_timestamp) ON CONFLICT (community, follower) DO NOTHING", &[&community_id, &follower_local_id, &activity_ap_id.as_str()]).await?;

                                crate::apub_util::spawn_enqueue_send_community_follow_accept(
                                    community_id,


@@ 235,7 235,7 @@ pub async fn ingest_object(
                .and_then(|key| key.signature_algorithm.as_deref());

            let id = CommunityLocalID(db.query_one(
                "INSERT INTO community (name, local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description_html) VALUES ($1, FALSE, $2, $3, $4, $5, $6, $7) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description_html=$7 RETURNING id",
                "INSERT INTO community (name, local, ap_id, ap_inbox, ap_shared_inbox, public_key, public_key_sigalg, description_html, created_local) VALUES ($1, FALSE, $2, $3, $4, $5, $6, $7, current_timestamp) ON CONFLICT (ap_id) DO UPDATE SET ap_inbox=$3, ap_shared_inbox=$4, public_key=$5, public_key_sigalg=$6, description_html=$7 RETURNING id",
                &[&name, &ap_id.as_str(), &inbox, &shared_inbox, &public_key, &public_key_sigalg, &description_html],
            ).await?.get(0));


M src/routes/api/communities.rs => src/routes/api/communities.rs +73 -29
@@ 27,6 27,13 @@ struct RespYourFollowInfo {
    accepted: bool,
}

#[derive(Serialize)]
struct RespModeratorInfo<'a> {
    #[serde(flatten)]
    base: RespMinimalAuthorInfo<'a>,
    moderator_since: Option<String>,
}

fn get_community_description_fields<'a>(
    description_text: &'a str,
    description_html: Option<&'a str>,


@@ 210,7 217,7 @@ async fn route_unstable_communities_create(

        let row = trans
            .query_one(
                "INSERT INTO community (name, local, private_key, public_key, created_by) VALUES ($1, TRUE, $2, $3, $4) RETURNING id",
                "INSERT INTO community (name, local, private_key, public_key, created_by, created_local) VALUES ($1, TRUE, $2, $3, $4, current_timestamp) RETURNING id",
                &[&body.name, &private_key, &public_key, &user.raw()],
            )
            .await?;


@@ 219,7 226,7 @@ async fn route_unstable_communities_create(

        trans
            .execute(
                "INSERT INTO community_moderator (community, person) VALUES ($1, $2)",
                "INSERT INTO community_moderator (community, person, created_local) VALUES ($1, $2, current_timestamp)",
                &[&community_id, &user],
            )
            .await?;


@@ 464,7 471,7 @@ async fn route_unstable_communities_moderators_list(
    })?;

    let rows = db.query(
        "SELECT id, username, local, ap_id, avatar FROM person WHERE id IN (SELECT person FROM community_moderator WHERE community=$1)",
        "SELECT person.id, person.username, person.local, person.ap_id, person.avatar, community_moderator.created_local FROM person, community_moderator WHERE person.id = community_moderator.person AND community_moderator.community = $1 ORDER BY community_moderator.created_local ASC NULLS FIRST",
        &[&community_id],
    ).await?;



@@ 475,15 482,21 @@ async fn route_unstable_communities_moderators_list(
            let local = row.get(2);
            let ap_id = row.get(3);

            RespMinimalAuthorInfo {
                id,
                username: Cow::Borrowed(row.get(1)),
                local,
                host: crate::get_actor_host_or_unknown(local, ap_id, &ctx.local_hostname),
                remote_url: ap_id.map(|x| x.into()),
                avatar: row.get::<_, Option<&str>>(4).map(|url| RespAvatarInfo {
                    url: ctx.process_avatar_href(url, id),
                }),
            let moderator_since: Option<chrono::DateTime<chrono::offset::Utc>> = row.get(5);

            RespModeratorInfo {
                base: RespMinimalAuthorInfo {
                    id,
                    username: Cow::Borrowed(row.get(1)),
                    local,
                    host: crate::get_actor_host_or_unknown(local, ap_id, &ctx.local_hostname),
                    remote_url: ap_id.map(|x| x.into()),
                    avatar: row.get::<_, Option<&str>>(4).map(|url| RespAvatarInfo {
                        url: ctx.process_avatar_href(url, id),
                    }),
                },

                moderator_since: moderator_since.map(|time| time.to_rfc3339()),
            }
        })
        .collect();


@@ 545,7 558,7 @@ async fn route_unstable_communities_moderators_add(
    })?;

    db.execute(
        "INSERT INTO community_moderator (community, person) VALUES ($1, $2)",
        "INSERT INTO community_moderator (community, person, created_local) VALUES ($1, $2, current_timestamp)",
        &[&community_id, &user_id],
    )
    .await?;


@@ 560,34 573,65 @@ async fn route_unstable_communities_moderators_remove(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id, user_id) = params;

    let db = ctx.db_pool.get().await?;
    let mut db = ctx.db_pool.get().await?;

    let lang = crate::get_lang_for_req(&req);
    let login_user = crate::require_login(&req, &db).await?;

    ({
        let row = db
            .query_opt(
                "SELECT 1 FROM community_moderator WHERE community=$1 AND person=$2",
                &[&community_id, &login_user],
            )
            .await?;
        match row {
    let self_moderator_row = db
        .query_opt(
            "SELECT created_local FROM community_moderator WHERE community=$1 AND person=$2",
            &[&community_id, &login_user],
        )
        .await?;

    let self_moderator_since: Option<chrono::DateTime<chrono::offset::Utc>> = ({
        match self_moderator_row {
            None => Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::FORBIDDEN,
                lang.tr("must_be_moderator", None).into_owned(),
            ))),
            Some(_) => Ok(()),
            Some(row) => Ok(row.get(0)),
        }
    })?;

    db.execute(
        "DELETE FROM community_moderator WHERE community=$1 AND person=$2",
        &[&community_id, &user_id],
    )
    .await?;
    {
        let trans = db.transaction().await?;
        let row = trans.query_opt(
            "DELETE FROM community_moderator WHERE community=$1 AND person=$2 RETURNING (created_local >= $3)",
            &[&community_id, &user_id, &self_moderator_since],
        )
        .await?;

    Ok(crate::empty_response())
        let is_allowed = match self_moderator_since {
            None => true, // self was moderator before timestamps existed, can remove anyone
            Some(_) => {
                match row {
                    None => true, // was already removed, ok
                    Some(row) => {
                        let res: Option<bool> = row.get(0);
                        match res {
                            None => false, // other has no timestamp, must be older
                            Some(value) => value,
                        }
                    }
                }
            }
        };

        if is_allowed {
            trans.commit().await?;
            Ok(crate::empty_response())
        } else {
            trans.rollback().await?;

            Err(crate::Error::UserError(crate::simple_response(
                hyper::StatusCode::FORBIDDEN,
                lang.tr("community_moderators_remove_must_be_older", None)
                    .into_owned(),
            )))
        }
    }
}

async fn route_unstable_communities_unfollow(