]> Untitled Git - lemmy.git/commitdiff
Added option to remove banned user data (posts, comments, communities) (#1093)
authorDessalines <dessalines@users.noreply.github.com>
Mon, 17 Aug 2020 18:12:36 +0000 (14:12 -0400)
committerGitHub <noreply@github.com>
Mon, 17 Aug 2020 18:12:36 +0000 (18:12 +0000)
- Works for both a site-ban, and a community ban.
- Fixes #557

docs/src/contributing_websocket_http_api.md
server/lemmy_db/src/comment.rs
server/lemmy_db/src/community.rs
server/lemmy_db/src/post.rs
server/src/api/community.rs
server/src/api/user.rs
ui/src/components/comment-node.tsx
ui/src/components/post-listing.tsx
ui/src/interfaces.ts
ui/translations/en.json

index fa241d162b92fedb8f7ff11d0a14756267c939a8..8ad7fbbf8ecb2afd22a33e69987b42c12fefc63f 100644 (file)
@@ -818,6 +818,7 @@ Marks all user replies and mentions as read.
   data: {
     user_id: i32,
     ban: bool,
+    remove_data: Option<bool>, // Removes/Restores their comments, posts, and communities
     reason: Option<String>,
     expires: Option<i64>,
     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<bool>, // Removes/Restores their comments and posts for that community
     reason: Option<String>,
     expires: Option<i64>,
     auth: String
index 8e52d7e2d4f17f5d34862951e8c1fca33d954695..f5a036f1a7171253afc53c21bb922dd6e78ab422 100644 (file)
@@ -97,16 +97,15 @@ impl Comment {
     comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
   }
 
-  pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+  pub fn permadelete_for_creator(conn: &PgConnection, for_creator_id: i32) -> Result<Vec<Self>, 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::<Self>(conn)
+      .get_results::<Self>(conn)
   }
 
   pub fn update_deleted(
@@ -131,6 +130,17 @@ impl Comment {
       .get_result::<Self>(conn)
   }
 
+  pub fn update_removed_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+    new_removed: bool,
+  ) -> Result<Vec<Self>, 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::<Self>(conn)
+  }
+
   pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
index 7490f366492b479b937fc68c92bee696c5c665a1..df5f129412604ccd6ad94c3d42abd5696747a14e 100644 (file)
@@ -121,6 +121,17 @@ impl Community {
       .get_result::<Self>(conn)
   }
 
+  pub fn update_removed_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+    new_removed: bool,
+  ) -> Result<Vec<Self>, 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::<Self>(conn)
+  }
+
   pub fn update_creator(
     conn: &PgConnection,
     community_id: i32,
index 1185aa845691c0c03b46322ea1448117583c96ec..d73901bb02642740341f8e04077ba3047bd1497e 100644 (file)
@@ -95,13 +95,13 @@ impl Post {
       .get_result::<Self>(conn)
   }
 
-  pub fn permadelete(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+  pub fn permadelete_for_creator(conn: &PgConnection, for_creator_id: i32) -> Result<Vec<Self>, 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::<Self>(conn)
+      .get_results::<Self>(conn)
   }
 
   pub fn update_deleted(
@@ -134,6 +134,26 @@ impl Post {
       .get_result::<Self>(conn)
   }
 
+  pub fn update_removed_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+    for_community_id: Option<i32>,
+    new_removed: bool,
+  ) -> Result<Vec<Self>, 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::<Self>(conn)
+  }
+
   pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
     use crate::schema::post::dsl::*;
     diesel::update(post.find(post_id))
index 01c43ce11ebc0f781b8478765554b9f078be381f..bd67145630214560e68af2ec80378cbc118e4110 100644 (file)
@@ -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<bool>,
   reason: Option<String>,
   expires: Option<i64>,
   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 {
index d8c03b695b988bd22f2e95d01aa5415c3facdf38..bbb8d482b61110699b66dc03cb4cc908d7c9b70a 100644 (file)
@@ -177,6 +177,7 @@ pub struct AddAdminResponse {
 pub struct BanUser {
   user_id: i32,
   ban: bool,
+  remove_data: Option<bool>,
   reason: Option<String>,
   expires: Option<i64>,
   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 {
index ef8a071823c2744b5085a23de9a7a1f57a9494ee..13263b8221b1492322d0b4bc413b9c61b69f5200 100644 (file)
@@ -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<CommentNodeProps, CommentNodeState> {
     showRemoveDialog: false,
     removeReason: null,
     showBanDialog: false,
+    removeData: null,
     banReason: null,
     banExpires: null,
     banType: BanType.Community,
@@ -699,6 +701,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 value={this.state.banReason}
                 onInput={linkEvent(this, this.handleModBanReasonChange)}
               />
+              <div class="form-group">
+                <div class="form-check">
+                  <input
+                    class="form-check-input"
+                    id="mod-ban-remove-data"
+                    type="checkbox"
+                    checked={this.state.removeData}
+                    onChange={linkEvent(this, this.handleModRemoveDataChange)}
+                  />
+                  <label class="form-check-label" htmlFor="mod-ban-remove-data">
+                    {i18n.t('remove_posts_comments')}
+                  </label>
+                </div>
+              </div>
             </div>
             {/* TODO hold off on expires until later */}
             {/* <div class="form-group row"> */}
@@ -951,6 +967,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     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<CommentNodeProps, CommentNodeState> {
     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),
       };
index 49ec3034311d8ed83596d11af8f41dcbcdc74de7..e3e19e99cbe52c27e5862e0265c7d5ab0a850746 100644 (file)
@@ -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<PostListingProps, PostListingState> {
     showRemoveDialog: false,
     removeReason: null,
     showBanDialog: false,
+    removeData: null,
     banReason: null,
     banExpires: null,
     banType: BanType.Community,
@@ -931,6 +933,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 value={this.state.banReason}
                 onInput={linkEvent(this, this.handleModBanReasonChange)}
               />
+              <div class="form-group">
+                <div class="form-check">
+                  <input
+                    class="form-check-input"
+                    id="mod-ban-remove-data"
+                    type="checkbox"
+                    checked={this.state.removeData}
+                    onChange={linkEvent(this, this.handleModRemoveDataChange)}
+                  />
+                  <label class="form-check-label" htmlFor="mod-ban-remove-data">
+                    {i18n.t('remove_posts_comments')}
+                  </label>
+                </div>
+              </div>
             </div>
             {/* TODO hold off on expires until later */}
             {/* <div class="form-group row"> */}
@@ -1241,6 +1257,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     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<PostListingProps, PostListingState> {
     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),
       };
index b449060df5135f510d0565f27cb53c2ee7c0750f..b2995926cb3bb7d1e1d2fa7be16fbba679b8c3c9 100644 (file)
@@ -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;
index cc73c326a242a52c1f1416412d5c1ab50d1ba228..de1fc8268baba5bec10e0d93ae7460c3a0810b0e 100644 (file)
@@ -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",