Added option to remove banned user data (posts, comments, communities) (#1093)

- Works for both a site-ban, and a community ban.
- Fixes #557
pull/1096/head
Dessalines 2020-08-17 14:12:36 -04:00 committed by GitHub
parent 725e46da4a
commit c323ab5275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 42 deletions

View File

@ -818,6 +818,7 @@ Marks all user replies and mentions as read.
data: { data: {
user_id: i32, user_id: i32,
ban: bool, ban: bool,
remove_data: Option<bool>, // Removes/Restores their comments, posts, and communities
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String auth: String
@ -1177,6 +1178,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
community_id: i32, community_id: i32,
user_id: i32, user_id: i32,
ban: bool, ban: bool,
remove_data: Option<bool>, // Removes/Restores their comments and posts for that community
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String auth: String

View File

@ -97,16 +97,15 @@ impl Comment {
comment.filter(ap_id.eq(object_id)).first::<Self>(conn) 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::*; use crate::schema::comment::dsl::*;
diesel::update(comment.filter(creator_id.eq(for_creator_id)))
diesel::update(comment.find(comment_id))
.set(( .set((
content.eq("*Permananently Deleted*"), content.eq("*Permananently Deleted*"),
deleted.eq(true), deleted.eq(true),
updated.eq(naive_now()), updated.eq(naive_now()),
)) ))
.get_result::<Self>(conn) .get_results::<Self>(conn)
} }
pub fn update_deleted( pub fn update_deleted(
@ -131,6 +130,17 @@ impl Comment {
.get_result::<Self>(conn) .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> { pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id)) diesel::update(comment.find(comment_id))

View File

@ -121,6 +121,17 @@ impl Community {
.get_result::<Self>(conn) .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( pub fn update_creator(
conn: &PgConnection, conn: &PgConnection,
community_id: i32, community_id: i32,

View File

@ -95,13 +95,13 @@ impl Post {
.get_result::<Self>(conn) .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::*; use crate::schema::post::dsl::*;
let perma_deleted = "*Permananently Deleted*"; let perma_deleted = "*Permananently Deleted*";
let perma_deleted_url = "https://deleted.com"; let perma_deleted_url = "https://deleted.com";
diesel::update(post.find(post_id)) diesel::update(post.filter(creator_id.eq(for_creator_id)))
.set(( .set((
name.eq(perma_deleted), name.eq(perma_deleted),
url.eq(perma_deleted_url), url.eq(perma_deleted_url),
@ -109,7 +109,7 @@ impl Post {
deleted.eq(true), deleted.eq(true),
updated.eq(naive_now()), updated.eq(naive_now()),
)) ))
.get_result::<Self>(conn) .get_results::<Self>(conn)
} }
pub fn update_deleted( pub fn update_deleted(
@ -134,6 +134,26 @@ impl Post {
.get_result::<Self>(conn) .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> { pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
diesel::update(post.find(post_id)) diesel::update(post.find(post_id))

View File

@ -13,8 +13,11 @@ use crate::{
use actix_web::client::Client; use actix_web::client::Client;
use anyhow::Context; use anyhow::Context;
use lemmy_db::{ use lemmy_db::{
comment::Comment,
comment_view::CommentQueryBuilder,
diesel_option_overwrite, diesel_option_overwrite,
naive_now, naive_now,
post::Post,
Bannable, Bannable,
Crud, Crud,
Followable, Followable,
@ -81,6 +84,7 @@ pub struct BanFromCommunity {
pub community_id: i32, pub community_id: i32,
user_id: i32, user_id: i32,
ban: bool, ban: bool,
remove_data: Option<bool>,
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String, auth: String,
@ -676,6 +680,7 @@ impl Perform for BanFromCommunity {
let user = get_user_from_jwt(&data.auth, pool).await?; let user = get_user_from_jwt(&data.auth, pool).await?;
let community_id = data.community_id; let community_id = data.community_id;
let banned_user_id = data.user_id;
// Verify that only mods or admins can ban // Verify that only mods or admins can ban
is_mod_or_admin(pool, user.id, community_id).await?; 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 // Mod tables
// TODO eventually do correct expires // TODO eventually do correct expires
let expires = match data.expires { let expires = match data.expires {

View File

@ -177,6 +177,7 @@ pub struct AddAdminResponse {
pub struct BanUser { pub struct BanUser {
user_id: i32, user_id: i32,
ban: bool, ban: bool,
remove_data: Option<bool>,
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String, auth: String,
@ -850,6 +851,27 @@ impl Perform for BanUser {
return Err(APIError::err("couldnt_update_user").into()); 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 // Mod tables
let expires = match data.expires { let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)), Some(time) => Some(naive_from_unix(time)),
@ -1064,40 +1086,15 @@ impl Perform for DeleteAccount {
// Comments // Comments
let user_id = user.id; let user_id = user.id;
let comments = blocking(pool, move |conn| { let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, user_id);
CommentQueryBuilder::create(conn) if blocking(pool, permadelete).await?.is_err() {
.for_creator_id(user_id) return Err(APIError::err("couldnt_update_comment").into());
.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());
}
} }
// Posts // Posts
let posts = blocking(pool, move |conn| { let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, user_id);
PostQueryBuilder::create(conn) if blocking(pool, permadelete).await?.is_err() {
.sort(&SortType::New) return Err(APIError::err("couldnt_update_post").into());
.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());
}
} }
Ok(LoginResponse { Ok(LoginResponse {

View File

@ -43,6 +43,7 @@ interface CommentNodeState {
showRemoveDialog: boolean; showRemoveDialog: boolean;
removeReason: string; removeReason: string;
showBanDialog: boolean; showBanDialog: boolean;
removeData: boolean;
banReason: string; banReason: string;
banExpires: string; banExpires: string;
banType: BanType; banType: BanType;
@ -87,6 +88,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showRemoveDialog: false, showRemoveDialog: false,
removeReason: null, removeReason: null,
showBanDialog: false, showBanDialog: false,
removeData: null,
banReason: null, banReason: null,
banExpires: null, banExpires: null,
banType: BanType.Community, banType: BanType.Community,
@ -699,6 +701,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
value={this.state.banReason} value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)} 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> </div>
{/* TODO hold off on expires until later */} {/* TODO hold off on expires until later */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
@ -951,6 +967,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state); i.setState(i.state);
} }
handleModRemoveDataChange(i: CommentNode, event: any) {
i.state.removeData = event.target.checked;
i.setState(i.state);
}
handleModRemoveSubmit(i: CommentNode) { handleModRemoveSubmit(i: CommentNode) {
event.preventDefault(); event.preventDefault();
let form: RemoveCommentForm = { let form: RemoveCommentForm = {
@ -1024,18 +1045,30 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
event.preventDefault(); event.preventDefault();
if (i.state.banType == BanType.Community) { 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 = { let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id, user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_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, reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires), expires: getUnixTime(i.state.banExpires),
}; };
WebSocketService.Instance.banFromCommunity(form); WebSocketService.Instance.banFromCommunity(form);
} else { } 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 = { let form: BanUserForm = {
user_id: i.props.node.comment.creator_id, user_id: i.props.node.comment.creator_id,
ban: !i.props.node.comment.banned, ban,
remove_data: i.state.removeData,
reason: i.state.banReason, reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires), expires: getUnixTime(i.state.banExpires),
}; };

View File

@ -44,6 +44,7 @@ interface PostListingState {
showRemoveDialog: boolean; showRemoveDialog: boolean;
removeReason: string; removeReason: string;
showBanDialog: boolean; showBanDialog: boolean;
removeData: boolean;
banReason: string; banReason: string;
banExpires: string; banExpires: string;
banType: BanType; banType: BanType;
@ -74,6 +75,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
showRemoveDialog: false, showRemoveDialog: false,
removeReason: null, removeReason: null,
showBanDialog: false, showBanDialog: false,
removeData: null,
banReason: null, banReason: null,
banExpires: null, banExpires: null,
banType: BanType.Community, banType: BanType.Community,
@ -931,6 +933,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
value={this.state.banReason} value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)} 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> </div>
{/* TODO hold off on expires until later */} {/* TODO hold off on expires until later */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
@ -1241,6 +1257,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState(i.state); i.setState(i.state);
} }
handleModRemoveDataChange(i: PostListing, event: any) {
i.state.removeData = event.target.checked;
i.setState(i.state);
}
handleModRemoveSubmit(i: PostListing) { handleModRemoveSubmit(i: PostListing) {
event.preventDefault(); event.preventDefault();
let form: RemovePostForm = { let form: RemovePostForm = {
@ -1311,18 +1332,30 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
event.preventDefault(); event.preventDefault();
if (i.state.banType == BanType.Community) { 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 = { let form: BanFromCommunityForm = {
user_id: i.props.post.creator_id, user_id: i.props.post.creator_id,
community_id: i.props.post.community_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, reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires), expires: getUnixTime(i.state.banExpires),
}; };
WebSocketService.Instance.banFromCommunity(form); WebSocketService.Instance.banFromCommunity(form);
} else { } 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 = { let form: BanUserForm = {
user_id: i.props.post.creator_id, user_id: i.props.post.creator_id,
ban: !i.props.post.banned, ban,
remove_data: i.state.removeData,
reason: i.state.banReason, reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires), expires: getUnixTime(i.state.banExpires),
}; };

View File

@ -413,6 +413,7 @@ export interface BanFromCommunityForm {
community_id: number; community_id: number;
user_id: number; user_id: number;
ban: boolean; ban: boolean;
remove_data?: boolean;
reason?: string; reason?: string;
expires?: number; expires?: number;
auth?: string; auth?: string;
@ -877,6 +878,7 @@ export interface SiteResponse {
export interface BanUserForm { export interface BanUserForm {
user_id: number; user_id: number;
ban: boolean; ban: boolean;
remove_data?: boolean;
reason?: string; reason?: string;
expires?: number; expires?: number;
auth?: string; auth?: string;

View File

@ -15,6 +15,7 @@
"number_of_comments": "{{count}} Comment", "number_of_comments": "{{count}} Comment",
"number_of_comments_plural": "{{count}} Comments", "number_of_comments_plural": "{{count}} Comments",
"remove_comment": "Remove Comment", "remove_comment": "Remove Comment",
"remove_posts_comments": "Remove Posts and Comments",
"communities": "Communities", "communities": "Communities",
"users": "Users", "users": "Users",
"create_a_community": "Create a community", "create_a_community": "Create a community",