diff --git a/crates/api/api/src/site/mod_log.rs b/crates/api/api/src/site/mod_log.rs index 0922eeaca..11f7a3265 100644 --- a/crates/api/api/src/site/mod_log.rs +++ b/crates/api/api/src/site/mod_log.rs @@ -87,22 +87,25 @@ mod tests { use lemmy_api_utils::utils::remove_or_restore_user_data; use lemmy_db_schema::{ source::{ - comment::{Comment, CommentInsertForm}, + comment::{Comment, CommentActions, CommentInsertForm, CommentLikeForm}, community::{Community, CommunityInsertForm}, instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, mod_log::moderator::{ModRemoveComment, ModRemovePost}, person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, + post::{Post, PostActions, PostInsertForm, PostLikeForm}, }, - traits::Crud, + traits::{Crud, Likeable}, ModlogActionType, }; + use lemmy_db_views_comment::CommentView; use lemmy_db_views_modlog_combined::{ impls::ModlogCombinedQuery, ModRemoveCommentView, ModRemovePostView, ModlogCombinedView, }; + use lemmy_db_views_post::PostView; use pretty_assertions::assert_eq; use serial_test::serial; @@ -112,54 +115,68 @@ mod tests { let context = LemmyContext::init_test_context().await; let pool = &mut context.pool(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_mod = PersonInsertForm::test_form(inserted_instance.id, "modder"); - let inserted_mod = Person::create(pool, &new_mod).await?; + // John is the mod + let john = PersonInsertForm::test_form(instance.id, "john the modder"); + let john = Person::create(pool, &john).await?; - let new_person = PersonInsertForm::test_form(inserted_instance.id, "chrimbus"); - let inserted_person = Person::create(pool, &new_person).await?; + let sara_form = PersonInsertForm::test_form(instance.id, "sara"); + let sara = Person::create(pool, &sara_form).await?; - let new_community = CommunityInsertForm::new( - inserted_instance.id, + let sara_local_user_form = LocalUserInsertForm::test_form(sara.id); + let sara_local_user = LocalUser::create(pool, &sara_local_user_form, Vec::new()).await?; + + let community_form = CommunityInsertForm::new( + instance.id, "mod_community crepes".to_string(), "nada".to_owned(), "pubkey".to_string(), ); - let inserted_community = Community::create(pool, &new_community).await?; + let community = Community::create(pool, &community_form).await?; - let post_form_1 = PostInsertForm::new( - "A test post tubular".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post_1 = Post::create(pool, &post_form_1).await?; + let post_form_1 = PostInsertForm::new("A test post tubular".into(), sara.id, community.id); + let post_1 = Post::create(pool, &post_form_1).await?; - let post_form_2 = PostInsertForm::new( - "A test post radical".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post_2 = Post::create(pool, &post_form_2).await?; + let post_like_form_1 = PostLikeForm::new(post_1.id, sara.id, 1); + let _post_like_1 = PostActions::like(pool, &post_like_form_1).await?; - let comment_form_1 = CommentInsertForm::new( - inserted_person.id, - inserted_post_1.id, - "A test comment tubular".into(), - ); - let _inserted_comment_1 = Comment::create(pool, &comment_form_1, None).await?; + let post_form_2 = PostInsertForm::new("A test post radical".into(), sara.id, community.id); + let post_2 = Post::create(pool, &post_form_2).await?; - let comment_form_2 = CommentInsertForm::new( - inserted_person.id, - inserted_post_2.id, - "A test comment radical".into(), + let comment_form_1 = + CommentInsertForm::new(sara.id, post_1.id, "A test comment tubular".into()); + let comment_1 = Comment::create(pool, &comment_form_1, None).await?; + + let comment_like_form_1 = CommentLikeForm::new(sara.id, comment_1.id, 1); + let _comment_like_1 = CommentActions::like(pool, &comment_like_form_1).await?; + + let comment_form_2 = + CommentInsertForm::new(sara.id, post_2.id, "A test comment radical".into()); + let _comment_2 = Comment::create(pool, &comment_form_2, None).await?; + + // Read saras post to make sure it has a like + let post_view_1 = + PostView::read(pool, post_1.id, Some(&sara_local_user), instance.id, false).await?; + assert_eq!(1, post_view_1.post.score); + assert_eq!( + Some(1), + post_view_1.post_actions.and_then(|pa| pa.like_score) + ); + + // Read saras comment to make sure it has a like + let comment_view_1 = + CommentView::read(pool, comment_1.id, Some(&sara_local_user), instance.id).await?; + assert_eq!(1, comment_view_1.post.score); + assert_eq!( + Some(1), + comment_view_1.comment_actions.and_then(|ca| ca.like_score) ); - let _inserted_comment_2 = Comment::create(pool, &comment_form_2, None).await?; // Remove the user data remove_or_restore_user_data( - inserted_mod.id, - inserted_person.id, + john.id, + sara.id, true, &Some("a remove reason".to_string()), &context, @@ -217,10 +234,26 @@ mod tests { ], )); + // Verify that the likes got removed + // post + let post_view_1 = + PostView::read(pool, post_1.id, Some(&sara_local_user), instance.id, false).await?; + assert_eq!(0, post_view_1.post.score); + assert_eq!(None, post_view_1.post_actions.and_then(|pa| pa.like_score)); + + // comment + let comment_view_1 = + CommentView::read(pool, comment_1.id, Some(&sara_local_user), instance.id).await?; + assert_eq!(0, comment_view_1.post.score); + assert_eq!( + None, + comment_view_1.comment_actions.and_then(|ca| ca.like_score) + ); + // Now restore the content, and make sure it got appended remove_or_restore_user_data( - inserted_mod.id, - inserted_person.id, + john.id, + sara.id, false, &Some("a restore reason".to_string()), &context, @@ -297,7 +330,7 @@ mod tests { ], )); - Instance::delete(pool, inserted_instance.id).await?; + Instance::delete(pool, instance.id).await?; Ok(()) } diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index f3bee60cb..0554c6a5f 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -623,11 +623,15 @@ pub async fn remove_or_restore_user_data( ) .await?; } + + // Remove post and comment votes + PostActions::remove_all_likes(pool, banned_person_id).await?; + CommentActions::remove_all_likes(pool, banned_person_id).await?; } // Posts let removed_or_restored_posts = - Post::update_removed_for_creator(pool, banned_person_id, None, None, removed).await?; + Post::update_removed_for_creator(pool, banned_person_id, removed).await?; create_modlog_entries_for_removed_or_restored_posts( pool, mod_person_id, @@ -709,10 +713,18 @@ pub async fn remove_or_restore_user_data_in_community( reason: &Option, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { + // These actions are only possible when removing, not restoring + if remove { + // Remove post and comment votes + PostActions::remove_likes_in_community(pool, banned_person_id, community_id).await?; + CommentActions::remove_likes_in_community(pool, banned_person_id, community_id).await?; + } + // Posts let posts = - Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), None, remove) + Post::update_removed_for_creator_and_community(pool, banned_person_id, community_id, remove) .await?; + create_modlog_entries_for_removed_or_restored_posts( pool, mod_person_id, diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index aac1f9331..4892fc509 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -210,14 +210,8 @@ async fn update_removed_for_instance( removed: bool, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - Post::update_removed_for_creator( - pool, - blocked_person.id, - None, - Some(site.instance_id), - removed, - ) - .await?; + Post::update_removed_for_creator_and_instance(pool, blocked_person.id, site.instance_id, removed) + .await?; Comment::update_removed_for_creator_and_instance( pool, blocked_person.id, diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 06f3b1bff..e17ad8838 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -70,31 +70,61 @@ impl Comment { .with_lemmy_type(LemmyErrorType::CouldntUpdateComment) } + /// Diesel can't update from join unfortunately, so you'll need to loop over these + async fn creator_comment_ids_in_community( + pool: &mut DbPool<'_>, + creator_id: PersonId, + community_id: CommunityId, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + comment::table + .inner_join(post::table) + .filter(comment::creator_id.eq(creator_id)) + .filter(post::community_id.eq(community_id)) + .select(comment::id) + .load::(conn) + .await + .with_lemmy_type(LemmyErrorType::NotFound) + } + + /// Diesel can't update from join unfortunately, so you'll need to loop over these + async fn creator_comment_ids_in_instance( + pool: &mut DbPool<'_>, + creator_id: PersonId, + instance_id: InstanceId, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + let community_join = community::table.on(post::community_id.eq(community::id)); + + comment::table + .inner_join(post::table) + .inner_join(community_join) + .filter(comment::creator_id.eq(creator_id)) + .filter(community::instance_id.eq(instance_id)) + .select(comment::id) + .load::(conn) + .await + .with_lemmy_type(LemmyErrorType::NotFound) + } + pub async fn update_removed_for_creator_and_community( pool: &mut DbPool<'_>, creator_id: PersonId, community_id: CommunityId, removed: bool, ) -> LemmyResult> { + let comment_ids = + Self::creator_comment_ids_in_community(pool, creator_id, community_id).await?; + let conn = &mut get_conn(pool).await?; - // Diesel can't update from join unfortunately, so you'll need to loop over these - let comment_ids = comment::table - .inner_join(post::table) - .filter(comment::creator_id.eq(creator_id)) - .filter(post::community_id.eq(community_id)) - .select(comment::id) - .load::(conn) - .await?; - - let form = &CommentUpdateForm { - removed: Some(removed), - ..Default::default() - }; - update(comment::table) .filter(comment::id.eq_any(comment_ids.clone())) - .set(form) + .set(( + comment::removed.eq(removed), + comment::updated_at.eq(Utc::now()), + )) .execute(conn) .await?; @@ -107,26 +137,15 @@ impl Comment { instance_id: InstanceId, removed: bool, ) -> LemmyResult> { + let comment_ids = Self::creator_comment_ids_in_instance(pool, creator_id, instance_id).await?; let conn = &mut get_conn(pool).await?; - // Diesel can't update from join unfortunately, so you'll need to loop over these - let community_join = community::table.on(post::community_id.eq(community::id)); - let comment_ids = comment::table - .inner_join(post::table) - .inner_join(community_join) - .filter(comment::creator_id.eq(creator_id)) - .filter(community::instance_id.eq(instance_id)) - .select(comment::id) - .load::(conn) - .await?; - - let form = &CommentUpdateForm { - removed: Some(removed), - ..Default::default() - }; update(comment::table) .filter(comment::id.eq_any(comment_ids.clone())) - .set(form) + .set(( + comment::removed.eq(removed), + comment::updated_at.eq(Utc::now()), + )) .execute(conn) .await?; Ok(comment_ids) @@ -273,6 +292,40 @@ impl Likeable for CommentActions { .await .with_lemmy_type(LemmyErrorType::CouldntLikeComment) } + + async fn remove_all_likes( + pool: &mut DbPool<'_>, + creator_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + + uplete::new(comment_actions::table.filter(comment_actions::person_id.eq(creator_id))) + .set_null(comment_actions::like_score) + .set_null(comment_actions::liked_at) + .get_result(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdateComment) + } + + async fn remove_likes_in_community( + pool: &mut DbPool<'_>, + creator_id: PersonId, + community_id: CommunityId, + ) -> LemmyResult { + let comment_ids = + Comment::creator_comment_ids_in_community(pool, creator_id, community_id).await?; + + let conn = &mut get_conn(pool).await?; + + uplete::new( + comment_actions::table.filter(comment_actions::comment_id.eq_any(comment_ids.clone())), + ) + .set_null(comment_actions::like_score) + .set_null(comment_actions::liked_at) + .get_result(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdateComment) + } } impl Saveable for CommentActions { diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 8e96b40af..5ace463b1 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -152,31 +152,67 @@ impl Post { .with_lemmy_type(LemmyErrorType::CouldntUpdatePost) } - pub async fn update_removed_for_creator( + async fn creator_post_ids_in_community( pool: &mut DbPool<'_>, creator_id: PersonId, - community_id: Option, - instance_id: Option, + community_id: CommunityId, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + post::table + .filter(post::creator_id.eq(creator_id)) + .filter(post::community_id.eq(community_id)) + .select(post::id) + .load::(conn) + .await + .with_lemmy_type(LemmyErrorType::NotFound) + } + + /// Diesel can't update from join unfortunately, so you sometimes need to fetch a list of post_ids + /// for a creator. + async fn creator_post_ids_in_instance( + pool: &mut DbPool<'_>, + creator_id: PersonId, + instance_id: InstanceId, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + post::table + .inner_join(community::table) + .filter(post::creator_id.eq(creator_id)) + .filter(community::instance_id.eq(instance_id)) + .select(post::id) + .load::(conn) + .await + .with_lemmy_type(LemmyErrorType::NotFound) + } + + pub async fn update_removed_for_creator_and_community( + pool: &mut DbPool<'_>, + creator_id: PersonId, + community_id: CommunityId, removed: bool, ) -> LemmyResult> { let conn = &mut get_conn(pool).await?; - // Diesel can't update from join unfortunately, so you'll need to loop over these - let community_join = community::table.on(post::community_id.eq(community::id)); - let mut posts_query = post::table - .inner_join(community_join) + update(post::table) .filter(post::creator_id.eq(creator_id)) - .into_boxed(); + .filter(post::community_id.eq(community_id)) + .set((post::removed.eq(removed), post::updated_at.eq(Utc::now()))) + .get_results::(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost) + } - if let Some(community_id) = community_id { - posts_query = posts_query.filter(post::community_id.eq(community_id)); - } + pub async fn update_removed_for_creator_and_instance( + pool: &mut DbPool<'_>, + creator_id: PersonId, + instance_id: InstanceId, + removed: bool, + ) -> LemmyResult> { + let post_ids = Self::creator_post_ids_in_instance(pool, creator_id, instance_id).await?; - if let Some(instance_id) = instance_id { - posts_query = posts_query.filter(community::instance_id.eq(instance_id)); - } - - let post_ids = posts_query.select(post::id).load::(conn).await?; + let conn = &mut get_conn(pool).await?; update(post::table) .filter(post::id.eq_any(post_ids.clone())) @@ -186,6 +222,21 @@ impl Post { .with_lemmy_type(LemmyErrorType::CouldntUpdatePost) } + pub async fn update_removed_for_creator( + pool: &mut DbPool<'_>, + creator_id: PersonId, + removed: bool, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + update(post::table) + .filter(post::creator_id.eq(creator_id)) + .set((post::removed.eq(removed), post::updated_at.eq(Utc::now()))) + .get_results::(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost) + } + pub fn is_post_creator(person_id: PersonId, post_creator_id: PersonId) -> bool { person_id == post_creator_id } @@ -304,6 +355,7 @@ impl Likeable for PostActions { .await .with_lemmy_type(LemmyErrorType::CouldntLikePost) } + async fn remove_like( pool: &mut DbPool<'_>, person_id: PersonId, @@ -317,6 +369,37 @@ impl Likeable for PostActions { .await .with_lemmy_type(LemmyErrorType::CouldntLikePost) } + + async fn remove_all_likes( + pool: &mut DbPool<'_>, + person_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + + uplete::new(post_actions::table.filter(post_actions::person_id.eq(person_id))) + .set_null(post_actions::like_score) + .set_null(post_actions::liked_at) + .get_result(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost) + } + + async fn remove_likes_in_community( + pool: &mut DbPool<'_>, + person_id: PersonId, + community_id: CommunityId, + ) -> LemmyResult { + let post_ids = Post::creator_post_ids_in_community(pool, person_id, community_id).await?; + + let conn = &mut get_conn(pool).await?; + + uplete::new(post_actions::table.filter(post_actions::post_id.eq_any(post_ids.clone()))) + .set_null(post_actions::like_score) + .set_null(post_actions::liked_at) + .get_result(conn) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost) + } } impl Saveable for PostActions { diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index 526b1eb73..49fc65386 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -143,6 +143,21 @@ pub trait Likeable { ) -> impl Future> + Send where Self: Sized; + + fn remove_all_likes( + pool: &mut DbPool<'_>, + creator_id: PersonId, + ) -> impl Future> + Send + where + Self: Sized; + + fn remove_likes_in_community( + pool: &mut DbPool<'_>, + creator_id: PersonId, + community_id: CommunityId, + ) -> impl Future> + Send + where + Self: Sized; } pub trait Bannable {