diff --git a/crates/api/src/site/admin_block_instance.rs b/crates/api/src/site/admin_block_instance.rs new file mode 100644 index 000000000..d68d28519 --- /dev/null +++ b/crates/api/src/site/admin_block_instance.rs @@ -0,0 +1,37 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::{AdminBlockInstance, AdminBlockInstanceResponse}, + utils::is_admin, +}; +use lemmy_db_schema::source::federation_blocklist::{FederationBlockList, FederationBlockListForm}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn block_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let instance_block_form = FederationBlockListForm { + instance_id: data.instance_id, + admin_person_id: Some(local_user_view.person.id), + reason: data.reason.clone(), + expires: data.expires, + updated: None, + }; + + if data.block { + FederationBlockList::block(&mut context.pool(), &instance_block_form).await?; + } else { + FederationBlockList::unblock(&mut context.pool(), &instance_block_form).await?; + } + + Ok(Json(AdminBlockInstanceResponse { + blocked: data.block, + })) +} diff --git a/crates/api/src/site/mod.rs b/crates/api/src/site/mod.rs index f18dea3d0..7c5b51cfb 100644 --- a/crates/api/src/site/mod.rs +++ b/crates/api/src/site/mod.rs @@ -1,3 +1,4 @@ +pub mod admin_block_instance; pub mod block; pub mod federated_instances; pub mod leave_admin; diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs index 8f5538566..d6aee8255 100644 --- a/crates/api/src/site/mod_log.rs +++ b/crates/api/src/site/mod_log.rs @@ -7,21 +7,10 @@ use lemmy_api_common::{ use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType}; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_moderator::structs::{ - AdminPurgeCommentView, - AdminPurgeCommunityView, - AdminPurgePersonView, - AdminPurgePostView, - ModAddCommunityView, - ModAddView, - ModBanFromCommunityView, - ModBanView, - ModFeaturePostView, - ModHideCommunityView, - ModLockPostView, - ModRemoveCommentView, - ModRemoveCommunityView, - ModRemovePostView, - ModTransferCommunityView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, + AdminPurgePostView, ModAddCommunityView, ModAddView, + ModBanFromCommunityView, ModBanView, ModFeaturePostView, ModHideCommunityView, ModLockPostView, + ModRemoveCommentView, ModRemoveCommunityView, ModRemovePostView, ModTransferCommunityView, ModlogListParams, }; use lemmy_utils::error::LemmyResult; @@ -121,6 +110,7 @@ pub async fn get_mod_log( admin_purged_communities, admin_purged_posts, admin_purged_comments, + admin_block_instance, ) = if data.community_id.is_none() { ( match type_ { @@ -161,6 +151,12 @@ pub async fn get_mod_log( } _ => Default::default(), }, + match type_ { + All | AdminBlockInstance if other_person_id.is_none() => { + AdminBlockInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, ) } else { Default::default() @@ -183,5 +179,6 @@ pub async fn get_mod_log( admin_purged_posts, admin_purged_comments, hidden_communities, + admin_block_instance, })) } diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 91c6151d7..502a24e6a 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -2,13 +2,7 @@ use crate::federate_retry_sleep_duration; use chrono::{DateTime, Utc}; use lemmy_db_schema::{ newtypes::{ - CommentId, - CommunityId, - InstanceId, - LanguageId, - PersonId, - PostId, - RegistrationApplicationId, + CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId, RegistrationApplicationId, }, source::{ community::Community, @@ -20,44 +14,20 @@ use lemmy_db_schema::{ person::Person, tagline::Tagline, }, - CommentSortType, - FederationMode, - ListingType, - ModlogActionType, - PostListingMode, - PostSortType, - RegistrationMode, - SearchType, + CommentSortType, FederationMode, ListingType, ModlogActionType, PostListingMode, PostSortType, + RegistrationMode, SearchType, }; use lemmy_db_views::structs::{ - CommentView, - LocalUserView, - PostView, - RegistrationApplicationView, - SiteView, + CommentView, LocalUserView, PostView, RegistrationApplicationView, SiteView, }; use lemmy_db_views_actor::structs::{ - CommunityFollowerView, - CommunityModeratorView, - CommunityView, - PersonView, + CommunityFollowerView, CommunityModeratorView, CommunityView, PersonView, }; use lemmy_db_views_moderator::structs::{ - AdminPurgeCommentView, - AdminPurgeCommunityView, - AdminPurgePersonView, - AdminPurgePostView, - ModAddCommunityView, - ModAddView, - ModBanFromCommunityView, - ModBanView, - ModFeaturePostView, - ModHideCommunityView, - ModLockPostView, - ModRemoveCommentView, - ModRemoveCommunityView, - ModRemovePostView, - ModTransferCommunityView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, + AdminPurgePostView, ModAddCommunityView, ModAddView, ModBanFromCommunityView, ModBanView, + ModFeaturePostView, ModHideCommunityView, ModLockPostView, ModRemoveCommentView, + ModRemoveCommunityView, ModRemovePostView, ModTransferCommunityView, }; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -183,6 +153,7 @@ pub struct GetModlogResponse { pub admin_purged_posts: Vec, pub admin_purged_comments: Vec, pub hidden_communities: Vec, + pub admin_block_instance: Vec, } #[skip_serializing_none] @@ -660,3 +631,20 @@ pub struct BlockInstance { pub struct BlockInstanceResponse { pub blocked: bool, } + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminBlockInstance { + pub instance_id: InstanceId, + pub block: bool, + pub reason: Option, + pub expires: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminBlockInstanceResponse { + pub blocked: bool, +} \ No newline at end of file diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 6c23adfb4..d2585ea43 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -19,8 +19,6 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ actor_language::SiteLanguage, - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, local_site::{LocalSite, LocalSiteUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_url_blocklist::LocalSiteUrlBlocklist, @@ -152,12 +150,6 @@ pub async fn update_site( .await .ok(); - // Replace the blocked and allowed instances - let allowed = data.allowed_instances.clone(); - FederationAllowList::replace(&mut context.pool(), allowed).await?; - let blocked = data.blocked_instances.clone(); - FederationBlockList::replace(&mut context.pool(), blocked).await?; - if let Some(url_blocklist) = data.blocked_urls.clone() { let parsed_urls = check_urls_are_valid(&url_blocklist)?; LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; diff --git a/crates/db_schema/src/impls/federation_blocklist.rs b/crates/db_schema/src/impls/federation_blocklist.rs index 2a6e0671d..e5c7cfcbf 100644 --- a/crates/db_schema/src/impls/federation_blocklist.rs +++ b/crates/db_schema/src/impls/federation_blocklist.rs @@ -1,49 +1,30 @@ use crate::{ schema::federation_blocklist, - source::{ - federation_blocklist::{FederationBlockList, FederationBlockListForm}, - instance::Instance, - }, + source::federation_blocklist::{FederationBlockList, FederationBlockListForm}, utils::{get_conn, DbPool}, }; +use diesel::{delete, ExpressionMethods, QueryDsl}; use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel_async::RunQueryDsl; impl FederationBlockList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { + pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationBlockListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_blocklist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) + insert_into(federation_blocklist::table) + .values(form) + .get_result::(conn) .await } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_blocklist::table) - .execute(conn) - .await + pub async fn unblock( + pool: &mut DbPool<'_>, + form: &FederationBlockListForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + delete( + federation_blocklist::table + .filter(federation_blocklist::dsl::instance_id.eq(form.instance_id)), + ) + .get_result(conn) + .await } } diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 7ee60cc1e..a67e1b0e3 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -215,6 +215,7 @@ pub enum ModlogActionType { AdminPurgeCommunity, AdminPurgePost, AdminPurgeComment, + AdminBlockInstance, } #[derive( diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 9e80c4693..0562765fa 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -282,6 +282,9 @@ diesel::table! { instance_id -> Int4, published -> Timestamptz, updated -> Nullable, + admin_person_id -> Nullable, + reason -> Nullable, + expires -> Nullable, } } @@ -955,6 +958,7 @@ diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); +diesel::joinable!(federation_blocklist -> person (admin_person_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); diesel::joinable!(instance_actions -> instance (instance_id)); diesel::joinable!(instance_actions -> person (person_id)); diff --git a/crates/db_schema/src/source/federation_blocklist.rs b/crates/db_schema/src/source/federation_blocklist.rs index 2176ce42d..367a97089 100644 --- a/crates/db_schema/src/source/federation_blocklist.rs +++ b/crates/db_schema/src/source/federation_blocklist.rs @@ -1,14 +1,15 @@ -use crate::newtypes::InstanceId; +use crate::newtypes::{InstanceId, PersonId}; #[cfg(feature = "full")] use crate::schema::federation_blocklist; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +use ts_rs::TS; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "full", - derive(Queryable, Selectable, Associations, Identifiable) + derive(TS, Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr( feature = "full", @@ -17,10 +18,15 @@ use std::fmt::Debug; #[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))] #[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] pub struct FederationBlockList { pub instance_id: InstanceId, pub published: DateTime, pub updated: Option>, + // TODO: would be good to make this mandatory but value doesnt exist for old entries + pub admin_person_id: Option, + pub reason: Option, + pub expires: Option>, } #[derive(Clone, Default)] @@ -29,4 +35,7 @@ pub struct FederationBlockList { pub struct FederationBlockListForm { pub instance_id: InstanceId, pub updated: Option>, + pub admin_person_id: Option, + pub reason: Option, + pub expires: Option>, } diff --git a/crates/db_views_moderator/src/admin_block_instance.rs b/crates/db_views_moderator/src/admin_block_instance.rs new file mode 100644 index 000000000..909cb6de6 --- /dev/null +++ b/crates/db_views_moderator/src/admin_block_instance.rs @@ -0,0 +1,57 @@ +use crate::structs::{AdminBlockInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{federation_blocklist, instance, person}, + utils::{functions::coalesce, get_conn, limit_and_offset, DbPool}, +}; + +impl AdminBlockInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = coalesce(federation_blocklist::admin_person_id, 0) + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = federation_blocklist::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + federation_blocklist::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(federation_blocklist::admin_person_id.eq(admin_person_id)); + }; + + // If a post or comment ID is given, then don't find any results + if params.post_id.is_some() || params.comment_id.is_some() { + return Ok(vec![]); + } + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(federation_blocklist::published.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/lib.rs b/crates/db_views_moderator/src/lib.rs index d3e7efffd..2da1555b5 100644 --- a/crates/db_views_moderator/src/lib.rs +++ b/crates/db_views_moderator/src/lib.rs @@ -1,4 +1,6 @@ #[cfg(feature = "full")] +pub mod admin_block_instance; +#[cfg(feature = "full")] pub mod admin_purge_comment_view; #[cfg(feature = "full")] pub mod admin_purge_community_view; diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs index 27ee82522..06177bfaa 100644 --- a/crates/db_views_moderator/src/structs.rs +++ b/crates/db_views_moderator/src/structs.rs @@ -5,6 +5,8 @@ use lemmy_db_schema::{ source::{ comment::Comment, community::Community, + federation_blocklist::FederationBlockList, + instance::Instance, moderator::{ AdminPurgeComment, AdminPurgeCommunity, @@ -233,6 +235,19 @@ pub struct AdminPurgePostView { pub community: Community, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a post. +pub struct AdminBlockInstanceView { + pub blocklist_entry: FederationBlockList, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/migrations/2024-11-19-142005_instance-block-mod-log/down.sql b/migrations/2024-11-19-142005_instance-block-mod-log/down.sql new file mode 100644 index 000000000..7444ade78 --- /dev/null +++ b/migrations/2024-11-19-142005_instance-block-mod-log/down.sql @@ -0,0 +1,3 @@ +alter table federation_blocklist drop column reason; +alter table federation_blocklist drop column expires; +alter table federation_blocklist drop column admin_person_id; \ No newline at end of file diff --git a/migrations/2024-11-19-142005_instance-block-mod-log/up.sql b/migrations/2024-11-19-142005_instance-block-mod-log/up.sql new file mode 100644 index 000000000..b8dca983c --- /dev/null +++ b/migrations/2024-11-19-142005_instance-block-mod-log/up.sql @@ -0,0 +1,3 @@ +alter table federation_blocklist add column admin_person_id int REFERENCES person(id) ON UPDATE CASCADE ON DELETE CASCADE; +alter table federation_blocklist add column reason text; +alter table federation_blocklist add column expires timestamptz; \ No newline at end of file diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 5900fe39f..8a370f99f 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -19,15 +19,7 @@ use lemmy_api_common::{ use lemmy_api_crud::post::create::send_webmention; use lemmy_db_schema::{ schema::{ - captcha_answer, - comment, - community, - community_actions, - instance, - person, - post, - received_activity, - sent_activity, + captcha_answer, comment, community, community_actions, federation_blocklist, instance, person, post, received_activity, sent_activity }, source::{ community::Community, @@ -439,6 +431,15 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { .await .inspect_err(|e| error!("Failed to remove community_ban expired rows: {e}")) .ok(); + + // TODO: need separate table for modlog, otherwise entry will disappear + diesel::delete( + federation_blocklist::table.filter(federation_blocklist::expires.lt(now().nullable())), + ) + .execute(&mut conn) + .await + .inspect_err(|e| error!("Failed to remove federation_blocklist expired rows: {e}")) + .ok(); } Err(e) => { error!("Failed to get connection from pool: {e}");