diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index fa241d162..8ad7fbbf8 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -818,6 +818,7 @@ Marks all user replies and mentions as read. data: { user_id: i32, ban: bool, + remove_data: Option, // Removes/Restores their comments, posts, and communities reason: Option, expires: Option, auth: String @@ -1177,6 +1178,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url` community_id: i32, user_id: i32, ban: bool, + remove_data: Option, // Removes/Restores their comments and posts for that community reason: Option, expires: Option, auth: String diff --git a/server/lemmy_db/src/comment.rs b/server/lemmy_db/src/comment.rs index 8e52d7e2d..f5a036f1a 100644 --- a/server/lemmy_db/src/comment.rs +++ b/server/lemmy_db/src/comment.rs @@ -97,16 +97,15 @@ impl Comment { comment.filter(ap_id.eq(object_id)).first::(conn) } - pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result { + pub fn permadelete_for_creator(conn: &PgConnection, for_creator_id: i32) -> Result, Error> { use crate::schema::comment::dsl::*; - - diesel::update(comment.find(comment_id)) + diesel::update(comment.filter(creator_id.eq(for_creator_id))) .set(( content.eq("*Permananently Deleted*"), deleted.eq(true), updated.eq(naive_now()), )) - .get_result::(conn) + .get_results::(conn) } pub fn update_deleted( @@ -131,6 +130,17 @@ impl Comment { .get_result::(conn) } + pub fn update_removed_for_creator( + conn: &PgConnection, + for_creator_id: i32, + new_removed: bool, + ) -> Result, Error> { + use crate::schema::comment::dsl::*; + diesel::update(comment.filter(creator_id.eq(for_creator_id))) + .set((removed.eq(new_removed), updated.eq(naive_now()))) + .get_results::(conn) + } + pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result { use crate::schema::comment::dsl::*; diesel::update(comment.find(comment_id)) diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index 7490f3664..df5f12941 100644 --- a/server/lemmy_db/src/community.rs +++ b/server/lemmy_db/src/community.rs @@ -121,6 +121,17 @@ impl Community { .get_result::(conn) } + pub fn update_removed_for_creator( + conn: &PgConnection, + for_creator_id: i32, + new_removed: bool, + ) -> Result, Error> { + use crate::schema::community::dsl::*; + diesel::update(community.filter(creator_id.eq(for_creator_id))) + .set((removed.eq(new_removed), updated.eq(naive_now()))) + .get_results::(conn) + } + pub fn update_creator( conn: &PgConnection, community_id: i32, diff --git a/server/lemmy_db/src/post.rs b/server/lemmy_db/src/post.rs index 1185aa845..d73901bb0 100644 --- a/server/lemmy_db/src/post.rs +++ b/server/lemmy_db/src/post.rs @@ -95,13 +95,13 @@ impl Post { .get_result::(conn) } - pub fn permadelete(conn: &PgConnection, post_id: i32) -> Result { + pub fn permadelete_for_creator(conn: &PgConnection, for_creator_id: i32) -> Result, Error> { use crate::schema::post::dsl::*; let perma_deleted = "*Permananently Deleted*"; let perma_deleted_url = "https://deleted.com"; - diesel::update(post.find(post_id)) + diesel::update(post.filter(creator_id.eq(for_creator_id))) .set(( name.eq(perma_deleted), url.eq(perma_deleted_url), @@ -109,7 +109,7 @@ impl Post { deleted.eq(true), updated.eq(naive_now()), )) - .get_result::(conn) + .get_results::(conn) } pub fn update_deleted( @@ -134,6 +134,26 @@ impl Post { .get_result::(conn) } + pub fn update_removed_for_creator( + conn: &PgConnection, + for_creator_id: i32, + for_community_id: Option, + new_removed: bool, + ) -> Result, Error> { + use crate::schema::post::dsl::*; + + let mut update = diesel::update(post).into_boxed(); + update = update.filter(creator_id.eq(for_creator_id)); + + if let Some(for_community_id) = for_community_id { + update = update.filter(community_id.eq(for_community_id)); + } + + update + .set((removed.eq(new_removed), updated.eq(naive_now()))) + .get_results::(conn) + } + pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result { use crate::schema::post::dsl::*; diesel::update(post.find(post_id)) diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 01c43ce11..bd6714563 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -13,8 +13,11 @@ use crate::{ use actix_web::client::Client; use anyhow::Context; use lemmy_db::{ + comment::Comment, + comment_view::CommentQueryBuilder, diesel_option_overwrite, naive_now, + post::Post, Bannable, Crud, Followable, @@ -81,6 +84,7 @@ pub struct BanFromCommunity { pub community_id: i32, user_id: i32, ban: bool, + remove_data: Option, reason: Option, expires: Option, auth: String, @@ -676,6 +680,7 @@ impl Perform for BanFromCommunity { let user = get_user_from_jwt(&data.auth, pool).await?; let community_id = data.community_id; + let banned_user_id = data.user_id; // Verify that only mods or admins can ban is_mod_or_admin(pool, user.id, community_id).await?; @@ -697,6 +702,34 @@ impl Perform for BanFromCommunity { } } + // Remove/Restore their data if that's desired + if let Some(remove_data) = data.remove_data { + // Posts + blocking(pool, move |conn: &'_ _| { + Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data) + }) + .await??; + + // Comments + // Diesel doesn't allow updates with joins, so this has to be a loop + let comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(conn) + .for_creator_id(banned_user_id) + .for_community_id(community_id) + .limit(std::i64::MAX) + .list() + }) + .await??; + + for comment in &comments { + let comment_id = comment.id; + blocking(pool, move |conn: &'_ _| { + Comment::update_removed(conn, comment_id, remove_data) + }) + .await??; + } + } + // Mod tables // TODO eventually do correct expires let expires = match data.expires { diff --git a/server/src/api/user.rs b/server/src/api/user.rs index d8c03b695..bbb8d482b 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -177,6 +177,7 @@ pub struct AddAdminResponse { pub struct BanUser { user_id: i32, ban: bool, + remove_data: Option, reason: Option, expires: Option, auth: String, @@ -850,6 +851,27 @@ impl Perform for BanUser { return Err(APIError::err("couldnt_update_user").into()); } + // Remove their data if that's desired + if let Some(remove_data) = data.remove_data { + // Posts + blocking(pool, move |conn: &'_ _| { + Post::update_removed_for_creator(conn, banned_user_id, None, remove_data) + }) + .await??; + + // Communities + blocking(pool, move |conn: &'_ _| { + Community::update_removed_for_creator(conn, banned_user_id, remove_data) + }) + .await??; + + // Comments + blocking(pool, move |conn: &'_ _| { + Comment::update_removed_for_creator(conn, banned_user_id, remove_data) + }) + .await??; + } + // Mod tables let expires = match data.expires { Some(time) => Some(naive_from_unix(time)), @@ -1064,40 +1086,15 @@ impl Perform for DeleteAccount { // Comments let user_id = user.id; - let comments = blocking(pool, move |conn| { - CommentQueryBuilder::create(conn) - .for_creator_id(user_id) - .limit(std::i64::MAX) - .list() - }) - .await??; - - // TODO: this should probably be a bulk operation - for comment in &comments { - let comment_id = comment.id; - let permadelete = move |conn: &'_ _| Comment::permadelete(conn, comment_id); - if blocking(pool, permadelete).await?.is_err() { - return Err(APIError::err("couldnt_update_comment").into()); - } + let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, user_id); + if blocking(pool, permadelete).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); } // Posts - let posts = blocking(pool, move |conn| { - PostQueryBuilder::create(conn) - .sort(&SortType::New) - .for_creator_id(user_id) - .limit(std::i64::MAX) - .list() - }) - .await??; - - // TODO: this should probably be a bulk operation - for post in &posts { - let post_id = post.id; - let permadelete = move |conn: &'_ _| Post::permadelete(conn, post_id); - if blocking(pool, permadelete).await?.is_err() { - return Err(APIError::err("couldnt_update_post").into()); - } + let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, user_id); + if blocking(pool, permadelete).await?.is_err() { + return Err(APIError::err("couldnt_update_post").into()); } Ok(LoginResponse { diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index ef8a07182..13263b822 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -43,6 +43,7 @@ interface CommentNodeState { showRemoveDialog: boolean; removeReason: string; showBanDialog: boolean; + removeData: boolean; banReason: string; banExpires: string; banType: BanType; @@ -87,6 +88,7 @@ export class CommentNode extends Component { showRemoveDialog: false, removeReason: null, showBanDialog: false, + removeData: null, banReason: null, banExpires: null, banType: BanType.Community, @@ -699,6 +701,20 @@ export class CommentNode extends Component { value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> +
+
+ + +
+
{/* TODO hold off on expires until later */} {/*
*/} @@ -951,6 +967,11 @@ export class CommentNode extends Component { i.setState(i.state); } + handleModRemoveDataChange(i: CommentNode, event: any) { + i.state.removeData = event.target.checked; + i.setState(i.state); + } + handleModRemoveSubmit(i: CommentNode) { event.preventDefault(); let form: RemoveCommentForm = { @@ -1024,18 +1045,30 @@ export class CommentNode extends Component { event.preventDefault(); if (i.state.banType == BanType.Community) { + // If its an unban, restore all their data + let ban = !i.props.node.comment.banned_from_community; + if (ban == false) { + i.state.removeData = false; + } let form: BanFromCommunityForm = { user_id: i.props.node.comment.creator_id, community_id: i.props.node.comment.community_id, - ban: !i.props.node.comment.banned_from_community, + ban, + remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), }; WebSocketService.Instance.banFromCommunity(form); } else { + // If its an unban, restore all their data + let ban = !i.props.node.comment.banned; + if (ban == false) { + i.state.removeData = false; + } let form: BanUserForm = { user_id: i.props.node.comment.creator_id, - ban: !i.props.node.comment.banned, + ban, + remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), }; diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 49ec30343..e3e19e99c 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -44,6 +44,7 @@ interface PostListingState { showRemoveDialog: boolean; removeReason: string; showBanDialog: boolean; + removeData: boolean; banReason: string; banExpires: string; banType: BanType; @@ -74,6 +75,7 @@ export class PostListing extends Component { showRemoveDialog: false, removeReason: null, showBanDialog: false, + removeData: null, banReason: null, banExpires: null, banType: BanType.Community, @@ -931,6 +933,20 @@ export class PostListing extends Component { value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> +
+
+ + +
+
{/* TODO hold off on expires until later */} {/*
*/} @@ -1241,6 +1257,11 @@ export class PostListing extends Component { i.setState(i.state); } + handleModRemoveDataChange(i: PostListing, event: any) { + i.state.removeData = event.target.checked; + i.setState(i.state); + } + handleModRemoveSubmit(i: PostListing) { event.preventDefault(); let form: RemovePostForm = { @@ -1311,18 +1332,30 @@ export class PostListing extends Component { event.preventDefault(); if (i.state.banType == BanType.Community) { + // If its an unban, restore all their data + let ban = !i.props.post.banned_from_community; + if (ban == false) { + i.state.removeData = false; + } let form: BanFromCommunityForm = { user_id: i.props.post.creator_id, community_id: i.props.post.community_id, - ban: !i.props.post.banned_from_community, + ban, + remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), }; WebSocketService.Instance.banFromCommunity(form); } else { + // If its an unban, restore all their data + let ban = !i.props.post.banned; + if (ban == false) { + i.state.removeData = false; + } let form: BanUserForm = { user_id: i.props.post.creator_id, - ban: !i.props.post.banned, + ban, + remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), }; diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index b449060df..b2995926c 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -413,6 +413,7 @@ export interface BanFromCommunityForm { community_id: number; user_id: number; ban: boolean; + remove_data?: boolean; reason?: string; expires?: number; auth?: string; @@ -877,6 +878,7 @@ export interface SiteResponse { export interface BanUserForm { user_id: number; ban: boolean; + remove_data?: boolean; reason?: string; expires?: number; auth?: string; diff --git a/ui/translations/en.json b/ui/translations/en.json index cc73c326a..de1fc8268 100644 --- a/ui/translations/en.json +++ b/ui/translations/en.json @@ -15,6 +15,7 @@ "number_of_comments": "{{count}} Comment", "number_of_comments_plural": "{{count}} Comments", "remove_comment": "Remove Comment", + "remove_posts_comments": "Remove Posts and Comments", "communities": "Communities", "users": "Users", "create_a_community": "Create a community",