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: diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index db31875058..dc072b22b8 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -23,6 +23,7 @@ import { reportComment, randomString, unfollows, + getComment, getComments, getCommentParentId, resolveCommunity, @@ -981,6 +982,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 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); + + // 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..117d105565 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, @@ -381,6 +382,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, @@ -575,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); } diff --git a/crates/api/api/src/site/purge/comment.rs b/crates/api/api/src/site/purge/comment.rs index b8b43b270e..966371b74d 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: false, }, &context, )?; diff --git a/crates/api/api/src/site/purge/post.rs b/crates/api/api/src/site/purge/post.rs index 59c13863fb..542475b962 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: false, }, &context, )?; diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index 1456ca1dda..45b4aa4b72 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -55,36 +55,83 @@ 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()); - } - - // 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 = if let Some(remove_children) = data.remove_children { + let updated_comments: Vec = Comment::update_removed_for_comment_and_children( + &mut context.pool(), + &orig_comment.comment.path, + remove_children, + ) + .await?; + + let updated_comment = updated_comments + .iter() + .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_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()); + 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. + if orig_comment.comment.deleted { + 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?; + + 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,6 +141,7 @@ pub async fn remove_comment( moderator: local_user_view.person.clone(), community: orig_comment.community, reason: data.reason.clone(), + 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 778b31de4b..20ce4c55fa 100644 --- a/crates/api/api_crud/src/post/remove.rs +++ b/crates/api/api_crud/src/post/remove.rs @@ -9,6 +9,8 @@ use lemmy_api_utils::{ }; use lemmy_db_schema::{ source::{ + comment::Comment, + comment_report::CommentReport, community::Community, local_user::LocalUser, modlog::{Modlog, ModlogInsertForm}, @@ -28,6 +30,7 @@ pub async fn remove_post( 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 @@ -46,8 +49,6 @@ pub async fn remove_post( .await?; // Update the post - let post_id = data.post_id; - let removed = data.removed; let post = Post::update( &mut context.pool(), post_id, @@ -67,15 +68,41 @@ pub async fn remove_post( let action = Modlog::create(&mut context.pool(), &[form]).await?; notify_mod_action(action, context.app_data()); + if data.remove_children.is_some() { + 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?; + } + 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.unwrap_or_default(), }, &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/send_activity.rs b/crates/api/api_utils/src/send_activity.rs index 54a8e191eb..1446c0f2aa 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: bool, }, LockPost(Post, Person, bool, String), FeaturePost(Post, Person, bool), @@ -50,6 +51,7 @@ pub enum SendActivityData { moderator: Person, community: Community, reason: String, + with_replies: bool, }, LockComment(Comment, Person, bool, String), LikePostOrComment { diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index 811c98ffa1..71dd924de8 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -55,6 +55,7 @@ impl Activity for Delete { &self.actor.dereference(context).await?, self.object.id(), reason, + self.with_replies, context, ) .await @@ -78,6 +79,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 +94,7 @@ impl Delete { id, audience: community.map(|c| c.ap_id.clone().into()), remove_data: None, + with_replies, }) } } @@ -100,6 +103,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()); @@ -145,21 +149,59 @@ 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?; + 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) => { - 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?; + 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); + 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/deletion/undo_delete.rs b/crates/apub/activities/src/deletion/undo_delete.rs index 248414e309..414f83307f 100644 --- a/crates/apub/activities/src/deletion/undo_delete.rs +++ b/crates/apub/activities/src/deletion/undo_delete.rs @@ -42,6 +42,7 @@ impl Activity for UndoDelete { &self.actor.dereference(context).await?, self.object.object.id(), reason, + self.object.with_replies, context, ) .await @@ -58,9 +59,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 +89,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 +132,56 @@ impl UndoDelete { }, ) .await?; + + let restore_children = with_replies.unwrap_or_default(); + if restore_children { + 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 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 { + 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?; + 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 648b2bc014..8dd4d0a127 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 @@ -183,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( @@ -191,6 +193,7 @@ pub async fn match_outgoing_activities( DeletableObjects::Post(post.into()), Some(reason), removed, + Some(with_replies), &context, ) .await @@ -217,13 +220,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 +240,7 @@ pub async fn match_outgoing_activities( deletable, Some(reason), is_removed, + Some(with_replies), &context, ) .await @@ -273,7 +281,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 +297,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 { diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index aaee1a686f..01047023cd 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. @@ -271,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<'_>, @@ -704,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, "sharah"); + 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 3766902c14..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}; @@ -96,3 +101,51 @@ 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) + } + + 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/comment/src/api.rs b/crates/db_views/comment/src/api.rs index db88d225f4..e145d25977 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 24ead55c19..63a772b15b 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)]