From c2c82aa8437b4d1c7f2910d7772b6fd17c7703a9 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Sat, 24 Jan 2026 09:53:12 +0000 Subject: [PATCH 01/16] Remove comment thread API --- crates/api/api/src/site/purge/comment.rs | 1 + crates/api/api_crud/src/comment/remove.rs | 131 +++++++++++++------ crates/api/api_utils/src/send_activity.rs | 1 + crates/api/api_utils/src/utils.rs | 31 ++++- crates/db_schema/src/impls/comment.rs | 13 ++ crates/db_schema/src/impls/comment_report.rs | 24 ++++ crates/db_views/comment/src/api.rs | 3 + 7 files changed, 159 insertions(+), 45 deletions(-) diff --git a/crates/api/api/src/site/purge/comment.rs b/crates/api/api/src/site/purge/comment.rs index b8b43b270e..a5910a4d31 100644 --- a/crates/api/api/src/site/purge/comment.rs +++ b/crates/api/api/src/site/purge/comment.rs @@ -63,6 +63,7 @@ pub async fn purge_comment( moderator: local_user_view.person.clone(), community: comment_view.community, reason: data.reason.clone(), + with_replies: None, }, &context, )?; diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index 1456ca1dda..2a18b6184e 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -5,7 +5,7 @@ use lemmy_api_utils::{ context::LemmyContext, notify::notify_mod_action, send_activity::{ActivityChannel, SendActivityData}, - utils::check_community_mod_action, + utils::{check_community_mod_action, remove_or_restore_comment_thread}, }; use lemmy_db_schema::{ source::{ @@ -22,7 +22,7 @@ use lemmy_db_views_comment::{ }; use lemmy_db_views_local_user::LocalUserView; use lemmy_diesel_utils::traits::Crud; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; pub async fn remove_comment( Json(data): Json, @@ -61,50 +61,99 @@ pub async fn remove_comment( return Err(LemmyErrorType::CouldntUpdate.into()); } - // Do the remove - let removed = data.removed; - let updated_comment = Comment::update( - &mut context.pool(), - comment_id, - &CommentUpdateForm { - removed: Some(removed), - ..Default::default() - }, - ) - .await?; + if let Some(remove_children) = data.remove_children { + let updated_comments = remove_or_restore_comment_thread( + &orig_comment.comment, + local_user_view.person.id, + remove_children, + &data.reason, + &context, + ) + .await?; + + let orig_comment_id = orig_comment.comment.id; + let updated_comment = updated_comments + .iter() + .find(|c| c.id == orig_comment_id) + .ok_or(LemmyErrorType::CouldntUpdate)?; - CommentReport::resolve_all_for_object(&mut context.pool(), comment_id, local_user_view.person.id) + CommentReport::resolve_all_for_thread( + &mut context.pool(), + &orig_comment.comment.path, + local_user_view.person.id, + ) .await?; - // Mod tables - let form = ModlogInsertForm::mod_remove_comment( - local_user_view.person.id, - &orig_comment.comment, - removed, - &data.reason, - ); - let actions = Modlog::create(&mut context.pool(), &[form]).await?; - notify_mod_action(actions, context.app_data()); + ActivityChannel::submit_activity( + SendActivityData::RemoveComment { + comment: updated_comment.clone(), + moderator: local_user_view.person.clone(), + community: orig_comment.community.clone(), + reason: data.reason.clone(), + with_replies: Some(remove_children), + }, + &context, + )?; + Ok(Json( + build_comment_response( + &context, + orig_comment_id, + Some(local_user_view), + local_instance_id, + ) + .await?, + )) + } else { + // Do the remove + let removed = data.removed; + let updated_comment = Comment::update( + &mut context.pool(), + comment_id, + &CommentUpdateForm { + removed: Some(removed), + ..Default::default() + }, + ) + .await?; - let updated_comment_id = updated_comment.id; + CommentReport::resolve_all_for_object( + &mut context.pool(), + comment_id, + local_user_view.person.id, + ) + .await?; - ActivityChannel::submit_activity( - SendActivityData::RemoveComment { - comment: updated_comment, - moderator: local_user_view.person.clone(), - community: orig_comment.community, - reason: data.reason.clone(), - }, - &context, - )?; + // Mod tables + let form = ModlogInsertForm::mod_remove_comment( + local_user_view.person.id, + &orig_comment.comment, + removed, + &data.reason, + ); + let actions = Modlog::create(&mut context.pool(), &[form]).await?; + notify_mod_action(actions, context.app_data()); - Ok(Json( - build_comment_response( + let updated_comment_id = updated_comment.id; + + ActivityChannel::submit_activity( + SendActivityData::RemoveComment { + comment: updated_comment, + moderator: local_user_view.person.clone(), + community: orig_comment.community, + reason: data.reason.clone(), + with_replies: None, + }, &context, - updated_comment_id, - Some(local_user_view), - local_instance_id, - ) - .await?, - )) + )?; + + Ok(Json( + build_comment_response( + &context, + updated_comment_id, + Some(local_user_view), + local_instance_id, + ) + .await?, + )) + } } diff --git a/crates/api/api_utils/src/send_activity.rs b/crates/api/api_utils/src/send_activity.rs index 54a8e191eb..aa86cabc4a 100644 --- a/crates/api/api_utils/src/send_activity.rs +++ b/crates/api/api_utils/src/send_activity.rs @@ -50,6 +50,7 @@ pub enum SendActivityData { moderator: Person, community: Community, reason: String, + with_replies: Option, }, LockComment(Comment, Person, bool, String), LikePostOrComment { diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index 37b761b7d3..3aab02bf8a 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -1,6 +1,7 @@ use crate::{ claims::Claims, context::LemmyContext, + notify::notify_mod_action, request::{delete_image_alias, fetch_pictrs_proxied_image_details, purge_image_from_pictrs_url}, }; use actix_web::{HttpRequest, http::header::Header}; @@ -626,16 +627,14 @@ async fn create_modlog_entries_for_removed_or_restored_comments( comments: &[Comment], removed: bool, reason: &str, -) -> LemmyResult<()> { +) -> LemmyResult> { // Build the forms let forms: Vec<_> = comments .iter() .map(|comment| ModlogInsertForm::mod_remove_comment(mod_person_id, comment, removed, reason)) .collect(); - Modlog::create(pool, &forms).await?; - - Ok(()) + Modlog::create(pool, &forms).await } pub async fn remove_or_restore_user_data_in_community( @@ -712,6 +711,30 @@ pub async fn purge_user_account( Ok(()) } +pub async fn remove_or_restore_comment_thread( + comment: &Comment, + mod_person_id: PersonId, + removed: bool, + reason: &str, + context: &LemmyContext, +) -> LemmyResult> { + let removed_comments = + Comment::update_removed_for_comment_and_children(&mut context.pool(), &comment.path, removed) + .await?; + + let actions = create_modlog_entries_for_removed_or_restored_comments( + &mut context.pool(), + mod_person_id, + &removed_comments, + removed, + reason, + ) + .await?; + + notify_mod_action(actions, context); + + Ok(removed_comments) +} pub fn generate_followers_url(ap_id: &DbUrl) -> Result { Ok(Url::parse(&format!("{ap_id}/followers"))?.into()) } diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index aaee1a686f..6365ea6168 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -254,6 +254,19 @@ impl Comment { Self::update_comment_and_children(pool, comment_path, &form).await } + /// Updates the removed field for a comment and all its children. + pub async fn update_removed_for_comment_and_children( + pool: &mut DbPool<'_>, + comment_path: &Ltree, + removed: bool, + ) -> LemmyResult> { + let form = CommentUpdateForm { + removed: Some(removed), + ..Default::default() + }; + Self::update_comment_and_children(pool, comment_path, &form).await + } + /// A helper function to update comment and all its children. /// /// Don't expose so as to make sure you aren't overwriting data. diff --git a/crates/db_schema/src/impls/comment_report.rs b/crates/db_schema/src/impls/comment_report.rs index 3766902c14..e544e50a0d 100644 --- a/crates/db_schema/src/impls/comment_report.rs +++ b/crates/db_schema/src/impls/comment_report.rs @@ -96,3 +96,27 @@ impl Reportable for CommentReport { .with_lemmy_type(LemmyErrorType::CouldntUpdate) } } + +impl CommentReport { + pub async fn resolve_all_for_thread( + pool: &mut DbPool<'_>, + comment_path: &Ltree, + by_resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let report_alias = diesel::alias!(comment_report as cr); + let report_subquery = report_alias + .inner_join(comment::table.on(comment::id.eq(report_alias.field(comment_report::comment_id)))) + .filter(comment::path.contained_by(comment_path)); + update(comment_report::table.filter( + comment_report::id.eq_any(report_subquery.select(report_alias.field(comment_report::id))), + )) + .set(( + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated_at.eq(Utc::now()), + )) + .execute(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdate) + } diff --git a/crates/db_views/comment/src/api.rs b/crates/db_views/comment/src/api.rs index 28db6612c0..0d13e28a2d 100644 --- a/crates/db_views/comment/src/api.rs +++ b/crates/db_views/comment/src/api.rs @@ -126,6 +126,9 @@ pub struct RemoveComment { pub comment_id: CommentId, pub removed: bool, pub reason: String, + /// Setting this will override whatever `removed` was set to, + /// leave as null to act just on the comment itself. + pub remove_children: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] From 96b7ab38e2afba275251b35fb8205f96c81e4e4f Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Sat, 24 Jan 2026 09:59:17 +0000 Subject: [PATCH 02/16] Remove Comment Apub --- crates/apub/activities/src/deletion/delete.rs | 42 ++++++++++++------- crates/apub/activities/src/deletion/mod.rs | 35 ++++++++++++++-- crates/apub/activities/src/lib.rs | 12 +++++- .../src/protocol/deletion/delete.rs | 5 +++ 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index 811c98ffa1..ae1eca878b 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -5,7 +5,11 @@ use crate::{ protocol::{IdOrNestedObject, deletion::delete::Delete}, }; use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::Activity}; -use lemmy_api_utils::{context::LemmyContext, notify::notify_mod_action}; +use lemmy_api_utils::{ + context::LemmyContext, + notify::notify_mod_action, + utils::{remove_or_restore_comment_thread, remove_or_restore_post_comments}, +}; use lemmy_apub_objects::objects::person::ApubPerson; use lemmy_db_schema::{ source::{ @@ -55,6 +59,7 @@ impl Activity for Delete { &self.actor.dereference(context).await?, self.object.id(), reason, + self.with_replies, context, ) .await @@ -78,6 +83,7 @@ impl Delete { to: Vec, community: Option<&Community>, summary: Option, + with_replies: Option, context: &Data, ) -> LemmyResult { let id = generate_activity_id(DeleteType::Delete, context)?; @@ -92,6 +98,7 @@ impl Delete { id, audience: community.map(|c| c.ap_id.clone().into()), remove_data: None, + with_replies, }) } } @@ -100,6 +107,7 @@ pub(crate) async fn receive_remove_action( actor: &ApubPerson, object: &Url, reason: Option, + with_replies: Option, context: &Data, ) -> LemmyResult<()> { let reason = reason.unwrap_or_else(|| MOD_ACTION_DEFAULT_REASON.to_string()); @@ -147,19 +155,25 @@ pub(crate) async fn receive_remove_action( .await?; } DeletableObjects::Comment(comment) => { - CommentReport::resolve_all_for_object(&mut context.pool(), comment.id, actor.id).await?; - let form = ModlogInsertForm::mod_remove_comment(actor.id, &comment, true, &reason); - let action = Modlog::create(&mut context.pool(), &[form]).await?; - notify_mod_action(action, context.app_data()); - Comment::update( - &mut context.pool(), - comment.id, - &CommentUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; + let remove_children = with_replies.unwrap_or_default(); + if remove_children { + CommentReport::resolve_all_for_thread(&mut context.pool(), &comment.path, actor.id).await?; + remove_or_restore_comment_thread(&comment, actor.id, true, &reason, context).await?; + } else { + CommentReport::resolve_all_for_object(&mut context.pool(), comment.id, actor.id).await?; + let form = ModlogInsertForm::mod_remove_comment(actor.id, &comment, true, &reason); + let action = Modlog::create(&mut context.pool(), &[form]).await?; + notify_mod_action(action, context.app_data()); + Comment::update( + &mut context.pool(), + comment.id, + &CommentUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + } } // TODO these need to be implemented yet, for now, return errors DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::NotFound)?, diff --git a/crates/apub/activities/src/deletion/mod.rs b/crates/apub/activities/src/deletion/mod.rs index 4f2569271a..2c99a0f0e7 100644 --- a/crates/apub/activities/src/deletion/mod.rs +++ b/crates/apub/activities/src/deletion/mod.rs @@ -57,16 +57,33 @@ pub(crate) async fn send_apub_delete_in_community( object: DeletableObjects, reason: Option, deleted: bool, + with_replies: Option, context: &Data, ) -> LemmyResult<()> { let actor = ApubPerson::from(actor); let is_mod_action = reason.is_some(); let to = generate_to(&community)?; let activity = if deleted { - let delete = Delete::new(&actor, object, to, Some(&community), reason, context)?; + let delete = Delete::new( + &actor, + object, + to, + Some(&community), + reason, + with_replies, + context, + )?; AnnouncableActivities::Delete(delete) } else { - let undo = UndoDelete::new(&actor, object, to, Some(&community), reason, context)?; + let undo = UndoDelete::new( + &actor, + object, + to, + Some(&community), + reason, + with_replies, + context, + )?; AnnouncableActivities::UndoDelete(undo) }; send_activity_in_community( @@ -100,6 +117,7 @@ pub(crate) async fn send_apub_delete_private_message( vec![recipient.id().clone()], None, None, + None, &context, )?; send_lemmy_activity(&context, delete, actor, inbox, true).await?; @@ -110,6 +128,7 @@ pub(crate) async fn send_apub_delete_private_message( vec![recipient.id().clone()], None, None, + None, &context, )?; send_lemmy_activity(&context, undo, actor, inbox, true).await?; @@ -125,7 +144,15 @@ pub async fn send_apub_delete_user( let person: ApubPerson = person.into(); let deletable = DeletableObjects::Person(person.clone()); - let mut delete: Delete = Delete::new(&person, deletable, vec![public()], None, None, &context)?; + let mut delete: Delete = Delete::new( + &person, + deletable, + vec![public()], + None, + None, + None, + &context, + )?; delete.remove_data = Some(remove_data); let inboxes = ActivitySendTargets::to_all_instances(); @@ -261,7 +288,7 @@ async fn receive_delete_action( let mod_: Person = actor.dereference(context).await?.deref().clone(); let object = DeletableObjects::Community(community.clone()); let c: Community = community.deref().clone(); - send_apub_delete_in_community(mod_, c, object, None, true, context).await?; + send_apub_delete_in_community(mod_, c, object, None, true, None, context).await?; } Community::update( diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 648b2bc014..d4d68d65cf 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -174,6 +174,7 @@ pub async fn match_outgoing_activities( DeletableObjects::Post(post.into()), None, is_deleted, + None, &context, ) .await @@ -217,13 +218,17 @@ pub async fn match_outgoing_activities( DeleteComment(comment, actor, community) => { let is_deleted = comment.deleted; let deletable = DeletableObjects::Comment(comment.into()); - send_apub_delete_in_community(actor, community, deletable, None, is_deleted, &context).await + send_apub_delete_in_community( + actor, community, deletable, None, is_deleted, None, &context, + ) + .await } RemoveComment { comment, moderator, community, reason, + with_replies, } => { let is_removed = comment.removed; let deletable = DeletableObjects::Comment(comment.into()); @@ -233,6 +238,7 @@ pub async fn match_outgoing_activities( deletable, Some(reason), is_removed, + with_replies, &context, ) .await @@ -273,7 +279,8 @@ pub async fn match_outgoing_activities( UpdateCommunity(actor, community) => send_update_community(community, actor, context).await, DeleteCommunity(actor, community, removed) => { let deletable = DeletableObjects::Community(community.clone().into()); - send_apub_delete_in_community(actor, community, deletable, None, removed, &context).await + send_apub_delete_in_community(actor, community, deletable, None, removed, None, &context) + .await } RemoveCommunity { moderator, @@ -288,6 +295,7 @@ pub async fn match_outgoing_activities( deletable, Some(reason), removed, + None, &context, ) .await diff --git a/crates/apub/activities/src/protocol/deletion/delete.rs b/crates/apub/activities/src/protocol/deletion/delete.rs index 9be23cb12f..7550f38b0f 100644 --- a/crates/apub/activities/src/protocol/deletion/delete.rs +++ b/crates/apub/activities/src/protocol/deletion/delete.rs @@ -41,6 +41,11 @@ pub struct Delete { /// Nonstandard field, only valid if object refers to a Person. If present, all content from the /// user should be deleted along with the account pub(crate) remove_data: Option, + /// Nonstandard field denoting that the replies to an `Object` should be removed along with the + /// `Object`. Only valid for `Pages` and `Notes`. + // See here for discussion of this: + // https://activitypub.space/topic/78/deleting-a-post-vs-deleting-an-entire-comment-tree + pub(crate) with_replies: Option, } impl InCommunity for Delete { From b32a15efd9939a04870ed22ce06d29bc9509eaf4 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Sat, 24 Jan 2026 09:59:27 +0000 Subject: [PATCH 03/16] Remove Post API+Apub --- crates/api/api/src/site/purge/post.rs | 1 + crates/api/api_crud/src/post/remove.rs | 23 +++++- crates/api/api_utils/src/send_activity.rs | 1 + crates/api/api_utils/src/utils.rs | 25 +++++++ crates/apub/activities/src/deletion/delete.rs | 6 ++ .../activities/src/deletion/undo_delete.rs | 53 ++++++++++---- crates/apub/activities/src/lib.rs | 2 + crates/db_schema/src/impls/comment.rs | 73 +++++++++++++++++++ crates/db_schema/src/impls/comment_report.rs | 33 ++++++++- crates/db_views/post/src/api.rs | 3 + 10 files changed, 201 insertions(+), 19 deletions(-) diff --git a/crates/api/api/src/site/purge/post.rs b/crates/api/api/src/site/purge/post.rs index 59c13863fb..80f5dcc52a 100644 --- a/crates/api/api/src/site/purge/post.rs +++ b/crates/api/api/src/site/purge/post.rs @@ -50,6 +50,7 @@ pub async fn purge_post( moderator: local_user_view.person.clone(), reason: data.reason.clone(), removed: true, + with_replies: None, }, &context, )?; diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 778b31de4b..2925780da2 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -5,10 +5,12 @@ use lemmy_api_utils::{ context::LemmyContext, notify::notify_mod_action, send_activity::{ActivityChannel, SendActivityData}, - utils::check_community_mod_action, + utils::{check_community_mod_action, remove_or_restore_post_comments}, }; use lemmy_db_schema::{ source::{ + comment::Comment, + comment_report::CommentReport, community::Community, local_user::LocalUser, modlog::{Modlog, ModlogInsertForm}, @@ -47,7 +49,7 @@ pub async fn remove_post( // Update the post let post_id = data.post_id; - let removed = data.removed; + let removed = data.remove_children.unwrap_or(data.removed); let post = Post::update( &mut context.pool(), post_id, @@ -67,12 +69,27 @@ pub async fn remove_post( let action = Modlog::create(&mut context.pool(), &[form]).await?; notify_mod_action(action, context.app_data()); + if let Some(remove_children) = data.remove_children { + remove_or_restore_post_comments( + &post, + local_user_view.person.id, + remove_children, + &data.reason, + &context, + ) + .await?; + + CommentReport::resolve_all_for_post(&mut context.pool(), post.id, local_user_view.person.id) + .await?; + } + ActivityChannel::submit_activity( SendActivityData::RemovePost { post, moderator: local_user_view.person.clone(), reason: data.reason.clone(), - removed: data.removed, + removed, + with_replies: data.remove_children, }, &context, )?; diff --git a/crates/api/api_utils/src/send_activity.rs b/crates/api/api_utils/src/send_activity.rs index aa86cabc4a..fbf561fd65 100644 --- a/crates/api/api_utils/src/send_activity.rs +++ b/crates/api/api_utils/src/send_activity.rs @@ -39,6 +39,7 @@ pub enum SendActivityData { moderator: Person, reason: String, removed: bool, + with_replies: Option, }, LockPost(Post, Person, bool, String), FeaturePost(Post, Person, bool), diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index 3aab02bf8a..195c3cb885 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -735,6 +735,31 @@ pub async fn remove_or_restore_comment_thread( Ok(removed_comments) } + +pub async fn remove_or_restore_post_comments( + post: &Post, + mod_person_id: PersonId, + removed: bool, + reason: &str, + context: &LemmyContext, +) -> LemmyResult> { + let removed_comments = + Comment::update_removed_for_post(&mut context.pool(), post.id, removed).await?; + + let actions = create_modlog_entries_for_removed_or_restored_comments( + &mut context.pool(), + mod_person_id, + &removed_comments, + removed, + reason, + ) + .await?; + + notify_mod_action(actions, context); + + Ok(removed_comments) +} + pub fn generate_followers_url(ap_id: &DbUrl) -> Result { Ok(Url::parse(&format!("{ap_id}/followers"))?.into()) } diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index ae1eca878b..f2c47fe9d2 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -153,6 +153,12 @@ pub(crate) async fn receive_remove_action( }, ) .await?; + + let remove_children = with_replies.unwrap_or_default(); + if remove_children { + CommentReport::resolve_all_for_post(&mut context.pool(), post.id, actor.id).await?; + remove_or_restore_post_comments(&post, actor.id, true, &reason, context).await?; + } } DeletableObjects::Comment(comment) => { let remove_children = with_replies.unwrap_or_default(); diff --git a/crates/apub/activities/src/deletion/undo_delete.rs b/crates/apub/activities/src/deletion/undo_delete.rs index 248414e309..50a9d42254 100644 --- a/crates/apub/activities/src/deletion/undo_delete.rs +++ b/crates/apub/activities/src/deletion/undo_delete.rs @@ -4,7 +4,11 @@ use crate::{ protocol::deletion::{delete::Delete, undo_delete::UndoDelete}, }; use activitypub_federation::{config::Data, kinds::activity::UndoType, traits::Activity}; -use lemmy_api_utils::{context::LemmyContext, notify::notify_mod_action}; +use lemmy_api_utils::{ + context::LemmyContext, + notify::notify_mod_action, + utils::{remove_or_restore_comment_thread, remove_or_restore_post_comments}, +}; use lemmy_apub_objects::objects::person::ApubPerson; use lemmy_db_schema::source::{ comment::{Comment, CommentUpdateForm}, @@ -42,6 +46,7 @@ impl Activity for UndoDelete { &self.actor.dereference(context).await?, self.object.object.id(), reason, + self.object.with_replies, context, ) .await @@ -58,9 +63,18 @@ impl UndoDelete { to: Vec, community: Option<&Community>, summary: Option, + with_replies: Option, context: &Data, ) -> LemmyResult { - let object = Delete::new(actor, object, to.clone(), community, summary, context)?; + let object = Delete::new( + actor, + object, + to.clone(), + community, + summary, + with_replies, + context, + )?; let id = generate_activity_id(UndoType::Undo, context)?; let cc: Option = community.map(|c| c.ap_id.clone().into()); @@ -79,6 +93,7 @@ impl UndoDelete { actor: &ApubPerson, object: &Url, reason: String, + with_replies: Option, context: &Data, ) -> LemmyResult<()> { match DeletableObjects::read_from_db(object, context).await? { @@ -121,20 +136,30 @@ impl UndoDelete { }, ) .await?; + + let restore_children = with_replies.unwrap_or_default(); + if restore_children { + remove_or_restore_post_comments(&post, actor.id, false, &reason, context).await?; + } } DeletableObjects::Comment(comment) => { - let form = ModlogInsertForm::mod_remove_comment(actor.id, &comment, false, &reason); - let action = Modlog::create(&mut context.pool(), &[form]).await?; - notify_mod_action(action, context.app_data()); - Comment::update( - &mut context.pool(), - comment.id, - &CommentUpdateForm { - removed: Some(false), - ..Default::default() - }, - ) - .await?; + let restore_children = with_replies.unwrap_or_default(); + if restore_children { + remove_or_restore_comment_thread(&comment, actor.id, false, &reason, context).await?; + } else { + let form = ModlogInsertForm::mod_remove_comment(actor.id, &comment, false, &reason); + let action = Modlog::create(&mut context.pool(), &[form]).await?; + notify_mod_action(action, context.app_data()); + Comment::update( + &mut context.pool(), + comment.id, + &CommentUpdateForm { + removed: Some(false), + ..Default::default() + }, + ) + .await?; + } } // TODO these need to be implemented yet, for now, return errors DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::NotFound)?, diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index d4d68d65cf..9a7ee9b2e4 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -184,6 +184,7 @@ pub async fn match_outgoing_activities( moderator, reason, removed, + with_replies, } => { let community = Community::read(&mut context.pool(), post.community_id).await?; send_apub_delete_in_community( @@ -192,6 +193,7 @@ pub async fn match_outgoing_activities( DeletableObjects::Post(post.into()), Some(reason), removed, + with_replies, &context, ) .await diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 6365ea6168..1f69821c26 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -284,6 +284,24 @@ impl Comment { .with_lemmy_type(LemmyErrorType::CouldntUpdate) } + /// Update the remove field for all the comments under a post. + pub async fn update_removed_for_post( + pool: &mut DbPool<'_>, + post_id: PostId, + removed: bool, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + diesel::update(comment::table) + .filter(comment::post_id.eq(post_id)) + .set(( + comment::removed.eq(removed), + comment::updated_at.eq(Utc::now()), + )) + .get_results(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdate) + } + pub async fn read_ap_ids_for_post( post_id: PostId, pool: &mut DbPool<'_>, @@ -717,4 +735,59 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn test_remove_post_children() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "mydomain.tld").await?; + let new_person = PersonInsertForm::test_form(inserted_instance.id, "john"); + let inserted_person = Person::create(pool, &new_person).await?; + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test".into(), + "test".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + let new_post = PostInsertForm::new( + "Post Title".to_string(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let comment_toplevel1_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "Top level".to_string(), + ); + let inserted_comment_toplevel1 = Comment::create(pool, &comment_toplevel1_form, None).await?; + + let child_comment_form = + CommentInsertForm::new(inserted_person.id, inserted_post.id, "Child".to_string()); + let _inserted_child_comment = Comment::create( + pool, + &child_comment_form, + Some(&inserted_comment_toplevel1.path), + ) + .await?; + + let comment_toplevel2_form = CommentInsertForm::new( + inserted_person.id, + inserted_post.id, + "Top level 2".to_string(), + ); + let _inserted_comment_toplevel2 = Comment::create(pool, &comment_toplevel2_form, None).await?; + + let updated_comments = Comment::update_removed_for_post(pool, inserted_post.id, true).await?; + + let updated_comments_num = updated_comments.iter().filter(|c| c.removed).count(); + + assert_eq!(updated_comments_num, 3); + + Ok(()) + } } diff --git a/crates/db_schema/src/impls/comment_report.rs b/crates/db_schema/src/impls/comment_report.rs index e544e50a0d..1d19d26866 100644 --- a/crates/db_schema/src/impls/comment_report.rs +++ b/crates/db_schema/src/impls/comment_report.rs @@ -1,5 +1,5 @@ use crate::{ - newtypes::{CommentId, CommentReportId}, + newtypes::{CommentId, CommentReportId, PostId}, source::comment_report::{CommentReport, CommentReportForm}, traits::Reportable, }; @@ -7,11 +7,16 @@ use chrono::Utc; use diesel::{ BoolExpressionMethods, ExpressionMethods, + JoinOnDsl, QueryDsl, dsl::{insert_into, update}, }; use diesel_async::RunQueryDsl; -use lemmy_db_schema_file::{PersonId, schema::comment_report}; +use diesel_ltree::{Ltree, LtreeExtensions}; +use lemmy_db_schema_file::{ + PersonId, + schema::{comment, comment_report}, +}; use lemmy_diesel_utils::connection::{DbPool, get_conn}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; @@ -120,3 +125,27 @@ impl CommentReport { .await .with_lemmy_type(LemmyErrorType::CouldntUpdate) } + + pub async fn resolve_all_for_post( + pool: &mut DbPool<'_>, + post_id: PostId, + by_resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + let report_alias = diesel::alias!(comment_report as cr); + let report_subquery = report_alias + .inner_join(comment::table.on(comment::id.eq(report_alias.field(comment_report::comment_id)))) + .filter(comment::post_id.eq(post_id)); + update(comment_report::table.filter( + comment_report::id.eq_any(report_subquery.select(report_alias.field(comment_report::id))), + )) + .set(( + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated_at.eq(Utc::now()), + )) + .execute(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdate) + } +} diff --git a/crates/db_views/post/src/api.rs b/crates/db_views/post/src/api.rs index 69421ec12b..cd6e69089e 100644 --- a/crates/db_views/post/src/api.rs +++ b/crates/db_views/post/src/api.rs @@ -247,6 +247,9 @@ pub struct RemovePost { pub post_id: PostId, pub removed: bool, pub reason: String, + /// Setting this will override whatever `removed` was set to, + /// leave as null to act just on the post itself. + pub remove_children: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] From f868e10caa78e44f34a92d0037735addd3d4c856 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Mon, 2 Feb 2026 06:16:43 +0000 Subject: [PATCH 04/16] Move to own API endpoint --- crates/api/api/src/comment/mod.rs | 1 + .../api/src/comment/remove_with_children.rs | 92 ++++++++++++ crates/api/api_crud/src/comment/remove.rs | 132 ++++++------------ crates/api/api_crud/src/post/remove.rs | 83 +++++++---- crates/api/api_utils/src/utils.rs | 31 ++-- crates/api/routes/src/lib.rs | 5 +- crates/db_views/comment/src/api.rs | 14 +- crates/db_views/post/src/api.rs | 14 +- 8 files changed, 236 insertions(+), 136 deletions(-) create mode 100644 crates/api/api/src/comment/remove_with_children.rs diff --git a/crates/api/api/src/comment/mod.rs b/crates/api/api/src/comment/mod.rs index d6d8339eb1..a5b935ee66 100644 --- a/crates/api/api/src/comment/mod.rs +++ b/crates/api/api/src/comment/mod.rs @@ -3,3 +3,4 @@ pub mod like; pub mod list_comment_likes; pub mod lock; pub mod save; +pub mod remove_with_children; diff --git a/crates/api/api/src/comment/remove_with_children.rs b/crates/api/api/src/comment/remove_with_children.rs new file mode 100644 index 0000000000..c8d979c6e0 --- /dev/null +++ b/crates/api/api/src/comment/remove_with_children.rs @@ -0,0 +1,92 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_utils::{ + build_response::build_comment_response, + context::LemmyContext, + send_activity::{ActivityChannel, SendActivityData}, + utils::{check_community_mod_action, remove_or_restore_comment_thread}, +}; +use lemmy_db_schema::source::{ + comment_report::CommentReport, + local_user::LocalUser, + }; +use lemmy_db_views_comment::{ + CommentView, + api::{CommentResponse, RemoveCommentWithChildren}, +}; +use lemmy_db_views_local_user::LocalUserView; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; + +pub async fn remove_comment_with_children( + Json(data): Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let comment_id = data.comment_id; + let local_instance_id = local_user_view.person.instance_id; + let orig_comment = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + local_instance_id, + ) + .await?; + + check_community_mod_action( + &local_user_view, + &orig_comment.community, + false, + &mut context.pool(), + ) + .await?; + + LocalUser::is_higher_mod_or_admin_check( + &mut context.pool(), + orig_comment.community.id, + local_user_view.person.id, + vec![orig_comment.creator.id], + ) + .await?; + + let updated_comments = remove_or_restore_comment_thread( + &orig_comment.comment, + local_user_view.person.id, + data.removed, + &data.reason, + &context, + ) + .await?; + + let orig_comment_id = orig_comment.comment.id; + let updated_comment = updated_comments + .iter() + .find(|c| c.id == orig_comment_id) + .ok_or(LemmyErrorType::CouldntUpdate)?; + + CommentReport::resolve_all_for_thread( + &mut context.pool(), + &orig_comment.comment.path, + local_user_view.person.id, + ) + .await?; + + ActivityChannel::submit_activity( + SendActivityData::RemoveComment { + comment: updated_comment.clone(), + moderator: local_user_view.person.clone(), + community: orig_comment.community.clone(), + reason: data.reason.clone(), + with_replies: data.removed.into(), + }, + &context, + )?; + + build_comment_response( + &context, + comment_id, + local_user_view.into(), + local_instance_id, + ) + .await + .map(Json) +} diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index 2a18b6184e..21408c065d 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -5,7 +5,7 @@ use lemmy_api_utils::{ context::LemmyContext, notify::notify_mod_action, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_mod_action, remove_or_restore_comment_thread}, + utils::check_community_mod_action, }; use lemmy_db_schema::{ source::{ @@ -22,7 +22,7 @@ use lemmy_db_views_comment::{ }; use lemmy_db_views_local_user::LocalUserView; use lemmy_diesel_utils::traits::Crud; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; pub async fn remove_comment( Json(data): Json, @@ -61,99 +61,55 @@ pub async fn remove_comment( return Err(LemmyErrorType::CouldntUpdate.into()); } - if let Some(remove_children) = data.remove_children { - let updated_comments = remove_or_restore_comment_thread( - &orig_comment.comment, - local_user_view.person.id, - remove_children, - &data.reason, - &context, - ) - .await?; - - let orig_comment_id = orig_comment.comment.id; - let updated_comment = updated_comments - .iter() - .find(|c| c.id == orig_comment_id) - .ok_or(LemmyErrorType::CouldntUpdate)?; - - CommentReport::resolve_all_for_thread( - &mut context.pool(), - &orig_comment.comment.path, - local_user_view.person.id, - ) - .await?; - - ActivityChannel::submit_activity( - SendActivityData::RemoveComment { - comment: updated_comment.clone(), - moderator: local_user_view.person.clone(), - community: orig_comment.community.clone(), - reason: data.reason.clone(), - with_replies: Some(remove_children), - }, - &context, - )?; - Ok(Json( - build_comment_response( - &context, - orig_comment_id, - Some(local_user_view), - local_instance_id, - ) - .await?, - )) - } else { - // Do the remove + // Do the remove let removed = data.removed; - let updated_comment = Comment::update( - &mut context.pool(), - comment_id, - &CommentUpdateForm { - removed: Some(removed), - ..Default::default() - }, - ) + let updated_comment = Comment::update( + &mut context.pool(), + comment_id, + &CommentUpdateForm { + removed: Some(removed), + ..Default::default() + }, + ) .await?; - CommentReport::resolve_all_for_object( - &mut context.pool(), - comment_id, - local_user_view.person.id, - ) + CommentReport::resolve_all_for_object( + &mut context.pool(), + comment_id, + local_user_view.person.id, + ) .await?; - // Mod tables - let form = ModlogInsertForm::mod_remove_comment( - local_user_view.person.id, - &orig_comment.comment, - removed, - &data.reason, - ); - let actions = Modlog::create(&mut context.pool(), &[form]).await?; - notify_mod_action(actions, context.app_data()); + // Mod tables + let form = ModlogInsertForm::mod_remove_comment( + local_user_view.person.id, + &orig_comment.comment, + removed, + &data.reason, + ); + let actions = Modlog::create(&mut context.pool(), &[form]).await?; + notify_mod_action(actions, context.app_data()); - let updated_comment_id = updated_comment.id; + let updated_comment_id = updated_comment.id; - ActivityChannel::submit_activity( - SendActivityData::RemoveComment { - comment: updated_comment, - moderator: local_user_view.person.clone(), - community: orig_comment.community, - reason: data.reason.clone(), - with_replies: None, - }, - &context, - )?; + ActivityChannel::submit_activity( + SendActivityData::RemoveComment { + comment: updated_comment, + moderator: local_user_view.person.clone(), + community: orig_comment.community, + reason: data.reason.clone(), + with_replies: None, + }, + &context, + )?; - Ok(Json( - build_comment_response( - &context, - updated_comment_id, - Some(local_user_view), - local_instance_id, - ) + Ok(Json( + build_comment_response( + &context, + updated_comment_id, + Some(local_user_view), + local_instance_id, + ) .await?, - )) - } + )) } diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 2925780da2..62bbff05b2 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -8,8 +8,8 @@ use lemmy_api_utils::{ utils::{check_community_mod_action, remove_or_restore_post_comments}, }; use lemmy_db_schema::{ + newtypes::PostId, source::{ - comment::Comment, comment_report::CommentReport, community::Community, local_user::LocalUser, @@ -20,17 +20,16 @@ use lemmy_db_schema::{ traits::Reportable, }; use lemmy_db_views_local_user::LocalUserView; -use lemmy_db_views_post::api::{PostResponse, RemovePost}; +use lemmy_db_views_post::api::{PostResponse, RemovePost, RemovePostWithChildren}; use lemmy_diesel_utils::traits::Crud; use lemmy_utils::error::LemmyResult; -pub async fn remove_post( - Json(data): Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let post_id = data.post_id; - +async fn do_remove( + context: &Data, + post_id: PostId, + removed: bool, + local_user_view: &LocalUserView, +) -> LemmyResult<(Post, Community)> { // We cannot use PostView to avoid a database read here, as it doesn't return removed items // by default. So we would have to pass in `is_mod_or_admin`, but that is impossible without // knowing which community the post belongs to. @@ -48,8 +47,7 @@ pub async fn remove_post( .await?; // Update the post - let post_id = data.post_id; - let removed = data.remove_children.unwrap_or(data.removed); + let post_id = post_id; let post = Post::update( &mut context.pool(), post_id, @@ -69,30 +67,63 @@ pub async fn remove_post( let action = Modlog::create(&mut context.pool(), &[form]).await?; notify_mod_action(action, context.app_data()); - if let Some(remove_children) = data.remove_children { - remove_or_restore_post_comments( - &post, - local_user_view.person.id, - remove_children, - &data.reason, - &context, - ) - .await?; + Ok((post, community)) +} - CommentReport::resolve_all_for_post(&mut context.pool(), post.id, local_user_view.person.id) - .await?; - } +pub async fn remove_post( + Json(data): Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let post_id = data.post_id; + + let (post, community) = do_remove(&context, post_id, data.removed, &local_user_view).await?; + + ActivityChannel::submit_activity( + SendActivityData::RemovePost { + post, + moderator: local_user_view.person.clone(), + reason: data.reason.clone(), + removed: data.removed, + with_replies: None, + }, + &context, + )?; + + build_post_response(&context, community.id, local_user_view, post_id).await +} + +pub async fn remove_post_with_children( + Json(data): Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let post_id = data.post_id; + + let (post, community) = do_remove(&context, post_id, data.removed, &local_user_view).await?; + + remove_or_restore_post_comments( + &post, + local_user_view.person.id, + data.removed, + &data.reason, + &context, + ) + .await?; + + CommentReport::resolve_all_for_post(&mut context.pool(), post.id, local_user_view.person.id) + .await?; ActivityChannel::submit_activity( SendActivityData::RemovePost { post, moderator: local_user_view.person.clone(), reason: data.reason.clone(), - removed, - with_replies: data.remove_children, + removed: data.removed, + with_replies: Some(removed), }, &context, )?; - build_post_response(&context, orig_post.community_id, local_user_view, post_id).await + build_post_response(&context, community.id, local_user_view, post_id).await } diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index 85567585d9..022d92113a 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -30,8 +30,7 @@ use lemmy_db_schema::{ traits::Likeable, }; use lemmy_db_schema_file::{ - InstanceId, - PersonId, + InstanceId, PersonId, enums::{FederationMode, RegistrationMode}, }; use lemmy_db_views_community_follower_approval::PendingFollowerView; @@ -41,16 +40,9 @@ use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_site::SiteView; use lemmy_diesel_utils::{connection::DbPool, dburl::DbUrl, traits::Crud}; use lemmy_utils::{ - CACHE_DURATION_FEDERATION, - CacheLock, - MAX_COMMENT_DEPTH_LIMIT, + CACHE_DURATION_FEDERATION, CacheLock, MAX_COMMENT_DEPTH_LIMIT, error::{ - LemmyError, - LemmyErrorExt, - LemmyErrorExt2, - LemmyErrorType, - LemmyResult, - UntranslatedError, + LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult, UntranslatedError, }, rate_limit::{ActionType, BucketConfig}, settings::{SETTINGS, structs::PictrsImageMode}, @@ -718,9 +710,13 @@ pub async fn remove_or_restore_comment_thread( reason: &str, context: &LemmyContext, ) -> LemmyResult> { - let removed_comments = + let removed_comments: Vec = Comment::update_removed_for_comment_and_children(&mut context.pool(), &comment.path, removed) - .await?; + .await? + // Filter out deleted comments here so their content doesn't show up in the modlog. + .into_iter() + .filter(|c| !c.deleted) + .collect(); let actions = create_modlog_entries_for_removed_or_restored_comments( &mut context.pool(), @@ -743,8 +739,13 @@ pub async fn remove_or_restore_post_comments( reason: &str, context: &LemmyContext, ) -> LemmyResult> { - let removed_comments = - Comment::update_removed_for_post(&mut context.pool(), post.id, removed).await?; + let removed_comments: Vec = + Comment::update_removed_for_post(&mut context.pool(), post.id, removed) + .await? + // Filter out deleted comments here so their content doesn't show up in the modlog. + .into_iter() + .filter(|c| !c.deleted) + .collect(); let actions = create_modlog_entries_for_removed_or_restored_comments( &mut context.pool(), diff --git a/crates/api/routes/src/lib.rs b/crates/api/routes/src/lib.rs index 9b34fc8a62..955ca0ade3 100644 --- a/crates/api/routes/src/lib.rs +++ b/crates/api/routes/src/lib.rs @@ -6,6 +6,7 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, lock::lock_comment, save::save_comment, + remove_with_children::remove_comment_with_children, }, community::{ add_mod::add_mod_to_community, @@ -140,7 +141,7 @@ use lemmy_api_crud::{ create::create_post, delete::delete_post, read::get_post, - remove::remove_post, + remove::{remove_post, remove_post_with_children}, update::update_post, }, private_message::{ @@ -278,6 +279,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("", put().to(update_post)) .route("", delete().to(delete_post)) .route("/remove", post().to(remove_post)) + .route("/remove_with_children", post().to(remove_post_with_children)) .route("/mark_as_read", post().to(mark_post_as_read)) .route("/mark_as_read/many", post().to(mark_posts_as_read)) .route("/hide", post().to(hide_post)) @@ -306,6 +308,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("", put().to(update_comment)) .route("", delete().to(delete_comment)) .route("/remove", post().to(remove_comment)) + .route("/remove_with_children", post().to(remove_comment_with_children)) .route("/distinguish", post().to(distinguish_comment)) .route("/like", post().to(like_comment)) .route("/like/list", get().to(list_comment_likes)) diff --git a/crates/db_views/comment/src/api.rs b/crates/db_views/comment/src/api.rs index 0d13e28a2d..6c4b587af6 100644 --- a/crates/db_views/comment/src/api.rs +++ b/crates/db_views/comment/src/api.rs @@ -126,9 +126,17 @@ pub struct RemoveComment { pub comment_id: CommentId, pub removed: bool, pub reason: String, - /// Setting this will override whatever `removed` was set to, - /// leave as null to act just on the comment itself. - pub remove_children: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] +/// Remove a comment and all its children (only doable by mods). +pub struct RemoveCommentWithChildren { + pub comment_id: CommentId, + pub removed: bool, + pub reason: String, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/db_views/post/src/api.rs b/crates/db_views/post/src/api.rs index cd6e69089e..3d1c41d1e0 100644 --- a/crates/db_views/post/src/api.rs +++ b/crates/db_views/post/src/api.rs @@ -247,9 +247,17 @@ pub struct RemovePost { pub post_id: PostId, pub removed: bool, pub reason: String, - /// Setting this will override whatever `removed` was set to, - /// leave as null to act just on the post itself. - pub remove_children: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] +/// Remove a post and all its children (only doable by mods). +pub struct RemovePostWithChildren { + pub post_id: PostId, + pub removed: bool, + pub reason: String, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] From faff6272beb18bd1bb2dbd073cbcf82cf52a1ddf Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Mon, 2 Feb 2026 06:17:10 +0000 Subject: [PATCH 05/16] fmt --- crates/api/api/src/comment/mod.rs | 2 +- crates/api/api/src/comment/remove_with_children.rs | 5 +---- crates/api/api_crud/src/comment/remove.rs | 12 ++++-------- crates/api/api_utils/src/utils.rs | 14 +++++++++++--- crates/api/routes/src/lib.rs | 12 +++++++++--- crates/db_schema/src/impls/comment.rs | 2 +- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/crates/api/api/src/comment/mod.rs b/crates/api/api/src/comment/mod.rs index a5b935ee66..0211320b2d 100644 --- a/crates/api/api/src/comment/mod.rs +++ b/crates/api/api/src/comment/mod.rs @@ -2,5 +2,5 @@ pub mod distinguish; pub mod like; pub mod list_comment_likes; pub mod lock; -pub mod save; pub mod remove_with_children; +pub mod save; diff --git a/crates/api/api/src/comment/remove_with_children.rs b/crates/api/api/src/comment/remove_with_children.rs index c8d979c6e0..c8c0be21a0 100644 --- a/crates/api/api/src/comment/remove_with_children.rs +++ b/crates/api/api/src/comment/remove_with_children.rs @@ -6,10 +6,7 @@ use lemmy_api_utils::{ send_activity::{ActivityChannel, SendActivityData}, utils::{check_community_mod_action, remove_or_restore_comment_thread}, }; -use lemmy_db_schema::source::{ - comment_report::CommentReport, - local_user::LocalUser, - }; +use lemmy_db_schema::source::{comment_report::CommentReport, local_user::LocalUser}; use lemmy_db_views_comment::{ CommentView, api::{CommentResponse, RemoveCommentWithChildren}, diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index 21408c065d..e7ef78034e 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -62,7 +62,7 @@ pub async fn remove_comment( } // Do the remove - let removed = data.removed; + let removed = data.removed; let updated_comment = Comment::update( &mut context.pool(), comment_id, @@ -71,13 +71,9 @@ pub async fn remove_comment( ..Default::default() }, ) - .await?; + .await?; - CommentReport::resolve_all_for_object( - &mut context.pool(), - comment_id, - local_user_view.person.id, - ) + CommentReport::resolve_all_for_object(&mut context.pool(), comment_id, local_user_view.person.id) .await?; // Mod tables @@ -110,6 +106,6 @@ pub async fn remove_comment( Some(local_user_view), local_instance_id, ) - .await?, + .await?, )) } diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index 022d92113a..1e9b365724 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -30,7 +30,8 @@ use lemmy_db_schema::{ traits::Likeable, }; use lemmy_db_schema_file::{ - InstanceId, PersonId, + InstanceId, + PersonId, enums::{FederationMode, RegistrationMode}, }; use lemmy_db_views_community_follower_approval::PendingFollowerView; @@ -40,9 +41,16 @@ use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_site::SiteView; use lemmy_diesel_utils::{connection::DbPool, dburl::DbUrl, traits::Crud}; use lemmy_utils::{ - CACHE_DURATION_FEDERATION, CacheLock, MAX_COMMENT_DEPTH_LIMIT, + CACHE_DURATION_FEDERATION, + CacheLock, + MAX_COMMENT_DEPTH_LIMIT, error::{ - LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult, UntranslatedError, + LemmyError, + LemmyErrorExt, + LemmyErrorExt2, + LemmyErrorType, + LemmyResult, + UntranslatedError, }, rate_limit::{ActionType, BucketConfig}, settings::{SETTINGS, structs::PictrsImageMode}, diff --git a/crates/api/routes/src/lib.rs b/crates/api/routes/src/lib.rs index 955ca0ade3..9bf2d0f054 100644 --- a/crates/api/routes/src/lib.rs +++ b/crates/api/routes/src/lib.rs @@ -5,8 +5,8 @@ use lemmy_api::{ like::like_comment, list_comment_likes::list_comment_likes, lock::lock_comment, - save::save_comment, remove_with_children::remove_comment_with_children, + save::save_comment, }, community::{ add_mod::add_mod_to_community, @@ -279,7 +279,10 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("", put().to(update_post)) .route("", delete().to(delete_post)) .route("/remove", post().to(remove_post)) - .route("/remove_with_children", post().to(remove_post_with_children)) + .route( + "/remove_with_children", + post().to(remove_post_with_children), + ) .route("/mark_as_read", post().to(mark_post_as_read)) .route("/mark_as_read/many", post().to(mark_posts_as_read)) .route("/hide", post().to(hide_post)) @@ -308,7 +311,10 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("", put().to(update_comment)) .route("", delete().to(delete_comment)) .route("/remove", post().to(remove_comment)) - .route("/remove_with_children", post().to(remove_comment_with_children)) + .route( + "/remove_with_children", + post().to(remove_comment_with_children), + ) .route("/distinguish", post().to(distinguish_comment)) .route("/like", post().to(like_comment)) .route("/like/list", get().to(list_comment_likes)) diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 1f69821c26..5f9ed4b4da 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -787,7 +787,7 @@ mod tests { let updated_comments_num = updated_comments.iter().filter(|c| c.removed).count(); assert_eq!(updated_comments_num, 3); - + Ok(()) } } From 59b8074a81e0a093bd2ecacfbf259d3e2af2f498 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Mon, 2 Feb 2026 14:53:22 +0000 Subject: [PATCH 06/16] Fix --- crates/api/api_crud/src/post/remove.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 62bbff05b2..4ac4115bef 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -28,6 +28,7 @@ async fn do_remove( context: &Data, post_id: PostId, removed: bool, + reason: &str, local_user_view: &LocalUserView, ) -> LemmyResult<(Post, Community)> { // We cannot use PostView to avoid a database read here, as it doesn't return removed items @@ -62,8 +63,7 @@ async fn do_remove( .await?; // Mod tables - let form = - ModlogInsertForm::mod_remove_post(local_user_view.person.id, &post, removed, &data.reason); + let form = ModlogInsertForm::mod_remove_post(local_user_view.person.id, &post, removed, reason); let action = Modlog::create(&mut context.pool(), &[form]).await?; notify_mod_action(action, context.app_data()); @@ -77,7 +77,14 @@ pub async fn remove_post( ) -> LemmyResult> { let post_id = data.post_id; - let (post, community) = do_remove(&context, post_id, data.removed, &local_user_view).await?; + let (post, community) = do_remove( + &context, + post_id, + data.removed, + &data.reason, + &local_user_view, + ) + .await?; ActivityChannel::submit_activity( SendActivityData::RemovePost { @@ -100,7 +107,14 @@ pub async fn remove_post_with_children( ) -> LemmyResult> { let post_id = data.post_id; - let (post, community) = do_remove(&context, post_id, data.removed, &local_user_view).await?; + let (post, community) = do_remove( + &context, + post_id, + data.removed, + &data.reason, + &local_user_view, + ) + .await?; remove_or_restore_post_comments( &post, @@ -120,7 +134,7 @@ pub async fn remove_post_with_children( moderator: local_user_view.person.clone(), reason: data.reason.clone(), removed: data.removed, - with_replies: Some(removed), + with_replies: Some(data.removed), }, &context, )?; From 403da651d5a59b82a569942292c6dc6a691bca8b Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Wed, 4 Feb 2026 14:03:45 +0000 Subject: [PATCH 07/16] Fix test --- crates/db_schema/src/impls/comment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 5f9ed4b4da..01047023cd 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -743,7 +743,7 @@ mod tests { let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "mydomain.tld").await?; - let new_person = PersonInsertForm::test_form(inserted_instance.id, "john"); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "sharah"); let inserted_person = Person::create(pool, &new_person).await?; let new_community = CommunityInsertForm::new( inserted_instance.id, From cbf596afb925ad4e2d5782bd193cedb407094688 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Wed, 4 Feb 2026 14:05:03 +0000 Subject: [PATCH 08/16] API test --- api_tests/src/comment.spec.ts | 61 +++++++++++++++++++++++++++++++++++ api_tests/src/shared.ts | 25 ++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index db31875058..bf4b14a863 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -16,6 +16,7 @@ import { editComment, deleteComment, removeComment, + removeCommentWithChildren, resolvePost, unfollowRemotes, createCommunity, @@ -23,6 +24,7 @@ import { reportComment, randomString, unfollows, + getComment, getComments, getCommentParentId, resolveCommunity, @@ -981,6 +983,65 @@ test("Lock comment", async () => { ).toBeDefined(); }); +test("Remove children", async () => { + const alphaCommunity = await resolveCommunity( + alpha, + "!main@lemmy-alpha:8541", + ); + if (!alphaCommunity) { + throw "Missing alpha community"; + } + + let post = await createPost(alpha, alphaCommunity.community.id); + let betaPost = await resolvePost(beta, post.post_view.post); + + if (!betaPost) { + throw "unable to locate post on beta"; + } + await followCommunity(beta, true, betaPost.community.id); + + let comment1 = await createComment(beta, betaPost.post.id); + let comment2 = await createComment( + beta, + betaPost.post.id, + comment1.comment_view.comment.id, + ); + await createComment(beta, betaPost.post.id, comment2.comment_view.comment.id); + await createComment(beta, betaPost.post.id, comment1.comment_view.comment.id); + + // Wait until the comments have federated + await waitUntil( + () => getPost(alpha, post.post_view.post.id), + p => p.post_view.post.comments == 4, + ); + + let commentOnAlpha = await resolveComment( + alpha, + comment1.comment_view.comment, + ); + if (!commentOnAlpha) { + throw "unable to locate comment on alpha"; + } + + await removeCommentWithChildren(alpha, true, commentOnAlpha.comment.id); + + let post2 = await getPost(alpha, post.post_view.post.id); + expect(post2.post_view.post.comments).toBe(0); + + // Wait until the remove has federated + await waitUntil( + () => getComment(beta, comment1.comment_view.comment.id), + c => c.comment_view.comment.removed, + ); + + // Make sure removal federates properly + let betaPost2 = await resolvePost(beta, post.post_view.post); + if (!betaPost2) { + throw "unable to locate post on beta"; + } + expect(betaPost2.post.comments).toBe(0); +}); + function checkCommentReportReason(rcv: ReportCombinedView, reason: string) { switch (rcv.type_) { case "comment": diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 84b15381e5..cc57175e9f 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -53,6 +53,7 @@ import { EditSite, FeaturePost, FollowCommunity, + GetComment, GetComments, GetCommunity, GetCommunityResponse, @@ -73,6 +74,7 @@ import { PrivateMessageResponse, Register, RemoveComment, + RemoveCommentWithChildren, RemoveCommunity, RemovePost, ResolveObject, @@ -381,6 +383,16 @@ export async function lockComment( return api.lockComment(form); } +export async function getComment( + api: LemmyHttp, + comment_id: number, +): Promise { + let form: GetComment = { + id: comment_id, + }; + return api.getComment(form); +} + export async function getComments( api: LemmyHttp, post_id?: number, @@ -596,6 +608,19 @@ export async function likeComment( return api.likeComment(form); } +export async function removeCommentWithChildren( + api: LemmyHttp, + removed: boolean, + comment_id: number, +): Promise { + let form: RemoveCommentWithChildren = { + comment_id, + removed, + reason: "reason", + }; + return api.removeCommentWithChildren(form); +} + export async function createCommunity( api: LemmyHttp, name_: string = randomString(10), From 69d01093203c9a8135ae08747ed91a1d57c8cd20 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Wed, 4 Feb 2026 14:05:11 +0000 Subject: [PATCH 09/16] Clippy --- crates/api/api_crud/src/post/remove.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 4ac4115bef..9ba8652ffb 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -37,7 +37,7 @@ async fn do_remove( let orig_post = Post::read(&mut context.pool(), post_id).await?; let community = Community::read(&mut context.pool(), orig_post.community_id).await?; - check_community_mod_action(&local_user_view, &community, false, &mut context.pool()).await?; + check_community_mod_action(local_user_view, &community, false, &mut context.pool()).await?; LocalUser::is_higher_mod_or_admin_check( &mut context.pool(), @@ -48,7 +48,6 @@ async fn do_remove( .await?; // Update the post - let post_id = post_id; let post = Post::update( &mut context.pool(), post_id, From 663205b51080a951e67898324534de3d2950f585 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Wed, 4 Feb 2026 14:33:57 +0000 Subject: [PATCH 10/16] Get rid of WithChildren structs --- crates/api/api/src/comment/remove_with_children.rs | 4 ++-- crates/api/api_crud/src/post/remove.rs | 4 ++-- crates/db_views/comment/src/api.rs | 11 ----------- crates/db_views/post/src/api.rs | 11 ----------- 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/crates/api/api/src/comment/remove_with_children.rs b/crates/api/api/src/comment/remove_with_children.rs index c8c0be21a0..c119baf96e 100644 --- a/crates/api/api/src/comment/remove_with_children.rs +++ b/crates/api/api/src/comment/remove_with_children.rs @@ -9,13 +9,13 @@ use lemmy_api_utils::{ use lemmy_db_schema::source::{comment_report::CommentReport, local_user::LocalUser}; use lemmy_db_views_comment::{ CommentView, - api::{CommentResponse, RemoveCommentWithChildren}, + api::{CommentResponse, RemoveComment}, }; use lemmy_db_views_local_user::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; pub async fn remove_comment_with_children( - Json(data): Json, + Json(data): Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 9ba8652ffb..acd90a5529 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ traits::Reportable, }; use lemmy_db_views_local_user::LocalUserView; -use lemmy_db_views_post::api::{PostResponse, RemovePost, RemovePostWithChildren}; +use lemmy_db_views_post::api::{PostResponse, RemovePost}; use lemmy_diesel_utils::traits::Crud; use lemmy_utils::error::LemmyResult; @@ -100,7 +100,7 @@ pub async fn remove_post( } pub async fn remove_post_with_children( - Json(data): Json, + Json(data): Json, context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { diff --git a/crates/db_views/comment/src/api.rs b/crates/db_views/comment/src/api.rs index 6c4b587af6..28db6612c0 100644 --- a/crates/db_views/comment/src/api.rs +++ b/crates/db_views/comment/src/api.rs @@ -128,17 +128,6 @@ pub struct RemoveComment { pub reason: String, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] -#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] -/// Remove a comment and all its children (only doable by mods). -pub struct RemoveCommentWithChildren { - pub comment_id: CommentId, - pub removed: bool, - pub reason: String, -} - #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] diff --git a/crates/db_views/post/src/api.rs b/crates/db_views/post/src/api.rs index 5f9a922043..fd564ed6a4 100644 --- a/crates/db_views/post/src/api.rs +++ b/crates/db_views/post/src/api.rs @@ -249,17 +249,6 @@ pub struct RemovePost { pub reason: String, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] -#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] -/// Remove a post and all its children (only doable by mods). -pub struct RemovePostWithChildren { - pub post_id: PostId, - pub removed: bool, - pub reason: String, -} - #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] From e5d009a9838bf26a409abd37e15f59fb06ac3559 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Mon, 9 Feb 2026 06:49:37 +0000 Subject: [PATCH 11/16] Merge API endpoints --- crates/api/api/src/comment/mod.rs | 1 - .../api/src/comment/remove_with_children.rs | 89 ----------------- crates/api/api/src/site/purge/comment.rs | 2 +- crates/api/api/src/site/purge/post.rs | 2 +- crates/api/api_crud/src/comment/remove.rs | 88 +++++++++++------ crates/api/api_crud/src/post/remove.rs | 96 +++++-------------- crates/api/api_utils/src/send_activity.rs | 4 +- crates/api/routes/src/lib.rs | 11 +-- crates/apub/activities/src/lib.rs | 4 +- crates/db_views/comment/src/api.rs | 3 + crates/db_views/post/src/api.rs | 3 + 11 files changed, 98 insertions(+), 205 deletions(-) delete mode 100644 crates/api/api/src/comment/remove_with_children.rs diff --git a/crates/api/api/src/comment/mod.rs b/crates/api/api/src/comment/mod.rs index 0211320b2d..d6d8339eb1 100644 --- a/crates/api/api/src/comment/mod.rs +++ b/crates/api/api/src/comment/mod.rs @@ -2,5 +2,4 @@ pub mod distinguish; pub mod like; pub mod list_comment_likes; pub mod lock; -pub mod remove_with_children; pub mod save; diff --git a/crates/api/api/src/comment/remove_with_children.rs b/crates/api/api/src/comment/remove_with_children.rs deleted file mode 100644 index c119baf96e..0000000000 --- a/crates/api/api/src/comment/remove_with_children.rs +++ /dev/null @@ -1,89 +0,0 @@ -use activitypub_federation::config::Data; -use actix_web::web::Json; -use lemmy_api_utils::{ - build_response::build_comment_response, - context::LemmyContext, - send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_mod_action, remove_or_restore_comment_thread}, -}; -use lemmy_db_schema::source::{comment_report::CommentReport, local_user::LocalUser}; -use lemmy_db_views_comment::{ - CommentView, - api::{CommentResponse, RemoveComment}, -}; -use lemmy_db_views_local_user::LocalUserView; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; - -pub async fn remove_comment_with_children( - Json(data): Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let comment_id = data.comment_id; - let local_instance_id = local_user_view.person.instance_id; - let orig_comment = CommentView::read( - &mut context.pool(), - comment_id, - Some(&local_user_view.local_user), - local_instance_id, - ) - .await?; - - check_community_mod_action( - &local_user_view, - &orig_comment.community, - false, - &mut context.pool(), - ) - .await?; - - LocalUser::is_higher_mod_or_admin_check( - &mut context.pool(), - orig_comment.community.id, - local_user_view.person.id, - vec![orig_comment.creator.id], - ) - .await?; - - let updated_comments = remove_or_restore_comment_thread( - &orig_comment.comment, - local_user_view.person.id, - data.removed, - &data.reason, - &context, - ) - .await?; - - let orig_comment_id = orig_comment.comment.id; - let updated_comment = updated_comments - .iter() - .find(|c| c.id == orig_comment_id) - .ok_or(LemmyErrorType::CouldntUpdate)?; - - CommentReport::resolve_all_for_thread( - &mut context.pool(), - &orig_comment.comment.path, - local_user_view.person.id, - ) - .await?; - - ActivityChannel::submit_activity( - SendActivityData::RemoveComment { - comment: updated_comment.clone(), - moderator: local_user_view.person.clone(), - community: orig_comment.community.clone(), - reason: data.reason.clone(), - with_replies: data.removed.into(), - }, - &context, - )?; - - build_comment_response( - &context, - comment_id, - local_user_view.into(), - local_instance_id, - ) - .await - .map(Json) -} diff --git a/crates/api/api/src/site/purge/comment.rs b/crates/api/api/src/site/purge/comment.rs index a5910a4d31..966371b74d 100644 --- a/crates/api/api/src/site/purge/comment.rs +++ b/crates/api/api/src/site/purge/comment.rs @@ -63,7 +63,7 @@ pub async fn purge_comment( moderator: local_user_view.person.clone(), community: comment_view.community, reason: data.reason.clone(), - with_replies: None, + with_replies: false, }, &context, )?; diff --git a/crates/api/api/src/site/purge/post.rs b/crates/api/api/src/site/purge/post.rs index 80f5dcc52a..542475b962 100644 --- a/crates/api/api/src/site/purge/post.rs +++ b/crates/api/api/src/site/purge/post.rs @@ -50,7 +50,7 @@ pub async fn purge_post( moderator: local_user_view.person.clone(), reason: data.reason.clone(), removed: true, - with_replies: None, + with_replies: false, }, &context, )?; diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index e7ef78034e..f8b2f1998a 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -5,7 +5,7 @@ use lemmy_api_utils::{ context::LemmyContext, notify::notify_mod_action, send_activity::{ActivityChannel, SendActivityData}, - utils::check_community_mod_action, + utils::{check_community_mod_action, remove_or_restore_comment_thread}, }; use lemmy_db_schema::{ source::{ @@ -55,36 +55,68 @@ pub async fn remove_comment( ) .await?; - // Don't allow removing or restoring comment which was deleted by user, as it would reveal - // the comment text in mod log. - if orig_comment.comment.deleted { - return Err(LemmyErrorType::CouldntUpdate.into()); - } + let updated_comment = if let Some(remove_children) = data.remove_children { + let updated_comments = remove_or_restore_comment_thread( + &orig_comment.comment, + local_user_view.person.id, + remove_children, + &data.reason, + &context, + ) + .await?; - // Do the remove - let removed = data.removed; - let updated_comment = Comment::update( - &mut context.pool(), - comment_id, - &CommentUpdateForm { - removed: Some(removed), - ..Default::default() - }, - ) - .await?; + let orig_comment_id = orig_comment.comment.id; + let updated_comment = updated_comments + .iter() + .find(|c| c.id == orig_comment_id) + .ok_or(LemmyErrorType::CouldntUpdate)?; + + CommentReport::resolve_all_for_thread( + &mut context.pool(), + &orig_comment.comment.path, + local_user_view.person.id, + ) + .await?; + + updated_comment.clone() + } else { + // Don't allow removing or restoring comment which was deleted by user, as it would reveal + // the comment text in mod log. + if orig_comment.comment.deleted { + return Err(LemmyErrorType::CouldntUpdate.into()); + } - CommentReport::resolve_all_for_object(&mut context.pool(), comment_id, local_user_view.person.id) + // Do the remove + let removed = data.removed; + let updated_comment = Comment::update( + &mut context.pool(), + comment_id, + &CommentUpdateForm { + removed: Some(removed), + ..Default::default() + }, + ) .await?; - // Mod tables - let form = ModlogInsertForm::mod_remove_comment( - local_user_view.person.id, - &orig_comment.comment, - removed, - &data.reason, - ); - let actions = Modlog::create(&mut context.pool(), &[form]).await?; - notify_mod_action(actions, context.app_data()); + CommentReport::resolve_all_for_object( + &mut context.pool(), + comment_id, + local_user_view.person.id, + ) + .await?; + + // Mod tables + let form = ModlogInsertForm::mod_remove_comment( + local_user_view.person.id, + &orig_comment.comment, + removed, + &data.reason, + ); + let actions = Modlog::create(&mut context.pool(), &[form]).await?; + notify_mod_action(actions, context.app_data()); + + updated_comment + }; let updated_comment_id = updated_comment.id; @@ -94,7 +126,7 @@ pub async fn remove_comment( moderator: local_user_view.person.clone(), community: orig_comment.community, reason: data.reason.clone(), - with_replies: None, + with_replies: data.remove_children.unwrap_or_default(), }, &context, )?; diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index acd90a5529..5246004eef 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -8,7 +8,6 @@ use lemmy_api_utils::{ utils::{check_community_mod_action, remove_or_restore_post_comments}, }; use lemmy_db_schema::{ - newtypes::PostId, source::{ comment_report::CommentReport, community::Community, @@ -24,20 +23,21 @@ use lemmy_db_views_post::api::{PostResponse, RemovePost}; use lemmy_diesel_utils::traits::Crud; use lemmy_utils::error::LemmyResult; -async fn do_remove( - context: &Data, - post_id: PostId, - removed: bool, - reason: &str, - local_user_view: &LocalUserView, -) -> LemmyResult<(Post, Community)> { +pub async fn remove_post( + Json(data): Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let post_id = data.post_id; + let removed = data.remove_children.unwrap_or(data.removed); + // We cannot use PostView to avoid a database read here, as it doesn't return removed items // by default. So we would have to pass in `is_mod_or_admin`, but that is impossible without // knowing which community the post belongs to. let orig_post = Post::read(&mut context.pool(), post_id).await?; let community = Community::read(&mut context.pool(), orig_post.community_id).await?; - check_community_mod_action(local_user_view, &community, false, &mut context.pool()).await?; + check_community_mod_action(&local_user_view, &community, false, &mut context.pool()).await?; LocalUser::is_higher_mod_or_admin_check( &mut context.pool(), @@ -62,78 +62,32 @@ async fn do_remove( .await?; // Mod tables - let form = ModlogInsertForm::mod_remove_post(local_user_view.person.id, &post, removed, reason); + let form = + ModlogInsertForm::mod_remove_post(local_user_view.person.id, &post, removed, &data.reason); let action = Modlog::create(&mut context.pool(), &[form]).await?; notify_mod_action(action, context.app_data()); - Ok((post, community)) -} - -pub async fn remove_post( - Json(data): Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let post_id = data.post_id; - - let (post, community) = do_remove( - &context, - post_id, - data.removed, - &data.reason, - &local_user_view, - ) - .await?; - - ActivityChannel::submit_activity( - SendActivityData::RemovePost { - post, - moderator: local_user_view.person.clone(), - reason: data.reason.clone(), - removed: data.removed, - with_replies: None, - }, - &context, - )?; - - build_post_response(&context, community.id, local_user_view, post_id).await -} - -pub async fn remove_post_with_children( - Json(data): Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let post_id = data.post_id; - - let (post, community) = do_remove( - &context, - post_id, - data.removed, - &data.reason, - &local_user_view, - ) - .await?; - - remove_or_restore_post_comments( - &post, - local_user_view.person.id, - data.removed, - &data.reason, - &context, - ) - .await?; - - CommentReport::resolve_all_for_post(&mut context.pool(), post.id, local_user_view.person.id) + if data.remove_children.is_some() { + remove_or_restore_post_comments( + &post, + local_user_view.person.id, + removed, + &data.reason, + &context, + ) .await?; + CommentReport::resolve_all_for_post(&mut context.pool(), post.id, local_user_view.person.id) + .await?; + } + ActivityChannel::submit_activity( SendActivityData::RemovePost { post, moderator: local_user_view.person.clone(), reason: data.reason.clone(), - removed: data.removed, - with_replies: Some(data.removed), + removed: removed, + with_replies: data.remove_children.unwrap_or_default(), }, &context, )?; diff --git a/crates/api/api_utils/src/send_activity.rs b/crates/api/api_utils/src/send_activity.rs index fbf561fd65..1446c0f2aa 100644 --- a/crates/api/api_utils/src/send_activity.rs +++ b/crates/api/api_utils/src/send_activity.rs @@ -39,7 +39,7 @@ pub enum SendActivityData { moderator: Person, reason: String, removed: bool, - with_replies: Option, + with_replies: bool, }, LockPost(Post, Person, bool, String), FeaturePost(Post, Person, bool), @@ -51,7 +51,7 @@ pub enum SendActivityData { moderator: Person, community: Community, reason: String, - with_replies: Option, + with_replies: bool, }, LockComment(Comment, Person, bool, String), LikePostOrComment { diff --git a/crates/api/routes/src/lib.rs b/crates/api/routes/src/lib.rs index f296344155..6e72c0ba32 100644 --- a/crates/api/routes/src/lib.rs +++ b/crates/api/routes/src/lib.rs @@ -5,7 +5,6 @@ use lemmy_api::{ like::like_comment, list_comment_likes::list_comment_likes, lock::lock_comment, - remove_with_children::remove_comment_with_children, save::save_comment, }, community::{ @@ -141,7 +140,7 @@ use lemmy_api_crud::{ create::create_post, delete::delete_post, read::get_post, - remove::{remove_post, remove_post_with_children}, + remove::remove_post, update::edit_post, }, private_message::{ @@ -279,10 +278,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("", put().to(edit_post)) .route("", delete().to(delete_post)) .route("/remove", post().to(remove_post)) - .route( - "/remove_with_children", - post().to(remove_post_with_children), - ) .route("/mark_as_read", post().to(mark_post_as_read)) .route("/mark_as_read/many", post().to(mark_posts_as_read)) .route("/hide", post().to(hide_post)) @@ -311,10 +306,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("", put().to(edit_comment)) .route("", delete().to(delete_comment)) .route("/remove", post().to(remove_comment)) - .route( - "/remove_with_children", - post().to(remove_comment_with_children), - ) .route("/distinguish", post().to(distinguish_comment)) .route("/like", post().to(like_comment)) .route("/like/list", get().to(list_comment_likes)) diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 9a7ee9b2e4..8dd4d0a127 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -193,7 +193,7 @@ pub async fn match_outgoing_activities( DeletableObjects::Post(post.into()), Some(reason), removed, - with_replies, + Some(with_replies), &context, ) .await @@ -240,7 +240,7 @@ pub async fn match_outgoing_activities( deletable, Some(reason), is_removed, - with_replies, + Some(with_replies), &context, ) .await diff --git a/crates/db_views/comment/src/api.rs b/crates/db_views/comment/src/api.rs index 28db6612c0..69f1a4dac9 100644 --- a/crates/db_views/comment/src/api.rs +++ b/crates/db_views/comment/src/api.rs @@ -126,6 +126,9 @@ pub struct RemoveComment { pub comment_id: CommentId, pub removed: bool, pub reason: String, + /// Setting this will override whatever `removed` was set to, + /// leave as null or unset to act just on the comment itself. + pub remove_children: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/db_views/post/src/api.rs b/crates/db_views/post/src/api.rs index fd564ed6a4..33d57ccc9c 100644 --- a/crates/db_views/post/src/api.rs +++ b/crates/db_views/post/src/api.rs @@ -247,6 +247,9 @@ pub struct RemovePost { pub post_id: PostId, pub removed: bool, pub reason: String, + /// Setting this will override whatever `removed` was set to, + /// leave as null or unset to act just on the post itself. + pub remove_children: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] From af39563e67e6b2a18d66a7fbbaabfa9aef2154cb Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Mon, 9 Feb 2026 07:10:30 +0000 Subject: [PATCH 12/16] Fix API tests --- api_tests/src/comment.spec.ts | 3 +-- api_tests/src/shared.ts | 16 ++-------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index bf4b14a863..dc072b22b8 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -16,7 +16,6 @@ import { editComment, deleteComment, removeComment, - removeCommentWithChildren, resolvePost, unfollowRemotes, createCommunity, @@ -1023,7 +1022,7 @@ test("Remove children", async () => { throw "unable to locate comment on alpha"; } - await removeCommentWithChildren(alpha, true, commentOnAlpha.comment.id); + await removeComment(alpha, true, commentOnAlpha.comment.id, true); let post2 = await getPost(alpha, post.post_view.post.id); expect(post2.post_view.post.comments).toBe(0); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index cc57175e9f..117d105565 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -74,7 +74,6 @@ import { PrivateMessageResponse, Register, RemoveComment, - RemoveCommentWithChildren, RemoveCommunity, RemovePost, ResolveObject, @@ -587,11 +586,13 @@ export async function removeComment( api: LemmyHttp, removed: boolean, comment_id: number, + remove_children?: boolean, ): Promise { let form: RemoveComment = { comment_id, removed, reason: "remove", + remove_children, }; return api.removeComment(form); } @@ -608,19 +609,6 @@ export async function likeComment( return api.likeComment(form); } -export async function removeCommentWithChildren( - api: LemmyHttp, - removed: boolean, - comment_id: number, -): Promise { - let form: RemoveCommentWithChildren = { - comment_id, - removed, - reason: "reason", - }; - return api.removeCommentWithChildren(form); -} - export async function createCommunity( api: LemmyHttp, name_: string = randomString(10), From ab47f8f02d014bf4d85d4f4585094ea96feae90b Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Mon, 9 Feb 2026 07:45:52 +0000 Subject: [PATCH 13/16] Clippy --- crates/api/api_crud/src/post/remove.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 5246004eef..34d1a41dcf 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -86,7 +86,7 @@ pub async fn remove_post( post, moderator: local_user_view.person.clone(), reason: data.reason.clone(), - removed: removed, + removed, with_replies: data.remove_children.unwrap_or_default(), }, &context, From 9d282b1ac85b2ee3d76f6de48af3ac87a4f89eb5 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Tue, 10 Feb 2026 04:10:16 +0000 Subject: [PATCH 14/16] Update test package --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api_tests/package.json b/api_tests/package.json index dd59f06f6a..f66b7b1962 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -33,7 +33,7 @@ "eslint-plugin-prettier": "^5.5.0", "jest": "^30.0.0", "joi": "^18.0.0", - "lemmy-js-client": "1.0.0-rename-community-tag.0", + "lemmy-js-client": "1.0.0-remove-children.0", "lemmy-js-client-019": "npm:lemmy-js-client@0.19.9", "prettier": "^3.5.3", "ts-jest": "^29.4.0", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 8d23d01624..0ad8a97e37 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^18.0.0 version: 18.0.2 lemmy-js-client: - specifier: 1.0.0-rename-community-tag.0 - version: 1.0.0-rename-community-tag.0 + specifier: 1.0.0-remove-children.0 + version: 1.0.0-remove-children.0 lemmy-js-client-019: specifier: npm:lemmy-js-client@0.19.9 version: lemmy-js-client@0.19.9 @@ -1648,8 +1648,8 @@ packages: lemmy-js-client@0.19.9: resolution: {integrity: sha512-MjeKtmtO8M9wHiKtm60LpZVd7ieI+4yctwwRhZTaxv6yUDI38bhltq8jFYaMDNQ3PKVHUhn33oDuGVnvV1sxKw==} - lemmy-js-client@1.0.0-rename-community-tag.0: - resolution: {integrity: sha512-3ZtoMPFWH/7HKhr795XHqvJkJuDyenxtx32niNs4lTAjHWa18tpBJqFbC1cRyh5RvYIuZQIdqWI2e7VXw7AHQQ==} + lemmy-js-client@1.0.0-remove-children.0: + resolution: {integrity: sha512-smzTDJl1dRhc20x+son2bwjvtZw14ILNPaRrcZxfwbed0P8wqXzEQa1CJQvRvz3SI6b3lvnh+9UDBGHIoCL5KA==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -4356,7 +4356,7 @@ snapshots: lemmy-js-client@0.19.9: {} - lemmy-js-client@1.0.0-rename-community-tag.0: + lemmy-js-client@1.0.0-remove-children.0: dependencies: '@tsoa/runtime': 6.6.0 transitivePeerDependencies: From ba654529a7198b7ad54f17eaba9ba4d499ed9d28 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Tue, 10 Feb 2026 04:10:43 +0000 Subject: [PATCH 15/16] Move actions out of function --- crates/api/api_crud/src/comment/remove.rs | 35 +++++++--- crates/api/api_crud/src/post/remove.rs | 30 ++++++--- crates/api/api_utils/src/utils.rs | 65 ++----------------- crates/apub/activities/src/deletion/delete.rs | 50 ++++++++++++-- .../activities/src/deletion/undo_delete.rs | 50 ++++++++++++-- 5 files changed, 136 insertions(+), 94 deletions(-) diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index f8b2f1998a..45b4aa4b72 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -5,7 +5,7 @@ use lemmy_api_utils::{ context::LemmyContext, notify::notify_mod_action, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_mod_action, remove_or_restore_comment_thread}, + utils::check_community_mod_action, }; use lemmy_db_schema::{ source::{ @@ -56,20 +56,35 @@ pub async fn remove_comment( .await?; let updated_comment = if let Some(remove_children) = data.remove_children { - let updated_comments = remove_or_restore_comment_thread( - &orig_comment.comment, - local_user_view.person.id, + let updated_comments: Vec = Comment::update_removed_for_comment_and_children( + &mut context.pool(), + &orig_comment.comment.path, remove_children, - &data.reason, - &context, ) .await?; - let orig_comment_id = orig_comment.comment.id; let updated_comment = updated_comments .iter() - .find(|c| c.id == orig_comment_id) - .ok_or(LemmyErrorType::CouldntUpdate)?; + .find(|c| c.id == comment_id) + .ok_or(LemmyErrorType::CouldntUpdate)? + .clone(); + + let forms: Vec<_> = updated_comments + .iter() + // Filter out deleted comments here so their content doesn't show up in the modlog. + .filter(|c| !c.deleted) + .map(|comment| { + ModlogInsertForm::mod_remove_comment( + local_user_view.person.id, + comment, + remove_children, + &data.reason, + ) + }) + .collect(); + + let actions = Modlog::create(&mut context.pool(), &forms).await?; + notify_mod_action(actions, &context); CommentReport::resolve_all_for_thread( &mut context.pool(), @@ -78,7 +93,7 @@ pub async fn remove_comment( ) .await?; - updated_comment.clone() + updated_comment } else { // Don't allow removing or restoring comment which was deleted by user, as it would reveal // the comment text in mod log. diff --git a/crates/api/api_crud/src/post/remove.rs b/crates/api/api_crud/src/post/remove.rs index 34d1a41dcf..20ce4c55fa 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -5,10 +5,11 @@ use lemmy_api_utils::{ context::LemmyContext, notify::notify_mod_action, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_mod_action, remove_or_restore_post_comments}, + utils::check_community_mod_action, }; use lemmy_db_schema::{ source::{ + comment::Comment, comment_report::CommentReport, community::Community, local_user::LocalUser, @@ -68,14 +69,25 @@ pub async fn remove_post( notify_mod_action(action, context.app_data()); if data.remove_children.is_some() { - remove_or_restore_post_comments( - &post, - local_user_view.person.id, - removed, - &data.reason, - &context, - ) - .await?; + let updated_comments: Vec = + Comment::update_removed_for_post(&mut context.pool(), post_id, removed).await?; + + let forms: Vec<_> = updated_comments + .iter() + // Filter out deleted comments here so their content doesn't show up in the modlog. + .filter(|c| !c.deleted) + .map(|comment| { + ModlogInsertForm::mod_remove_comment( + local_user_view.person.id, + comment, + removed, + &data.reason, + ) + }) + .collect(); + + let actions = Modlog::create(&mut context.pool(), &forms).await?; + notify_mod_action(actions, &context); CommentReport::resolve_all_for_post(&mut context.pool(), post.id, local_user_view.person.id) .await?; diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index ba0acd060c..7cf426ec4a 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -1,7 +1,6 @@ use crate::{ claims::Claims, context::LemmyContext, - notify::notify_mod_action, request::{delete_image_alias, fetch_pictrs_proxied_image_details, purge_image_from_pictrs_url}, }; use actix_web::{HttpRequest, http::header::Header}; @@ -626,14 +625,16 @@ async fn create_modlog_entries_for_removed_or_restored_comments( comments: &[Comment], removed: bool, reason: &str, -) -> LemmyResult> { +) -> LemmyResult<()> { // Build the forms let forms: Vec<_> = comments .iter() .map(|comment| ModlogInsertForm::mod_remove_comment(mod_person_id, comment, removed, reason)) .collect(); - Modlog::create(pool, &forms).await + Modlog::create(pool, &forms).await?; + + Ok(()) } pub async fn remove_or_restore_user_data_in_community( @@ -710,64 +711,6 @@ pub async fn purge_user_account( Ok(()) } -pub async fn remove_or_restore_comment_thread( - comment: &Comment, - mod_person_id: PersonId, - removed: bool, - reason: &str, - context: &LemmyContext, -) -> LemmyResult> { - let removed_comments: Vec = - Comment::update_removed_for_comment_and_children(&mut context.pool(), &comment.path, removed) - .await? - // Filter out deleted comments here so their content doesn't show up in the modlog. - .into_iter() - .filter(|c| !c.deleted) - .collect(); - - let actions = create_modlog_entries_for_removed_or_restored_comments( - &mut context.pool(), - mod_person_id, - &removed_comments, - removed, - reason, - ) - .await?; - - notify_mod_action(actions, context); - - Ok(removed_comments) -} - -pub async fn remove_or_restore_post_comments( - post: &Post, - mod_person_id: PersonId, - removed: bool, - reason: &str, - context: &LemmyContext, -) -> LemmyResult> { - let removed_comments: Vec = - Comment::update_removed_for_post(&mut context.pool(), post.id, removed) - .await? - // Filter out deleted comments here so their content doesn't show up in the modlog. - .into_iter() - .filter(|c| !c.deleted) - .collect(); - - let actions = create_modlog_entries_for_removed_or_restored_comments( - &mut context.pool(), - mod_person_id, - &removed_comments, - removed, - reason, - ) - .await?; - - notify_mod_action(actions, context); - - Ok(removed_comments) -} - pub fn generate_followers_url(ap_id: &DbUrl) -> Result { Ok(Url::parse(&format!("{ap_id}/followers"))?.into()) } diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index f2c47fe9d2..a4a32644fa 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -5,11 +5,7 @@ use crate::{ protocol::{IdOrNestedObject, deletion::delete::Delete}, }; use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::Activity}; -use lemmy_api_utils::{ - context::LemmyContext, - notify::notify_mod_action, - utils::{remove_or_restore_comment_thread, remove_or_restore_post_comments}, -}; +use lemmy_api_utils::{context::LemmyContext, notify::notify_mod_action}; use lemmy_apub_objects::objects::person::ApubPerson; use lemmy_db_schema::{ source::{ @@ -157,14 +153,54 @@ pub(crate) async fn receive_remove_action( let remove_children = with_replies.unwrap_or_default(); if remove_children { CommentReport::resolve_all_for_post(&mut context.pool(), post.id, actor.id).await?; - remove_or_restore_post_comments(&post, actor.id, true, &reason, context).await?; + let updated_comments: Vec = + Comment::update_removed_for_post(&mut context.pool(), post.id, true).await?; + + let forms: Vec<_> = updated_comments + .iter() + // Filter out deleted comments here so their content doesn't show up in the modlog. + .filter(|c| !c.deleted) + .map(|comment| { + ModlogInsertForm::mod_remove_comment( + actor.id, + comment, + true, + &reason, + ) + }) + .collect(); + + let actions = Modlog::create(&mut context.pool(), &forms).await?; + notify_mod_action(actions, context); } } DeletableObjects::Comment(comment) => { let remove_children = with_replies.unwrap_or_default(); if remove_children { CommentReport::resolve_all_for_thread(&mut context.pool(), &comment.path, actor.id).await?; - remove_or_restore_comment_thread(&comment, actor.id, true, &reason, context).await?; + let updated_comments: Vec = Comment::update_removed_for_comment_and_children( + &mut context.pool(), + &comment.path, + true, + ) + .await?; + + let forms: Vec<_> = updated_comments + .iter() + // Filter out deleted comments here so their content doesn't show up in the modlog. + .filter(|c| !c.deleted) + .map(|comment| { + ModlogInsertForm::mod_remove_comment( + actor.id, + comment, + true, + &reason, + ) + }) + .collect(); + + let actions = Modlog::create(&mut context.pool(), &forms).await?; + notify_mod_action(actions, context); } else { CommentReport::resolve_all_for_object(&mut context.pool(), comment.id, actor.id).await?; let form = ModlogInsertForm::mod_remove_comment(actor.id, &comment, true, &reason); diff --git a/crates/apub/activities/src/deletion/undo_delete.rs b/crates/apub/activities/src/deletion/undo_delete.rs index 50a9d42254..8baa998b1b 100644 --- a/crates/apub/activities/src/deletion/undo_delete.rs +++ b/crates/apub/activities/src/deletion/undo_delete.rs @@ -4,11 +4,7 @@ use crate::{ protocol::deletion::{delete::Delete, undo_delete::UndoDelete}, }; use activitypub_federation::{config::Data, kinds::activity::UndoType, traits::Activity}; -use lemmy_api_utils::{ - context::LemmyContext, - notify::notify_mod_action, - utils::{remove_or_restore_comment_thread, remove_or_restore_post_comments}, -}; +use lemmy_api_utils::{context::LemmyContext, notify::notify_mod_action}; use lemmy_apub_objects::objects::person::ApubPerson; use lemmy_db_schema::source::{ comment::{Comment, CommentUpdateForm}, @@ -139,13 +135,53 @@ impl UndoDelete { let restore_children = with_replies.unwrap_or_default(); if restore_children { - remove_or_restore_post_comments(&post, actor.id, false, &reason, context).await?; + let updated_comments: Vec = + Comment::update_removed_for_post(&mut context.pool(), post.id, false).await?; + + let forms: Vec<_> = updated_comments + .iter() + // Filter out deleted comments here so their content doesn't show up in the modlog. + .filter(|c| !c.deleted) + .map(|comment| { + ModlogInsertForm::mod_remove_comment( + actor.id, + comment, + false, + &reason, + ) + }) + .collect(); + + let actions = Modlog::create(&mut context.pool(), &forms).await?; + notify_mod_action(actions, context); } } DeletableObjects::Comment(comment) => { let restore_children = with_replies.unwrap_or_default(); if restore_children { - remove_or_restore_comment_thread(&comment, actor.id, false, &reason, context).await?; + let updated_comments: Vec = Comment::update_removed_for_comment_and_children( + &mut context.pool(), + &comment.path, + false, + ) + .await?; + + let forms: Vec<_> = updated_comments + .iter() + // Filter out deleted comments here so their content doesn't show up in the modlog. + .filter(|c| !c.deleted) + .map(|comment| { + ModlogInsertForm::mod_remove_comment( + actor.id, + comment, + false, + &reason, + ) + }) + .collect(); + + let actions = Modlog::create(&mut context.pool(), &forms).await?; + notify_mod_action(actions, context); } else { let form = ModlogInsertForm::mod_remove_comment(actor.id, &comment, false, &reason); let action = Modlog::create(&mut context.pool(), &[form]).await?; From 7474a00ef369367eebf48664c0824183d45fdf62 Mon Sep 17 00:00:00 2001 From: flamingos-cant Date: Tue, 10 Feb 2026 04:15:32 +0000 Subject: [PATCH 16/16] fmt --- crates/apub/activities/src/deletion/delete.rs | 18 ++---------------- .../activities/src/deletion/undo_delete.rs | 18 ++---------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index a4a32644fa..71dd924de8 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -160,14 +160,7 @@ pub(crate) async fn receive_remove_action( .iter() // Filter out deleted comments here so their content doesn't show up in the modlog. .filter(|c| !c.deleted) - .map(|comment| { - ModlogInsertForm::mod_remove_comment( - actor.id, - comment, - true, - &reason, - ) - }) + .map(|comment| ModlogInsertForm::mod_remove_comment(actor.id, comment, true, &reason)) .collect(); let actions = Modlog::create(&mut context.pool(), &forms).await?; @@ -189,14 +182,7 @@ pub(crate) async fn receive_remove_action( .iter() // Filter out deleted comments here so their content doesn't show up in the modlog. .filter(|c| !c.deleted) - .map(|comment| { - ModlogInsertForm::mod_remove_comment( - actor.id, - comment, - true, - &reason, - ) - }) + .map(|comment| ModlogInsertForm::mod_remove_comment(actor.id, comment, true, &reason)) .collect(); let actions = Modlog::create(&mut context.pool(), &forms).await?; diff --git a/crates/apub/activities/src/deletion/undo_delete.rs b/crates/apub/activities/src/deletion/undo_delete.rs index 8baa998b1b..414f83307f 100644 --- a/crates/apub/activities/src/deletion/undo_delete.rs +++ b/crates/apub/activities/src/deletion/undo_delete.rs @@ -142,14 +142,7 @@ impl UndoDelete { .iter() // Filter out deleted comments here so their content doesn't show up in the modlog. .filter(|c| !c.deleted) - .map(|comment| { - ModlogInsertForm::mod_remove_comment( - actor.id, - comment, - false, - &reason, - ) - }) + .map(|comment| ModlogInsertForm::mod_remove_comment(actor.id, comment, false, &reason)) .collect(); let actions = Modlog::create(&mut context.pool(), &forms).await?; @@ -170,14 +163,7 @@ impl UndoDelete { .iter() // Filter out deleted comments here so their content doesn't show up in the modlog. .filter(|c| !c.deleted) - .map(|comment| { - ModlogInsertForm::mod_remove_comment( - actor.id, - comment, - false, - &reason, - ) - }) + .map(|comment| ModlogInsertForm::mod_remove_comment(actor.id, comment, false, &reason)) .collect(); let actions = Modlog::create(&mut context.pool(), &forms).await?;