Remove votes when ban + remove data for a site or community. (#5810)

* Remove votes when ban + remove data for a site or community.

- Fixes #5569

* Starting to revert cleanup.

* Finishing up.
This commit is contained in:
Dessalines 2025-06-26 03:52:13 -04:00 committed by GitHub
parent 9ccd647e02
commit d274d2bb1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 286 additions and 96 deletions

View file

@ -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(())
}

View file

@ -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<String>,
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,

View file

@ -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,

View file

@ -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<Vec<CommentId>> {
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::<CommentId>(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<Vec<CommentId>> {
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::<CommentId>(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<Vec<CommentId>> {
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::<CommentId>(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<Vec<CommentId>> {
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::<CommentId>(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<uplete::Count> {
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<uplete::Count> {
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 {

View file

@ -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<CommunityId>,
instance_id: Option<InstanceId>,
community_id: CommunityId,
) -> LemmyResult<Vec<PostId>> {
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::<PostId>(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<Vec<PostId>> {
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::<PostId>(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<Vec<Self>> {
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::<Self>(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<Vec<Self>> {
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::<PostId>(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<Vec<Self>> {
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::<Self>(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<uplete::Count> {
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<uplete::Count> {
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 {

View file

@ -143,6 +143,21 @@ pub trait Likeable {
) -> impl Future<Output = LemmyResult<uplete::Count>> + Send
where
Self: Sized;
fn remove_all_likes(
pool: &mut DbPool<'_>,
creator_id: PersonId,
) -> impl Future<Output = LemmyResult<uplete::Count>> + Send
where
Self: Sized;
fn remove_likes_in_community(
pool: &mut DbPool<'_>,
creator_id: PersonId,
community_id: CommunityId,
) -> impl Future<Output = LemmyResult<uplete::Count>> + Send
where
Self: Sized;
}
pub trait Bannable {