mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 10:20:58 -06:00
FEATURE: Support an end date for user silencing
This commit is contained in:
parent
52480d554a
commit
971e302ff2
@ -70,7 +70,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
unsuspend() {
|
||||
this.get("model").unsuspend().catch(popupAjaxError);
|
||||
},
|
||||
|
||||
showSilenceModal() {
|
||||
this.get('adminTools').showSilenceModal(this.get('model'));
|
||||
},
|
||||
|
||||
toggleUsernameEdit() {
|
||||
this.set('userUsernameValue', this.get('model.username'));
|
||||
|
@ -0,0 +1,50 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
silenceUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
silencing: false,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
silenceUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
silencing: false,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
});
|
||||
},
|
||||
|
||||
@computed('silenceUntil', 'reason', 'silencing')
|
||||
submitDisabled(silenceUntil, reason, silencing) {
|
||||
return (silencing || Ember.isEmpty(silenceUntil) || !reason || reason.length < 1);
|
||||
},
|
||||
|
||||
actions: {
|
||||
silence() {
|
||||
if (this.get('submitDisabled')) { return; }
|
||||
|
||||
this.set('silencing', true);
|
||||
this.get('user').silence({
|
||||
silenced_till: this.get('silenceUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id')
|
||||
}).then(result => {
|
||||
this.send('closeModal');
|
||||
let callback = this.get('successCallback');
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}).catch(popupAjaxError).finally(() => this.set('silencing', false));
|
||||
}
|
||||
}
|
||||
});
|
@ -8,6 +8,8 @@ import Group from 'discourse/models/group';
|
||||
import TL3Requirements from 'admin/models/tl3-requirements';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
const wrapAdmin = user => user ? AdminUser.create(user) : null;
|
||||
|
||||
const AdminUser = Discourse.User.extend({
|
||||
adminUserView: true,
|
||||
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
|
||||
@ -232,6 +234,7 @@ const AdminUser = Discourse.User.extend({
|
||||
}.property('trust_level'),
|
||||
|
||||
isSuspended: Em.computed.equal('suspended', true),
|
||||
isSilenced: Ember.computed.equal('silenced', true),
|
||||
canSuspend: Em.computed.not('staff'),
|
||||
|
||||
suspendDuration: function() {
|
||||
@ -301,44 +304,36 @@ const AdminUser = Discourse.User.extend({
|
||||
|
||||
unsilence() {
|
||||
this.set('silencingUser', true);
|
||||
return ajax('/admin/users/' + this.id + '/unsilence', {
|
||||
|
||||
return ajax(`/admin/users/${this.id}/unsilence`, {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
var error = I18n.t('admin.user.unsilence_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
}).then(result => {
|
||||
this.setProperties(result.unsilence);
|
||||
}).catch(e => {
|
||||
let error = I18n.t('admin.user.unsilence_failed', {
|
||||
error: `http: ${e.status} - ${e.body}`
|
||||
});
|
||||
bootbox.alert(error);
|
||||
}).finally(() => {
|
||||
this.set('silencingUser', false);
|
||||
});
|
||||
},
|
||||
|
||||
silence() {
|
||||
const user = this,
|
||||
message = I18n.t("admin.user.silence_confirm");
|
||||
|
||||
const performSilence = function() {
|
||||
user.set('silencingUser', true);
|
||||
return ajax('/admin/users/' + user.id + '/silence', {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
var error = I18n.t('admin.user.silence_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
bootbox.alert(error);
|
||||
user.set('silencingUser', false);
|
||||
silence(data) {
|
||||
this.set('silencingUser', true);
|
||||
return ajax(`/admin/users/${this.id}/silence`, {
|
||||
type: 'PUT',
|
||||
data
|
||||
}).then(result => {
|
||||
this.setProperties(result.silence);
|
||||
}).catch(e => {
|
||||
let error = I18n.t('admin.user.silence_failed', {
|
||||
error: `http: ${e.status} - ${e.body}`
|
||||
});
|
||||
bootbox.alert(error);
|
||||
}).finally(() => {
|
||||
this.set('silencingUser', false);
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [{
|
||||
"label": I18n.t("composer.cancel"),
|
||||
"class": "cancel",
|
||||
"link": true
|
||||
}, {
|
||||
"label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.silence_accept'),
|
||||
"class": "btn btn-danger",
|
||||
"callback": function() { performSilence(); }
|
||||
}];
|
||||
|
||||
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
|
||||
},
|
||||
|
||||
sendActivationEmail() {
|
||||
@ -475,17 +470,14 @@ const AdminUser = Discourse.User.extend({
|
||||
}
|
||||
}.property('tl3_requirements'),
|
||||
|
||||
suspendedBy: function() {
|
||||
if (this.get('suspended_by')) {
|
||||
return AdminUser.create(this.get('suspended_by'));
|
||||
}
|
||||
}.property('suspended_by'),
|
||||
@computed('suspended_by')
|
||||
suspendedBy: wrapAdmin,
|
||||
|
||||
approvedBy: function() {
|
||||
if (this.get('approved_by')) {
|
||||
return AdminUser.create(this.get('approved_by'));
|
||||
}
|
||||
}.property('approved_by')
|
||||
@computed('silenced_by')
|
||||
silencedBy: wrapAdmin,
|
||||
|
||||
@computed('approved_by')
|
||||
approvedBy: wrapAdmin,
|
||||
|
||||
});
|
||||
|
||||
|
@ -20,12 +20,12 @@ export default Ember.Service.extend({
|
||||
};
|
||||
},
|
||||
|
||||
showSuspendModal(user, opts) {
|
||||
_showControlModal(type, user, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
let controller = showModal('admin-suspend-user', {
|
||||
let controller = showModal(`admin-${type}-user`, {
|
||||
admin: true,
|
||||
modalClass: 'suspend-user-modal'
|
||||
modalClass: `${type}-user-modal`
|
||||
});
|
||||
if (opts.post) {
|
||||
controller.set('post', opts.post);
|
||||
@ -44,6 +44,14 @@ export default Ember.Service.extend({
|
||||
});
|
||||
},
|
||||
|
||||
showSilenceModal(user, opts) {
|
||||
this._showControlModal('silence', user, opts);
|
||||
},
|
||||
|
||||
showSuspendModal(user, opts) {
|
||||
this._showControlModal('suspend', user, opts);
|
||||
},
|
||||
|
||||
_deleteSpammer(adminUser) {
|
||||
return adminUser.checkEmail().then(() => {
|
||||
|
||||
|
@ -0,0 +1,50 @@
|
||||
{{#d-modal-body title="admin.user.silence_modal_title"}}
|
||||
{{#conditional-loading-spinner condition=loadingUser}}
|
||||
|
||||
<div class='until-controls'>
|
||||
<label>
|
||||
{{future-date-input
|
||||
class="silence-until"
|
||||
label="admin.user.silence_duration"
|
||||
includeFarFuture=true
|
||||
input=silenceUntil}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class='reason-controls'>
|
||||
<label>
|
||||
<div class='silence-reason-label'>
|
||||
{{{i18n 'admin.user.silence_reason_label'}}}
|
||||
</div>
|
||||
|
||||
{{text-field
|
||||
value=reason
|
||||
class="silence-reason"
|
||||
placeholderKey="admin.user.silence_reason_placeholder"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<div class='silence-message-label'>
|
||||
{{i18n "admin.user.silence_message"}}
|
||||
</div>
|
||||
{{textarea
|
||||
value=message
|
||||
class="silence-message"
|
||||
placeholder=(i18n "admin.user.silence_message_placeholder")}}
|
||||
</label>
|
||||
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button
|
||||
class="btn-danger perform-silence"
|
||||
action="silence"
|
||||
disabled=submitDisabled
|
||||
icon="microphone-slash"
|
||||
label="admin.user.silence"}}
|
||||
{{d-modal-cancel close=(action "closeModal")}}
|
||||
{{conditional-loading-spinner condition=loading size="small"}}
|
||||
</div>
|
@ -349,19 +349,49 @@
|
||||
|
||||
<div class="display-row {{if model.silenced 'highlight-danger'}}">
|
||||
<div class='field'>{{i18n 'admin.user.silenced'}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.silenced}}</div>
|
||||
<div class='value'>
|
||||
{{i18n-yes-no model.silenced}}
|
||||
{{#if model.isSilenced}}
|
||||
{{#unless model.silencedForever}}
|
||||
{{i18n "admin.user.suspended_until" until=model.silencedTillDate}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#conditional-loading-spinner size="small" condition=model.silencingUser}}
|
||||
{{#if model.silenced}}
|
||||
{{d-button action="unsilence" icon="thumbs-o-up" label="admin.user.unsilence"}}
|
||||
{{d-button
|
||||
class="btn-danger unsilence-user"
|
||||
action="unsilence"
|
||||
icon="microphone-slash"
|
||||
label="admin.user.unsilence"}}
|
||||
{{i18n 'admin.user.silence_explanation'}}
|
||||
{{else}}
|
||||
{{d-button action="silence" icon="ban" label="admin.user.silence"}}
|
||||
{{d-button
|
||||
class="btn-danger silence-user"
|
||||
action=(action "showSilenceModal")
|
||||
icon="microphone-slash"
|
||||
label="admin.user.silence"}}
|
||||
{{i18n 'admin.user.silence_explanation'}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.isSilenced}}
|
||||
<div class='display-row highlight-danger silence-info'>
|
||||
<div class='field'>{{i18n 'admin.user.silenced_by'}}</div>
|
||||
<div class='value'>
|
||||
{{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<b>{{i18n 'admin.user.silence_reason'}}</b>:
|
||||
{{model.silence_reason}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
|
||||
{{#if currentUser.admin}}
|
||||
|
@ -16,6 +16,8 @@ import PreloadStore from 'preload-store';
|
||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
const isForever = dt => moment().diff(dt, 'years') < -500;
|
||||
|
||||
const User = RestModel.extend({
|
||||
|
||||
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
|
||||
@ -178,14 +180,16 @@ const User = RestModel.extend({
|
||||
},
|
||||
|
||||
@computed("suspended_till")
|
||||
suspendedForever(suspendedTill) {
|
||||
return moment().diff(suspendedTill, 'years') < -500;
|
||||
},
|
||||
suspendedForever: isForever,
|
||||
|
||||
@computed("silenced_till")
|
||||
silencedForever: isForever,
|
||||
|
||||
@computed("suspended_till")
|
||||
suspendedTillDate(suspendedTill) {
|
||||
return longDate(suspendedTill);
|
||||
},
|
||||
suspendedTillDate: longDate,
|
||||
|
||||
@computed("silenced_till")
|
||||
silencedTillDate: longDate,
|
||||
|
||||
changeUsername(new_username) {
|
||||
return ajax(userPath(`${this.get('username_lower')}/preferences/username`), {
|
||||
|
@ -274,14 +274,48 @@ class Admin::UsersController < Admin::AdminController
|
||||
|
||||
def silence
|
||||
guardian.ensure_can_silence_user! @user
|
||||
UserSilencer.silence(@user, current_user, keep_posts: true)
|
||||
render body: nil
|
||||
|
||||
message = params[:message]
|
||||
|
||||
silencer = UserSilencer.new(
|
||||
@user,
|
||||
current_user,
|
||||
silenced_till: params[:silenced_till],
|
||||
reason: params[:reason],
|
||||
context: message,
|
||||
keep_posts: true
|
||||
)
|
||||
if silencer.silence && message.present?
|
||||
Jobs.enqueue(
|
||||
:critical_user_email,
|
||||
type: :account_silenced,
|
||||
user_id: @user.id,
|
||||
user_history_id: silencer.user_history.id
|
||||
)
|
||||
end
|
||||
|
||||
render_json_dump(
|
||||
silence: {
|
||||
silenced: true,
|
||||
silence_reason: params[:reason],
|
||||
silenced_till: @user.silenced_till,
|
||||
suspended_at: @user.silenced_at
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def unsilence
|
||||
guardian.ensure_can_unsilence_user! @user
|
||||
UserSilencer.unsilence(@user, current_user)
|
||||
render body: nil
|
||||
|
||||
render_json_dump(
|
||||
unsilence: {
|
||||
silenced: false,
|
||||
silence_reason: nil,
|
||||
silenced_till: nil,
|
||||
suspended_at: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def reject_bulk
|
||||
|
@ -10,7 +10,7 @@ module Jobs
|
||||
|
||||
HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new(
|
||||
user_archive: ['topic_title', 'category', 'sub_category', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'],
|
||||
user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'silenced', 'active', 'admin', 'moderator', 'ip_address', 'staged'],
|
||||
user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'silenced_till', 'active', 'admin', 'moderator', 'ip_address', 'staged'],
|
||||
user_stats: ['topics_entered', 'posts_read_count', 'time_read', 'topic_count', 'post_count', 'likes_given', 'likes_received'],
|
||||
user_profile: ['location', 'website', 'views'],
|
||||
user_sso: ['external_id', 'external_email', 'external_username', 'external_name', 'external_avatar_url'],
|
||||
@ -181,7 +181,7 @@ module Jobs
|
||||
|
||||
def get_base_user_array(user)
|
||||
user_array = []
|
||||
user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views)
|
||||
user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced_till, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views)
|
||||
end
|
||||
|
||||
def add_single_sign_on(user, user_info_array)
|
||||
|
@ -22,7 +22,7 @@ module Jobs
|
||||
ub.badge_id = #{Badge::Anniversary} AND
|
||||
ub.granted_at BETWEEN '#{fmt_start_date}' AND '#{fmt_end_date}'
|
||||
WHERE u.active AND
|
||||
NOT u.silenced AND
|
||||
u.silenced_till IS NULL AND
|
||||
NOT p.hidden AND
|
||||
p.deleted_at IS NULL AND
|
||||
t.visible AND
|
||||
|
@ -68,6 +68,21 @@ class UserNotifications < ActionMailer::Base
|
||||
email_token: opts[:email_token])
|
||||
end
|
||||
|
||||
def account_silenced(user, opts = nil)
|
||||
opts ||= {}
|
||||
|
||||
return unless user_history = opts[:user_history]
|
||||
|
||||
build_email(
|
||||
user.email,
|
||||
template: "user_notifications.account_silenced",
|
||||
locale: user_locale(user),
|
||||
reason: user_history.details,
|
||||
message: user_history.context,
|
||||
silenced_till: I18n.l(user.silenced_till, format: :long)
|
||||
)
|
||||
end
|
||||
|
||||
def account_suspended(user, opts = nil)
|
||||
opts ||= {}
|
||||
|
||||
|
@ -82,7 +82,7 @@ class DirectoryItem < ActiveRecord::Base
|
||||
LEFT OUTER JOIN posts AS p ON ua.target_post_id = p.id
|
||||
LEFT OUTER JOIN categories AS c ON t.category_id = c.id
|
||||
WHERE u.active
|
||||
AND NOT u.silenced
|
||||
AND u.silenced_till IS NULL
|
||||
AND t.deleted_at IS NULL
|
||||
AND COALESCE(t.visible, true)
|
||||
AND p.deleted_at IS NULL
|
||||
|
@ -147,8 +147,8 @@ class User < ActiveRecord::Base
|
||||
# TODO-PERF: There is no indexes on any of these
|
||||
# and NotifyMailingListSubscribers does a select-all-and-loop
|
||||
# may want to create an index on (active, silence, suspended_till)?
|
||||
scope :silenced, -> { where(silenced: true) }
|
||||
scope :not_silenced, -> { where(silenced: false) }
|
||||
scope :silenced, -> { where("silenced_till IS NOT NULL AND silenced_till > ?", Time.zone.now) }
|
||||
scope :not_silenced, -> { where("silenced_till IS NULL OR silenced_till <= ?", Time.zone.now) }
|
||||
scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) }
|
||||
scope :not_suspended, -> { where('suspended_till IS NULL OR suspended_till <= ?', Time.zone.now) }
|
||||
scope :activated, -> { where(active: true) }
|
||||
@ -660,6 +660,22 @@ class User < ActiveRecord::Base
|
||||
!!(suspended_till && suspended_till > DateTime.now)
|
||||
end
|
||||
|
||||
def silenced?
|
||||
!!(silenced_till && silenced_till > DateTime.now)
|
||||
end
|
||||
|
||||
def silenced_record
|
||||
UserHistory.for(self, :silence_user).order('id DESC').first
|
||||
end
|
||||
|
||||
def silence_reason
|
||||
silenced_record.try(:details) if silenced?
|
||||
end
|
||||
|
||||
def silenced_at
|
||||
silenced_record.try(:created_at) if silenced?
|
||||
end
|
||||
|
||||
def suspend_record
|
||||
UserHistory.for(self, :suspend_user).order('id DESC').first
|
||||
end
|
||||
|
@ -18,6 +18,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||
:can_be_anonymized,
|
||||
:suspend_reason,
|
||||
:suspended_till,
|
||||
:silence_reason,
|
||||
:primary_group_id,
|
||||
:badge_count,
|
||||
:warnings_received_count,
|
||||
@ -29,6 +30,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
||||
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :silenced_by, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :tl3_requirements, serializer: TrustLevel3RequirementsSerializer, embed: :objects
|
||||
has_many :groups, embed: :object, serializer: BasicGroupSerializer
|
||||
|
||||
@ -72,6 +74,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||
object.suspend_record.try(:acting_user)
|
||||
end
|
||||
|
||||
def silence_reason
|
||||
object.silence_reason
|
||||
end
|
||||
|
||||
def silenced_by
|
||||
object.silenced_record.try(:acting_user)
|
||||
end
|
||||
|
||||
def include_tl3_requirements?
|
||||
object.has_trust_level?(TrustLevel[2])
|
||||
end
|
||||
|
@ -23,6 +23,7 @@ class AdminUserListSerializer < BasicUserSerializer
|
||||
:suspended_till,
|
||||
:suspended,
|
||||
:silenced,
|
||||
:silenced_till,
|
||||
:time_read,
|
||||
:staged
|
||||
|
||||
@ -40,6 +41,22 @@ class AdminUserListSerializer < BasicUserSerializer
|
||||
|
||||
alias_method :include_associated_accounts?, :include_email?
|
||||
|
||||
def silenced
|
||||
object.silenced?
|
||||
end
|
||||
|
||||
def include_silenced?
|
||||
object.silenced?
|
||||
end
|
||||
|
||||
def silenced_till
|
||||
object.silenced_till
|
||||
end
|
||||
|
||||
def include_silenced_till?
|
||||
object.silenced_till?
|
||||
end
|
||||
|
||||
def suspended
|
||||
object.suspended?
|
||||
end
|
||||
|
@ -275,8 +275,13 @@ class StaffActionLogger
|
||||
|
||||
def log_silence_user(user, opts = {})
|
||||
raise Discourse::InvalidParameters.new(:user) unless user
|
||||
UserHistory.create(params(opts).merge(action: UserHistory.actions[:silence_user],
|
||||
target_user_id: user.id))
|
||||
UserHistory.create(
|
||||
params(opts).merge(
|
||||
action: UserHistory.actions[:silence_user],
|
||||
target_user_id: user.id,
|
||||
details: opts[:details]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def log_unsilence_user(user, opts = {})
|
||||
|
@ -1,5 +1,7 @@
|
||||
class UserSilencer
|
||||
|
||||
attr_reader :user_history
|
||||
|
||||
def initialize(user, by_user = nil, opts = {})
|
||||
@user, @by_user, @opts = user, by_user, opts
|
||||
end
|
||||
@ -14,14 +16,26 @@ class UserSilencer
|
||||
|
||||
def silence
|
||||
hide_posts unless @opts[:keep_posts]
|
||||
unless @user.silenced?
|
||||
@user.silenced = true
|
||||
unless @user.silenced_till.present?
|
||||
@user.silenced_till = @opts[:silenced_till] || 1000.years.from_now
|
||||
if @user.save
|
||||
message_type = @opts[:message] || :silenced_by_staff
|
||||
post = SystemMessage.create(@user, message_type)
|
||||
if post && @by_user
|
||||
StaffActionLogger.new(@by_user).log_silence_user(@user, context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}")
|
||||
|
||||
if @opts[:context].present?
|
||||
context = @opts[:context]
|
||||
else
|
||||
context = "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}"
|
||||
SystemMessage.create(@user, message_type)
|
||||
end
|
||||
|
||||
if @by_user
|
||||
@user_history = StaffActionLogger.new(@by_user).log_silence_user(
|
||||
@user,
|
||||
context: context,
|
||||
details: @opts[:reason]
|
||||
)
|
||||
end
|
||||
return true
|
||||
end
|
||||
else
|
||||
false
|
||||
@ -37,7 +51,7 @@ class UserSilencer
|
||||
end
|
||||
|
||||
def unsilence
|
||||
@user.silenced = false
|
||||
@user.silenced_till = nil
|
||||
if @user.save
|
||||
SystemMessage.create(@user, :unsilenced)
|
||||
StaffActionLogger.new(@by_user).log_unsilence_user(@user) if @by_user
|
||||
|
@ -3281,6 +3281,14 @@ en:
|
||||
suspend_message: "Email Message"
|
||||
suspend_message_placeholder: "Optionally, provide more information about the suspension and it will be emailed to the user."
|
||||
suspended_by: "Suspended by"
|
||||
silence_reason: "Reason"
|
||||
silenced_by: "Silenced By"
|
||||
silence_modal_title: "Silence User"
|
||||
silence_duration: "How long will the user be silenced for?"
|
||||
silence_reason_label: "Why are you silencing this user?"
|
||||
silence_reason_placeholder: "Silence Reason"
|
||||
silence_message: "Email Message"
|
||||
silence_message_placeholder: "(leave blank to send default message)"
|
||||
suspended_until: "(until %{until})"
|
||||
cant_suspend: "This user cannot be suspended."
|
||||
delete_all_posts: "Delete all posts"
|
||||
|
@ -2660,6 +2660,17 @@ en:
|
||||
|
||||
%{message}
|
||||
|
||||
account_silenced:
|
||||
title: "Account Silenced"
|
||||
subject_template: "[%{email_prefix}] Your account has been silenced"
|
||||
text_body_template: |
|
||||
You have been silenced from the forum until %{silenced_till}.
|
||||
|
||||
%{reason}
|
||||
|
||||
%{message}
|
||||
|
||||
|
||||
account_exists:
|
||||
title: "Account already exists"
|
||||
subject_template: "[%{email_prefix}] Account already exists"
|
||||
|
@ -72,6 +72,17 @@ ColumnDropper.drop(
|
||||
}
|
||||
)
|
||||
|
||||
ColumnDropper.drop(
|
||||
table: 'users',
|
||||
after_migration: 'AddSilencedTillToUsers',
|
||||
columns: %w[
|
||||
silenced
|
||||
],
|
||||
on_drop: ->() {
|
||||
STDERR.puts 'Removing user silenced column!'
|
||||
}
|
||||
)
|
||||
|
||||
# User for the smoke tests
|
||||
if ENV["SMOKE"] == "1"
|
||||
UserEmail.seed do |ue|
|
||||
|
14
db/migrate/20171113175414_add_silenced_till_to_users.rb
Normal file
14
db/migrate/20171113175414_add_silenced_till_to_users.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class AddSilencedTillToUsers < ActiveRecord::Migration[5.1]
|
||||
def up
|
||||
add_column :users, :silenced_till, :timestamp, null: true
|
||||
execute <<~SQL
|
||||
UPDATE users
|
||||
SET silenced_till = CURRENT_TIMESTAMP + INTERVAL '1000 YEAR'
|
||||
WHERE silenced
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
add_column :users, :silenced_till
|
||||
end
|
||||
end
|
@ -141,10 +141,10 @@ SQL
|
||||
SELECT invited_by_id
|
||||
FROM invites i
|
||||
JOIN users u2 ON u2.id = i.user_id
|
||||
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.silenced
|
||||
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND u2.silenced_till IS NULL
|
||||
GROUP BY invited_by_id
|
||||
HAVING COUNT(*) >= #{count.to_i}
|
||||
) AND u.active AND NOT u.silenced AND u.id > 0 AND
|
||||
) AND u.active AND u.silenced_till IS NULL AND u.id > 0 AND
|
||||
(:backfill OR u.id IN (:user_ids) )
|
||||
"
|
||||
end
|
||||
|
@ -167,7 +167,7 @@ describe AdminUserIndexQuery do
|
||||
|
||||
describe "with a silenced user" do
|
||||
|
||||
let!(:user) { Fabricate(:user, silenced: true) }
|
||||
let!(:user) { Fabricate(:user, silenced_till: 1.year.from_now) }
|
||||
|
||||
it "finds the silenced user" do
|
||||
query = ::AdminUserIndexQuery.new(query: 'silenced')
|
||||
|
@ -57,7 +57,7 @@ describe Email::Receiver do
|
||||
end
|
||||
|
||||
it "raises a SilencedUserError when the sender has been silenced" do
|
||||
Fabricate(:user, email: "silenced@bar.com", silenced: true)
|
||||
Fabricate(:user, email: "silenced@bar.com", silenced_till: 1.year.from_now)
|
||||
expect { process(:silenced_sender) }.to raise_error(Email::Receiver::SilencedUserError)
|
||||
end
|
||||
|
||||
|
@ -195,7 +195,7 @@ describe Guardian do
|
||||
|
||||
context "author is silenced" do
|
||||
before do
|
||||
user.silenced = true
|
||||
user.silenced_till = 1.year.from_now
|
||||
user.save
|
||||
end
|
||||
|
||||
@ -853,13 +853,13 @@ describe Guardian do
|
||||
end
|
||||
|
||||
it "allows new posts from silenced users included in the pm" do
|
||||
user.update_attribute(:silenced, true)
|
||||
user.update_attribute(:silenced_till, 1.year.from_now)
|
||||
private_message.topic_allowed_users.create!(user_id: user.id)
|
||||
expect(Guardian.new(user).can_create?(Post, private_message)).to be_truthy
|
||||
end
|
||||
|
||||
it "doesn't allow new posts from silenced users not invited to the pm" do
|
||||
user.update_attribute(:silenced, true)
|
||||
user.update_attribute(:silenced_till, 1.year.from_now)
|
||||
expect(Guardian.new(user).can_create?(Post, private_message)).to be_falsey
|
||||
end
|
||||
end
|
||||
@ -1376,7 +1376,7 @@ describe Guardian do
|
||||
|
||||
context 'when user is silenced' do
|
||||
it 'returns false' do
|
||||
user.toggle!(:silenced)
|
||||
user.update_column(:silenced_till, 1.year.from_now)
|
||||
expect(Guardian.new(user).can_moderate?(post)).to be(false)
|
||||
expect(Guardian.new(user).can_moderate?(topic)).to be(false)
|
||||
end
|
||||
|
@ -495,15 +495,7 @@ describe Admin::UsersController do
|
||||
end
|
||||
|
||||
context '.destroy' do
|
||||
before do
|
||||
@delete_me = Fabricate(:user)
|
||||
end
|
||||
|
||||
it "raises an error when the user doesn't have permission" do
|
||||
Guardian.any_instance.expects(:can_delete_user?).with(@delete_me).returns(false)
|
||||
delete :destroy, params: { id: @delete_me.id }, format: :json
|
||||
expect(response).to be_forbidden
|
||||
end
|
||||
let(:delete_me) { Fabricate(:user) }
|
||||
|
||||
it "returns a 403 if the user doesn't exist" do
|
||||
delete :destroy, params: { id: 123123 }, format: :json
|
||||
@ -511,31 +503,26 @@ describe Admin::UsersController do
|
||||
end
|
||||
|
||||
context "user has post" do
|
||||
let(:topic) { create_topic(user: delete_me) }
|
||||
|
||||
before do
|
||||
@user = Fabricate(:user)
|
||||
topic = create_topic(user: @user)
|
||||
_post = create_post(topic: topic, user: @user)
|
||||
@user.stubs(:first_post_created_at).returns(Time.zone.now)
|
||||
User.expects(:find_by).with(id: @delete_me.id).returns(@user)
|
||||
_post = create_post(topic: topic, user: delete_me)
|
||||
end
|
||||
|
||||
it "returns an error" do
|
||||
delete :destroy, params: { id: @delete_me.id }, format: :json
|
||||
delete :destroy, params: { id: delete_me.id }, format: :json
|
||||
expect(response).to be_forbidden
|
||||
end
|
||||
|
||||
it "doesn't return an error if delete_posts == true" do
|
||||
UserDestroyer.any_instance.expects(:destroy).with(@user, has_entry('delete_posts' => true)).returns(true)
|
||||
delete :destroy, params: { id: @delete_me.id, delete_posts: true }, format: :json
|
||||
delete :destroy, params: { id: delete_me.id, delete_posts: true }, format: :json
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
it "deletes the user record" do
|
||||
UserDestroyer.any_instance.expects(:destroy).returns(true)
|
||||
delete :destroy, params: { id: @delete_me.id }, format: :json
|
||||
delete :destroy, params: { id: delete_me.id }, format: :json
|
||||
end
|
||||
end
|
||||
|
||||
@ -590,9 +577,10 @@ describe Admin::UsersController do
|
||||
|
||||
it "raises an error when the user doesn't have permission" do
|
||||
Guardian.any_instance.expects(:can_silence_user?).with(@reg_user).returns(false)
|
||||
UserSilencer.expects(:silence).never
|
||||
put :silence, params: { user_id: @reg_user.id }, format: :json
|
||||
expect(response).to be_forbidden
|
||||
@reg_user.reload
|
||||
expect(@reg_user).not_to be_silenced
|
||||
end
|
||||
|
||||
it "returns a 403 if the user doesn't exist" do
|
||||
@ -601,8 +589,43 @@ describe Admin::UsersController do
|
||||
end
|
||||
|
||||
it "punishes the user for spamming" do
|
||||
UserSilencer.expects(:silence).with(@reg_user, @user, anything)
|
||||
put :silence, params: { user_id: @reg_user.id }, format: :json
|
||||
expect(response).to be_success
|
||||
@reg_user.reload
|
||||
expect(@reg_user).to be_silenced
|
||||
end
|
||||
|
||||
it "will set a length of time if provided" do
|
||||
future_date = 1.month.from_now.to_date
|
||||
put(
|
||||
:silence,
|
||||
params: {
|
||||
user_id: @reg_user.id,
|
||||
silenced_till: future_date
|
||||
},
|
||||
format: :json
|
||||
)
|
||||
@reg_user.reload
|
||||
expect(@reg_user.silenced_till).to eq(future_date)
|
||||
end
|
||||
|
||||
it "will send a message if provided" do
|
||||
Jobs.expects(:enqueue).with(
|
||||
:critical_user_email,
|
||||
has_entries(
|
||||
type: :account_silenced,
|
||||
user_id: @reg_user.id
|
||||
)
|
||||
)
|
||||
|
||||
put(
|
||||
:silence,
|
||||
params: {
|
||||
user_id: @reg_user.id,
|
||||
message: "Email this to the user"
|
||||
},
|
||||
format: :json
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -710,7 +710,7 @@ describe PostsController do
|
||||
expect(parsed["action"]).to eq("enqueued")
|
||||
|
||||
user.reload
|
||||
expect(user.silenced).to eq(true)
|
||||
expect(user).to be_silenced
|
||||
|
||||
qp = QueuedPost.first
|
||||
|
||||
@ -718,7 +718,7 @@ describe PostsController do
|
||||
qp.approve!(mod)
|
||||
|
||||
user.reload
|
||||
expect(user.silenced).to eq(false)
|
||||
expect(user).not_to be_silenced
|
||||
end
|
||||
|
||||
it "doesn't enqueue replies when the topic is closed" do
|
||||
@ -763,7 +763,7 @@ describe PostsController do
|
||||
expect(parsed["action"]).to eq("enqueued")
|
||||
|
||||
user.reload
|
||||
expect(user.silenced).to eq(true)
|
||||
expect(user).to be_silenced
|
||||
end
|
||||
|
||||
it "can send a message to a group" do
|
||||
|
@ -24,7 +24,7 @@ describe Jobs::GrantAnniversaryBadges do
|
||||
end
|
||||
|
||||
it "doesn't award to a silenced user" do
|
||||
user = Fabricate(:user, created_at: 400.days.ago, silenced: true)
|
||||
user = Fabricate(:user, created_at: 400.days.ago, silenced_till: 1.year.from_now)
|
||||
Fabricate(:post, user: user, created_at: 1.week.ago)
|
||||
granter.execute({})
|
||||
|
||||
|
@ -66,7 +66,7 @@ describe Jobs::NotifyMailingListSubscribers do
|
||||
end
|
||||
|
||||
context "to a silenced user" do
|
||||
before { mailing_list_user.update(silenced: true) }
|
||||
before { mailing_list_user.update(silenced_till: 1.year.from_now) }
|
||||
include_examples "no emails"
|
||||
end
|
||||
|
||||
|
@ -677,7 +677,7 @@ describe Topic do
|
||||
|
||||
context "when moderator post fails to be created" do
|
||||
before do
|
||||
user.toggle!(:silenced)
|
||||
user.update_column(:silenced_till, 1.year.from_now)
|
||||
end
|
||||
|
||||
it "should not increment moderator_posts_count" do
|
||||
@ -833,7 +833,7 @@ describe Topic do
|
||||
it_should_behave_like 'a status that closes a topic'
|
||||
|
||||
context 'topic was set to close when it was created' do
|
||||
it 'puts the autoclose duration in the moderator post' do
|
||||
it 'includes the autoclose duration in the moderator post' do
|
||||
freeze_time(Time.new(2000, 1, 1))
|
||||
@topic.created_at = 3.days.ago
|
||||
@topic.update_status(status, true, @user)
|
||||
@ -842,7 +842,7 @@ describe Topic do
|
||||
end
|
||||
|
||||
context 'topic was set to close after it was created' do
|
||||
it 'puts the autoclose duration in the moderator post' do
|
||||
it 'includes the autoclose duration in the moderator post' do
|
||||
freeze_time(Time.new(2000, 1, 1))
|
||||
|
||||
@topic.created_at = 7.days.ago
|
||||
|
@ -1532,4 +1532,32 @@ describe User do
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
describe "silenced?" do
|
||||
|
||||
it "is not silenced by default" do
|
||||
expect(Fabricate(:user)).not_to be_silenced
|
||||
end
|
||||
|
||||
it "is not silenced with a date in the past" do
|
||||
expect(Fabricate(:user, silenced_till: 1.month.ago)).not_to be_silenced
|
||||
end
|
||||
|
||||
it "is is silenced with a date in the future" do
|
||||
expect(Fabricate(:user, silenced_till: 1.month.from_now)).to be_silenced
|
||||
end
|
||||
|
||||
context "finders" do
|
||||
let!(:user0) { Fabricate(:user, silenced_till: 1.month.ago) }
|
||||
let!(:user1) { Fabricate(:user, silenced_till: 1.month.from_now) }
|
||||
|
||||
it "doesn't return old silenced records" do
|
||||
expect(User.silenced).to_not include(user0)
|
||||
expect(User.silenced).to include(user1)
|
||||
expect(User.not_silenced).to include(user0)
|
||||
expect(User.not_silenced).to_not include(user1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -275,7 +275,7 @@ describe SpamRule::AutoSilence do
|
||||
end
|
||||
|
||||
context "silenced, but has higher trust level now" do
|
||||
let(:user) { Fabricate(:user, silenced: true, trust_level: TrustLevel[1]) }
|
||||
let(:user) { Fabricate(:user, silenced_till: 1.year.from_now, trust_level: TrustLevel[1]) }
|
||||
subject { described_class.new(user) }
|
||||
|
||||
it 'returns false' do
|
||||
|
@ -7,7 +7,7 @@ describe UserSilencer do
|
||||
end
|
||||
|
||||
describe 'silence' do
|
||||
let(:user) { stub_everything(save: true) }
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:silencer) { UserSilencer.new(user) }
|
||||
subject(:silence_user) { silencer.silence }
|
||||
|
||||
@ -53,7 +53,7 @@ describe UserSilencer do
|
||||
end
|
||||
|
||||
it "doesn't send a pm if the user is already silenced" do
|
||||
user.stubs(:silenced?).returns(true)
|
||||
user.silenced_till = 1.year.from_now
|
||||
SystemMessage.unstub(:create)
|
||||
SystemMessage.expects(:create).never
|
||||
expect(silence_user).to eq(false)
|
||||
@ -73,7 +73,7 @@ describe UserSilencer do
|
||||
subject(:unsilence_user) { UserSilencer.unsilence(user, Fabricate.build(:admin)) }
|
||||
|
||||
it 'unsilences the user' do
|
||||
u = Fabricate(:user, silenced: true)
|
||||
u = Fabricate(:user, silenced_till: 1.year.from_now)
|
||||
expect { UserSilencer.unsilence(u) }.to change { u.reload.silenced? }
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user