diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index e4ab91b080..3ec3569eee 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -28,11 +28,7 @@ use lemmy_db_schema::{ utils::DbPool, }; use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; -use lemmy_db_views_actor::structs::{ - CommunityModeratorView, - CommunityPersonBanView, - CommunityView, -}; +use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityPersonBanView, CommunityView, SitePersonBanView}; use lemmy_utils::{ email::{send_email, translations::Lang}, error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, @@ -51,6 +47,7 @@ use std::{collections::HashSet, time::Duration}; use tracing::warn; use url::{ParseError, Url}; use urlencoding::encode; +use lemmy_db_views::structs::SiteView; pub static AUTH_COOKIE_NAME: &str = "jwt"; #[cfg(debug_assertions)] @@ -204,11 +201,26 @@ async fn check_community_ban( community_id: CommunityId, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - // check if user was banned from site or community - let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?; - if is_banned { + // check if user was banned from community + let is_banned_from_community = CommunityPersonBanView::get(pool, person.id, community_id).await?; + if is_banned_from_community { Err(LemmyErrorType::BannedFromCommunity)? } + + + // for remote communities, check if the user was banned on the host site + let community_view = CommunityView::read(pool, community_id, None, false).await?; + if (!community_view.community.local) { + + let instance_id = community_view.community.instance_id; + if let Some(site) = Site::read_from_instance_id(pool, instance_id).await? { + let is_banned_from_site_of_community = SitePersonBanView::get(pool, person.id, site.id).await?; + if is_banned_from_site_of_community { + Err(LemmyErrorType::BannedFromCommunity)? + } + } + } + Ok(()) } diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index a2a1f25bf0..04e1d39a34 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -35,7 +35,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, - person::{Person, PersonUpdateForm}, + site::{SitePersonBan, SitePersonBanForm}, }, traits::{Bannable, Crud, Followable}, }; @@ -155,19 +155,26 @@ impl ActivityHandler for BlockUser { let blocked_person = self.object.dereference(context).await?; let target = self.target.dereference(context).await?; match target { - SiteOrCommunity::Site(_site) => { - let blocked_person = Person::update( - &mut context.pool(), - blocked_person.id, - &PersonUpdateForm { - banned: Some(true), - ban_expires: Some(expires), - ..Default::default() - }, - ) - .await?; + SiteOrCommunity::Site(site) => { + let site_user_ban_form = SitePersonBanForm { + site_id: site.id, + person_id: blocked_person.id, + expires: Some(expires), + }; + SitePersonBan::ban(&mut context.pool(), &site_user_ban_form).await?; if self.remove_data.unwrap_or(false) { - remove_user_data(blocked_person.id, context).await?; + let user_banned_on_home_instance = verify_domains_match(&site.id(), self.actor.inner())? + && verify_domains_match(&site.id(), self.object.inner())?; + if user_banned_on_home_instance { + remove_user_data(blocked_person.id, context).await?; + } else { + // Currently, remote site bans federate data removal through corresponding community + // bans, because remote site bans were not even stored unless they came from the user's + // home instance. We can continue to use community bans to federate data removal for + // backwards compatibility, initially but at some point, when most instances have been + // upgraded to have the logic of storing remote site bans, we can start doing data + // removal here & remove the logic of federating community bans on remote site bans. + } } // write mod log diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 756d0a149c..2aab775226 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -23,7 +23,7 @@ use lemmy_db_schema::{ activity::ActivitySendTargets, community::{CommunityPersonBan, CommunityPersonBanForm}, moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, - person::{Person, PersonUpdateForm}, + site::{SitePersonBan, SitePersonBanForm}, }, traits::{Bannable, Crud}, }; @@ -102,17 +102,13 @@ impl ActivityHandler for UndoBlockUser { let mod_person = self.actor.dereference(context).await?; let blocked_person = self.object.object.dereference(context).await?; match self.object.target.dereference(context).await? { - SiteOrCommunity::Site(_site) => { - let blocked_person = Person::update( - &mut context.pool(), - blocked_person.id, - &PersonUpdateForm { - banned: Some(false), - ban_expires: Some(expires), - ..Default::default() - }, - ) - .await?; + SiteOrCommunity::Site(site) => { + let site_user_ban_form = SitePersonBanForm { + site_id: site.id, + person_id: blocked_person.id, + expires: None, + }; + SitePersonBan::unban(&mut context.pool(), &site_user_ban_form).await?; // write mod log let form = ModBanForm { diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index 7e9329afbc..5f6cf67ab6 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -3,9 +3,9 @@ use crate::{ schema::site::dsl::{actor_id, id, instance_id, site}, source::{ actor_language::SiteLanguage, - site::{Site, SiteInsertForm, SiteUpdateForm}, + site::{Site, SiteInsertForm, SitePersonBan, SitePersonBanForm, SiteUpdateForm}, }, - traits::Crud, + traits::{Bannable, Crud}, utils::{get_conn, DbPool}, }; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl}; @@ -100,3 +100,35 @@ impl Site { url } } + +#[async_trait] +impl Bannable for SitePersonBan { + type Form = SitePersonBanForm; + async fn ban( + pool: &mut DbPool<'_>, + site_person_ban_form: &SitePersonBanForm, + ) -> Result { + use crate::schema::site_person_ban::dsl::{person_id, site_id, site_person_ban}; + let conn = &mut get_conn(pool).await?; + insert_into(site_person_ban) + .values(site_person_ban_form) + .on_conflict((site_id, person_id)) + .do_update() + .set(site_person_ban_form) + .get_result::(conn) + .await + } + + async fn unban( + pool: &mut DbPool<'_>, + site_person_ban_form: &SitePersonBanForm, + ) -> Result { + use crate::schema::site_person_ban::dsl::site_person_ban; + let conn = &mut get_conn(pool).await?; + diesel::delete( + site_person_ban.find((site_person_ban_form.person_id, site_person_ban_form.site_id)), + ) + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 408ed05403..64c6a4573e 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -926,6 +926,15 @@ diesel::table! { } } +diesel::table! { + site_person_ban (person_id, site_id) { + site_id -> Int4, + person_id -> Int4, + published -> Timestamptz, + expires -> Nullable, + } +} + diesel::table! { tagline (id) { id -> Int4, @@ -1028,6 +1037,8 @@ diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); +diesel::joinable!(site_person_ban -> person (person_id)); +diesel::joinable!(site_person_ban -> site (site_id)); diesel::joinable!(tagline -> local_site (local_site_id)); diesel::allow_tables_to_appear_in_same_query!( @@ -1102,5 +1113,6 @@ diesel::allow_tables_to_appear_in_same_query!( site, site_aggregates, site_language, + site_person_ban, tagline, ); diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index 40ba14f967..42555c75ff 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{DbUrl, InstanceId, SiteId}; +use crate::newtypes::{CommunityId, DbUrl, InstanceId, PersonId, SiteId}; #[cfg(feature = "full")] use crate::schema::site; use chrono::{DateTime, Utc}; @@ -84,3 +84,28 @@ pub struct SiteUpdateForm { pub public_key: Option, pub content_warning: Option>, } + +#[derive(PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, Associations) +)] +#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::site::Site)))] +#[cfg_attr(feature = "full", diesel(table_name = site_person_ban))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, site_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct SitePersonBan { + pub site_id: SiteId, + pub person_id: PersonId, + pub published: DateTime, + pub expires: Option>, +} + +#[derive(Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = site_person_ban))] +pub struct SitePersonBanForm { + pub site_id: SiteId, + pub person_id: PersonId, + pub expires: Option>>, +} diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views_actor/src/lib.rs index e9f8e41890..ef181cd0cc 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -18,4 +18,6 @@ pub mod person_block_view; pub mod person_mention_view; #[cfg(feature = "full")] pub mod person_view; +#[cfg(feature = "full")] +pub mod site_person_ban_view; pub mod structs; diff --git a/crates/db_views_actor/src/site_person_ban_view.rs b/crates/db_views_actor/src/site_person_ban_view.rs new file mode 100644 index 0000000000..55da6b5385 --- /dev/null +++ b/crates/db_views_actor/src/site_person_ban_view.rs @@ -0,0 +1,25 @@ +use crate::structs::SitePersonBanView; +use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::{PersonId, SiteId}, + schema::site_person_ban, + utils::{get_conn, DbPool}, +}; + +impl SitePersonBanView { + pub async fn get( + pool: &mut DbPool<'_>, + from_person_id: PersonId, + from_site_id: SiteId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + select(exists( + site_person_ban::table + .filter(site_person_ban::site_id.eq(from_site_id)) + .filter(site_person_ban::person_id.eq(from_person_id)), + )) + .get_result::(conn) + .await + } +} diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index f25662f7b2..384af283a9 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -149,3 +149,12 @@ pub struct PersonView { pub counts: PersonAggregates, pub is_admin: bool, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A site person ban. +pub struct SitePersonBanView { + pub site: Site, + pub person: Person, +} diff --git a/migrations/2024-03-26-131042_site_person_ban/down.sql b/migrations/2024-03-26-131042_site_person_ban/down.sql new file mode 100644 index 0000000000..33e9a4babd --- /dev/null +++ b/migrations/2024-03-26-131042_site_person_ban/down.sql @@ -0,0 +1 @@ +DROP TABLE site_person_ban; \ No newline at end of file diff --git a/migrations/2024-03-26-131042_site_person_ban/up.sql b/migrations/2024-03-26-131042_site_person_ban/up.sql new file mode 100644 index 0000000000..34dd4ad797 --- /dev/null +++ b/migrations/2024-03-26-131042_site_person_ban/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE site_person_ban +( + site_id INTEGER NOT NULL + REFERENCES site + ON UPDATE CASCADE ON DELETE CASCADE, + person_id INTEGER NOT NULL + REFERENCES person + ON UPDATE CASCADE ON DELETE CASCADE, + published TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + expires TIMESTAMP WITH TIME ZONE, + PRIMARY KEY (person_id, site_id) +); + diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 491902fb3f..2364c57986 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -21,6 +21,7 @@ use lemmy_db_schema::{ post, received_activity, sent_activity, + site_person_ban, }, source::{ instance::{Instance, InstanceForm}, @@ -441,8 +442,14 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { ) .execute(&mut conn) .await - .inspect_err(|e| error!("Failed to remove community_ban expired rows: {e}")) + .inspect_err(|e| error!("Failed to remove community_person_ban expired rows: {e}")) .ok(); + + diesel::delete(site_person_ban::table.filter(site_person_ban::expires.lt(now().nullable()))) + .execute(&mut conn) + .await + .inspect_err(|e| error!("Failed to remove site_person_ban expired rows: {e}")) + .ok(); } Err(e) => { error!("Failed to get connection from pool: {e}");