diff --git a/Cargo.lock b/Cargo.lock index 60c4fdda2..82c9afa6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2555,7 +2555,6 @@ dependencies = [ "captcha", "chrono", "elementtree", - "itertools 0.12.0", "lemmy_api_common", "lemmy_db_schema", "lemmy_db_views", diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 5e2700464..21b07420a 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -33,7 +33,6 @@ anyhow = { workspace = true } tracing = { workspace = true } chrono = { workspace = true } url = { workspace = true } -itertools = { workspace = true } wav = "1.0.0" sitemap-rs = "0.2.0" totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] } diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 62f5a6ea0..f92fcb850 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -3,19 +3,26 @@ use actix_web::{http::header::Header, HttpRequest}; use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine}; use captcha::Captcha; -use itertools::Itertools; use lemmy_api_common::{ claims::Claims, community::BanFromCommunity, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME}, + utils::{check_expire_time, check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME}, }; -use lemmy_db_schema::source::{ - comment::Comment, - local_site::LocalSite, - person::Person, - post::Post, +use lemmy_db_schema::{ + source::{ + community::{ + CommunityFollower, + CommunityFollowerForm, + CommunityPersonBan, + CommunityPersonBanForm, + }, + local_site::LocalSite, + moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + person::Person, + }, + traits::{Bannable, Crud, Followable}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ @@ -150,35 +157,77 @@ pub(crate) fn build_totp_2fa( .with_lemmy_type(LemmyErrorType::CouldntGenerateTotp) } -/// Since removals and bans are only federated for local users, -/// you also need to send bans for their content to local communities. +/// Site bans are only federated for local users. +/// This is a problem, because site-banning non-local users will still leave content +/// they've posted to our local communities, on other servers. +/// +/// So when doing a site ban for a non-local user, you need to federate/send a +/// community ban for every local community they've participated in. /// See https://github.com/LemmyNet/lemmy/issues/4118 #[tracing::instrument(skip_all)] -pub(crate) async fn send_bans_and_removals_to_local_communities( +pub(crate) async fn ban_nonlocal_user_from_local_communities( local_user_view: &LocalUserView, target: &Person, + ban: bool, reason: &Option, remove_data: &Option, + expires: &Option, context: &Data, ) -> LemmyResult<()> { - let posts_community_ids = - Post::list_creators_local_community_ids(&mut context.pool(), target.id).await?; - let comments_community_ids = - Comment::list_creators_local_community_ids(&mut context.pool(), target.id).await?; - - let ids = [posts_community_ids, comments_community_ids] - .concat() - .into_iter() - .unique(); + let ids = Person::list_local_community_ids(&mut context.pool(), target.id).await?; for community_id in ids { + let expires_dt = check_expire_time(*expires)?; + + // Ban / unban them from our local communities + let community_user_ban_form = CommunityPersonBanForm { + community_id, + person_id: target.id, + expires: Some(expires_dt), + }; + + if ban { + CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form) + .await + .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned) + .ok(); + + // Also unsubscribe them from the community, if they are subscribed + let community_follower_form = CommunityFollowerForm { + community_id, + person_id: target.id, + pending: false, + }; + + CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) + .await + .ok(); + } else { + CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form) + .await + .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?; + } + + // Mod tables + let form = ModBanFromCommunityForm { + mod_person_id: local_user_view.person.id, + other_person_id: target.id, + community_id, + reason: reason.clone(), + banned: Some(ban), + expires: expires_dt, + }; + + ModBanFromCommunity::create(&mut context.pool(), &form).await?; + + // Federate the ban from community let ban_from_community = BanFromCommunity { community_id, person_id: target.id, - ban: true, - remove_data: *remove_data, + ban, reason: reason.clone(), - expires: None, + remove_data: *remove_data, + expires: *expires, }; ActivityChannel::submit_activity( diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 75cd13fd5..308f1db17 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -1,4 +1,4 @@ -use crate::send_bans_and_removals_to_local_communities; +use crate::ban_nonlocal_user_from_local_communities; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ @@ -72,14 +72,18 @@ pub async fn ban_from_site( let person_view = PersonView::read(&mut context.pool(), person.id).await?; - send_bans_and_removals_to_local_communities( - &local_user_view, - &person, - &data.reason, - &data.remove_data, - &context, - ) - .await?; + if !person.local { + ban_nonlocal_user_from_local_communities( + &local_user_view, + &person, + data.ban, + &data.reason, + &data.remove_data, + &data.expires, + &context, + ) + .await?; + } ActivityChannel::submit_activity( SendActivityData::BanFromSite { diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 524181fa8..979c3b721 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -1,6 +1,6 @@ use crate::{ - newtypes::{CommentId, CommunityId, DbUrl, PersonId}, - schema::{comment, community, post}, + newtypes::{CommentId, DbUrl, PersonId}, + schema::comment, source::comment::{ Comment, CommentInsertForm, @@ -17,7 +17,6 @@ use diesel::{ dsl::{insert_into, sql_query}, result::Error, ExpressionMethods, - JoinOnDsl, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -56,23 +55,6 @@ impl Comment { .await } - /// Lists local community ids for all comments for a given creator. - pub async fn list_creators_local_community_ids( - pool: &mut DbPool<'_>, - for_creator_id: PersonId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - comment::table - .inner_join(post::table) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .filter(community::local.eq(true)) - .filter(comment::creator_id.eq(for_creator_id)) - .select(community::id) - .distinct() - .load::(conn) - .await - } - pub async fn create( pool: &mut DbPool<'_>, comment_form: &CommentInsertForm, diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 9fb1ee1c5..0de7c32f5 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{instance, local_user, person, person_follower}, + schema::{comment, community, instance, local_user, person, person_follower, post}, source::person::{ Person, PersonFollower, @@ -11,7 +11,7 @@ use crate::{ traits::{ApubActor, Crud, Followable}, utils::{functions::lower, get_conn, naive_now, DbPool}, }; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{dsl::insert_into, result::Error, CombineDsl, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; #[async_trait] @@ -84,6 +84,29 @@ impl Person { .get_result::(conn) .await } + + /// Lists local community ids for all posts and comments for a given creator. + pub async fn list_local_community_ids( + pool: &mut DbPool<'_>, + for_creator_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + comment::table + .inner_join(post::table) + .inner_join(community::table.on(post::community_id.eq(community::id))) + .filter(community::local.eq(true)) + .filter(comment::creator_id.eq(for_creator_id)) + .select(community::id) + .union( + post::table + .inner_join(community::table) + .filter(community::local.eq(true)) + .filter(post::creator_id.eq(for_creator_id)) + .select(community::id), + ) + .load::(conn) + .await + } } impl PersonInsertForm { diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index a2c8c5584..82f82d1b7 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, PersonId, PostId}, - schema::{community, post}, + schema::post, source::post::{ Post, PostInsertForm, @@ -236,22 +236,6 @@ impl Post { .get_results::(conn) .await } - - /// Lists local community ids for all posts for a given creator. - pub async fn list_creators_local_community_ids( - pool: &mut DbPool<'_>, - for_creator_id: PersonId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - post::table - .inner_join(community::table) - .filter(community::local.eq(true)) - .filter(post::creator_id.eq(for_creator_id)) - .select(community::id) - .distinct() - .load::(conn) - .await - } } #[async_trait]