FIX: When disagreeing with a flag that silenced a user, unsilence them

Previously it would unhide their post but leave them silenced.

This fix also cleans up some of the helper classes to make it easier
to pass extra data to the silencing code (for example, a link to the
post that caused the user to be silenced.)

This patch also refactors the auto_silence specs to avoid using
stubs.
This commit is contained in:
Robin Ward 2019-02-07 13:46:05 -05:00
parent 44d9bc12c9
commit bc3efab816
14 changed files with 186 additions and 174 deletions

View File

@ -114,8 +114,6 @@ class Admin::FlagsController < Admin::AdminController
PostAction.clear_flags!(post, current_user) PostAction.clear_flags!(post, current_user)
post.unhide!
render body: nil render body: nil
end end

View File

@ -231,6 +231,8 @@ class PostAction < ActiveRecord::Base
end end
update_flagged_posts_count update_flagged_posts_count
undo_hide_and_silence(post)
end end
def self.defer_flags!(post, moderator, delete_post = false) def self.defer_flags!(post, moderator, delete_post = false)
@ -375,6 +377,13 @@ class PostAction < ActiveRecord::Base
PostAction.where(where_attrs).first PostAction.where(where_attrs).first
end end
def self.undo_hide_and_silence(post)
return unless post.hidden?
post.unhide!
UserSilencer.unsilence(post.user) if UserSilencer.was_silenced_for?(post)
end
def self.copy(original_post, target_post) def self.copy(original_post, target_post)
cols_to_copy = (column_names - %w{id post_id}).join(', ') cols_to_copy = (column_names - %w{id post_id}).join(', ')
@ -537,7 +546,7 @@ class PostAction < ActiveRecord::Base
post = Post.with_deleted.where(id: post_id).first post = Post.with_deleted.where(id: post_id).first
PostAction.auto_close_if_threshold_reached(post.topic) PostAction.auto_close_if_threshold_reached(post.topic)
PostAction.auto_hide_if_needed(user, post, post_action_type_key) PostAction.auto_hide_if_needed(user, post, post_action_type_key)
SpamRulesEnforcer.enforce!(post.user) SpamRule::AutoSilence.new(post.user, post).perform
end end
def create_user_action def create_user_action

View File

@ -1,37 +1,38 @@
class SpamRule::AutoSilence class SpamRule::AutoSilence
def initialize(user) attr_reader :group_message
def initialize(user, post = nil)
@user = user @user = user
end @post = post
def self.silence?(user)
self.new(user).silence?
end
def self.punish!(user)
self.new(user).silence_user
end end
def perform def perform
silence_user if silence? I18n.with_locale(SiteSetting.default_locale) do
silence_user if should_autosilence?
end
end end
def silence? def self.prevent_posting?(user)
return true if @user.silenced? user.blank? || user.silenced? || new(user).should_autosilence?
end
def should_autosilence?
return false if @user.blank?
return false if @user.staged? return false if @user.staged?
return false if @user.has_trust_level?(TrustLevel[1]) return false if @user.has_trust_level?(TrustLevel[1])
if SiteSetting.num_spam_flags_to_silence_new_user > (0) && if SiteSetting.num_spam_flags_to_silence_new_user > 0 &&
SiteSetting.num_users_to_silence_new_user > (0) && SiteSetting.num_users_to_silence_new_user > 0 &&
num_spam_flags_against_user >= (SiteSetting.num_spam_flags_to_silence_new_user) && num_spam_flags_against_user >= SiteSetting.num_spam_flags_to_silence_new_user &&
num_users_who_flagged_spam_against_user >= (SiteSetting.num_users_to_silence_new_user) num_users_who_flagged_spam_against_user >= SiteSetting.num_users_to_silence_new_user
return true return true
end end
if SiteSetting.num_tl3_flags_to_silence_new_user > (0) && if SiteSetting.num_tl3_flags_to_silence_new_user > 0 &&
SiteSetting.num_tl3_users_to_silence_new_user > (0) && SiteSetting.num_tl3_users_to_silence_new_user > 0 &&
num_tl3_flags_against_user >= (SiteSetting.num_tl3_flags_to_silence_new_user) && num_tl3_flags_against_user >= SiteSetting.num_tl3_flags_to_silence_new_user &&
num_tl3_users_who_flagged >= (SiteSetting.num_tl3_users_to_silence_new_user) num_tl3_users_who_flagged >= SiteSetting.num_tl3_users_to_silence_new_user
return true return true
end end
@ -72,8 +73,16 @@ class SpamRule::AutoSilence
def silence_user def silence_user
Post.transaction do Post.transaction do
if UserSilencer.silence(@user, Discourse.system_user, message: :too_many_spam_flags) && SiteSetting.notify_mods_when_user_silenced
GroupMessage.create(Group[:moderators].name, :user_automatically_silenced, user: @user, limit_once_per: false) silencer = UserSilencer.new(
@user,
Discourse.system_user,
message: :too_many_spam_flags,
post_id: @post&.id
)
if silencer.silence && SiteSetting.notify_mods_when_user_silenced
@group_message = GroupMessage.create(Group[:moderators].name, :user_automatically_silenced, user: @user, limit_once_per: false)
end end
end end
end end

View File

@ -5,6 +5,7 @@ class SpamRule::FlagSockpuppets
end end
def perform def perform
I18n.with_locale(SiteSetting.default_locale) do
if SiteSetting.flag_sockpuppets && reply_is_from_sockpuppet? if SiteSetting.flag_sockpuppets && reply_is_from_sockpuppet?
flag_sockpuppet_users flag_sockpuppet_users
true true
@ -12,6 +13,7 @@ class SpamRule::FlagSockpuppets
false false
end end
end end
end
def reply_is_from_sockpuppet? def reply_is_from_sockpuppet?
return false if @post.try(:post_number) == 1 return false if @post.try(:post_number) == 1

View File

@ -1,22 +0,0 @@
# The SpamRulesEnforcer class takes action against users based on flags that their posts
# receive, their trust level, etc.
class SpamRulesEnforcer
def self.enforce!(arg)
SpamRulesEnforcer.new(arg).enforce!
end
def initialize(arg)
@user = arg if arg.is_a?(User)
@post = arg if arg.is_a?(Post)
end
def enforce!
I18n.with_locale(SiteSetting.default_locale) do
SpamRule::AutoSilence.new(@user).perform if @user
SpamRule::FlagSockpuppets.new(@post).perform if @post
end
true
end
end

View File

@ -16,6 +16,12 @@ class UserSilencer
UserSilencer.new(user, by_user, opts).unsilence UserSilencer.new(user, by_user, opts).unsilence
end end
def self.was_silenced_for?(post)
return false unless post.present?
UserHistory.where(action: UserHistory.actions[:silence_user], post: post).exists?
end
def silence def silence
hide_posts unless @opts[:keep_posts] hide_posts unless @opts[:keep_posts]
unless @user.silenced_till.present? unless @user.silenced_till.present?

View File

@ -108,10 +108,9 @@ module PostGuardian
# Creating Method # Creating Method
def can_create_post?(parent) def can_create_post?(parent)
return false if !SiteSetting.enable_system_message_replies? && parent.try(:subtype) == "system_message" return false if !SiteSetting.enable_system_message_replies? && parent.try(:subtype) == "system_message"
(!SpamRule::AutoSilence.silence?(@user) || (!!parent.try(:private_message?) && parent.allowed_users.include?(@user))) && ( (!SpamRule::AutoSilence.prevent_posting?(@user) || (!!parent.try(:private_message?) && parent.allowed_users.include?(@user))) && (
!parent || !parent ||
!parent.category || !parent.category ||
Category.post_create_allowed(self).where(id: parent.category.id).count == 1 Category.post_create_allowed(self).where(id: parent.category.id).count == 1

View File

@ -359,7 +359,7 @@ class PostCreator
limit_once_per: 24.hours, limit_once_per: 24.hours,
message_params: { domains: @post.linked_hosts.keys.join(', ') }) message_params: { domains: @post.linked_hosts.keys.join(', ') })
elsif @post && errors.blank? && !skip_validations? elsif @post && errors.blank? && !skip_validations?
SpamRulesEnforcer.enforce!(@post) SpamRule::FlagSockpuppets.new(@post).perform
end end
end end

View File

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
describe SpamRulesEnforcer do describe "spammers on same IP" do
let(:ip_address) { '182.189.119.174' } let(:ip_address) { '182.189.119.174' }
let!(:spammer1) { Fabricate(:user, ip_address: ip_address) } let!(:spammer1) { Fabricate(:user, ip_address: ip_address) }

View File

@ -2,9 +2,9 @@
require 'rails_helper' require 'rails_helper'
describe SpamRulesEnforcer do describe "spam rules for users" do
describe 'auto-silenceing users based on flagging' do describe 'auto-silence users based on flagging' do
let!(:admin) { Fabricate(:admin) } # needed to send a system message let!(:admin) { Fabricate(:admin) } # needed to send a system message
let!(:moderator) { Fabricate(:moderator) } let!(:moderator) { Fabricate(:moderator) }
let(:user1) { Fabricate(:user) } let(:user1) { Fabricate(:user) }

View File

@ -102,4 +102,26 @@ RSpec.describe Admin::FlagsController do
expect(first_post.deleted_at).to eq(nil) expect(first_post.deleted_at).to eq(nil)
end end
end end
context '#disagree' do
it "unhides the post and unsilences the user if disagreed" do
SiteSetting.num_spam_flags_to_silence_new_user = 1
SiteSetting.num_users_to_silence_new_user = 1
new_user = Fabricate(:newuser)
new_post = create_post(user: new_user)
PostAction.act(Fabricate(:leader), new_post, PostActionType.types[:spam])
post "/admin/flags/disagree/#{new_post.id}.json"
expect(response.status).to eq(200)
new_post.reload
new_user.reload
expect(new_post).to_not be_hidden
expect(new_post.spam_count).to eq(0)
expect(new_user).to_not be_silenced
end
end
end end

View File

@ -9,24 +9,25 @@ describe SpamRule::AutoSilence do
end end
describe 'perform' do describe 'perform' do
let(:post) { Fabricate.build(:post, user: Fabricate.build(:user, trust_level: TrustLevel[0])) } let(:user) { Fabricate.build(:newuser) }
let(:post) { Fabricate(:post, user: user) }
subject { described_class.new(post.user) } subject { described_class.new(post.user) }
it 'takes no action if user should not be silenced' do it 'takes no action if user should not be silenced' do
subject.stubs(:silence?).returns(false)
subject.expects(:silence_user).never
subject.perform subject.perform
expect(post.user.reload).not_to be_silenced
end end
it 'delivers punishment when user should be silenced' do it 'delivers punishment when user should be silenced' do
subject.stubs(:silence?).returns(true) SiteSetting.num_spam_flags_to_silence_new_user = 1
subject.expects(:silence_user) SiteSetting.num_users_to_silence_new_user = 1
PostAction.act(Discourse.system_user, post, PostActionType.types[:spam])
subject.perform subject.perform
expect(post.user.reload).to be_silenced
end end
end end
describe 'num_spam_flags_against_user' do describe 'num_spam_flags_against_user' do
before { described_class.any_instance.stubs(:silence_user) }
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
let(:enforcer) { described_class.new(post.user) } let(:enforcer) { described_class.new(post.user) }
subject { enforcer.num_spam_flags_against_user } subject { enforcer.num_spam_flags_against_user }
@ -53,7 +54,6 @@ describe SpamRule::AutoSilence do
end end
describe 'num_users_who_flagged_spam_against_user' do describe 'num_users_who_flagged_spam_against_user' do
before { described_class.any_instance.stubs(:silence_user) }
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
let(:enforcer) { described_class.new(post.user) } let(:enforcer) { described_class.new(post.user) }
subject { enforcer.num_users_who_flagged_spam_against_user } subject { enforcer.num_users_who_flagged_spam_against_user }
@ -130,146 +130,157 @@ describe SpamRule::AutoSilence do
let!(:post) { Fabricate(:post, user: user) } let!(:post) { Fabricate(:post, user: user) }
subject { described_class.new(user) } subject { described_class.new(user) }
before do
described_class.stubs(:silence?).with { |u| u.id != user.id }.returns(false)
described_class.stubs(:silence?).with { |u| u.id == user.id }.returns(true)
subject.stubs(:silence?).returns(true)
end
context 'user is not silenced' do context 'user is not silenced' do
before do
UserSilencer.expects(:silence).with(user, Discourse.system_user, message: :too_many_spam_flags).returns(true)
end
it 'prevents the user from making new posts' do it 'prevents the user from making new posts' do
subject.silence_user subject.silence_user
expect(user).to be_silenced
expect(Guardian.new(user).can_create_post?(nil)).to be_falsey expect(Guardian.new(user).can_create_post?(nil)).to be_falsey
end end
context "with a moderator" do
let!(:moderator) { Fabricate(:moderator) }
it 'sends private message to moderators' do it 'sends private message to moderators' do
SiteSetting.notify_mods_when_user_silenced = true SiteSetting.notify_mods_when_user_silenced = true
moderator = Fabricate(:moderator)
GroupMessage.expects(:create).with do |group, msg_type, params|
group == (Group[:moderators].name) && msg_type == (:user_automatically_silenced) && params[:user].id == (user.id)
end
subject.silence_user subject.silence_user
expect(subject.group_message).to be_present
end end
it "doesn't send a pm to moderators if notify_mods_when_user_silenced is false" do it "doesn't send a pm to moderators if notify_mods_when_user_silenced is false" do
SiteSetting.notify_mods_when_user_silenced = false SiteSetting.notify_mods_when_user_silenced = false
GroupMessage.expects(:create).never
subject.silence_user subject.silence_user
expect(subject.group_message).to be_blank
end
end end
end end
context 'user is already silenced' do context 'user is already silenced' do
before do before do
UserSilencer.expects(:silence).with(user, Discourse.system_user, message: :too_many_spam_flags).returns(false) UserSilencer.silence(user)
end end
it "doesn't send a pm to moderators if the user is already silenced" do it "doesn't send a pm to moderators if the user is already silenced" do
GroupMessage.expects(:create).never
subject.silence_user subject.silence_user
expect(subject.group_message).to be_blank
end end
end end
end end
describe 'silence?' do describe 'autosilenced?' do
let(:user) { Fabricate(:newuser) }
let(:flagger) { Fabricate(:user) }
let(:flagger2) { Fabricate(:user) }
let(:post) { Fabricate(:post, user: user) }
let(:post2) { Fabricate(:post, user: user) }
context "higher trust levels or staff" do
it "should not autosilence any of them" do
PostAction.act(flagger, post, PostActionType.types[:spam])
PostAction.act(flagger2, post, PostActionType.types[:spam])
context 'never been silenced' do
shared_examples "can't be silenced" do
it "returns false" do
enforcer = described_class.new(user) enforcer = described_class.new(user)
enforcer.expects(:num_spam_flags_against_user).never expect(enforcer.should_autosilence?).to eq(true)
enforcer.expects(:num_users_who_flagged_spam_against_user).never
enforcer.expects(:num_flags_against_user).never
enforcer.expects(:num_users_who_flagged).never
expect(enforcer.silence?).to eq(false)
end
end
(1..4).each do |trust_level| user.trust_level = 1
context "user has trust level #{trust_level}" do expect(enforcer.should_autosilence?).to eq(false)
let(:user) { Fabricate(:user, trust_level: trust_level) }
include_examples "can't be silenced"
end
end
context "user is an admin" do user.trust_level = 2
let(:user) { Fabricate(:admin) } expect(enforcer.should_autosilence?).to eq(false)
include_examples "can't be silenced"
end
context "user is a moderator" do user.trust_level = 3
let(:user) { Fabricate(:moderator) } expect(enforcer.should_autosilence?).to eq(false)
include_examples "can't be silenced"
user.trust_level = 4
expect(enforcer.should_autosilence?).to eq(false)
user.trust_level = 0
user.moderator = true
expect(enforcer.should_autosilence?).to eq(false)
user.moderator = false
user.admin = true
expect(enforcer.should_autosilence?).to eq(false)
end end
end end
context 'new user' do context 'new user' do
let(:user) { Fabricate(:user, trust_level: TrustLevel[0]) }
subject { described_class.new(user) } subject { described_class.new(user) }
it 'returns false if there are no spam flags' do it 'returns false if there are no spam flags' do
subject.stubs(:num_spam_flags_against_user).returns(0) expect(subject.num_spam_flags_against_user).to eq(0)
subject.stubs(:num_users_who_flagged_spam_against_user).returns(0) expect(subject.num_users_who_flagged_spam_against_user).to eq(0)
expect(subject.silence?).to be_falsey expect(subject.should_autosilence?).to eq(false)
end end
it 'returns false if there are not received enough flags' do it 'returns false if there are not received enough flags' do
subject.stubs(:num_spam_flags_against_user).returns(1) PostAction.act(flagger, post, PostActionType.types[:spam])
subject.stubs(:num_users_who_flagged_spam_against_user).returns(2) expect(subject.num_spam_flags_against_user).to eq(1)
expect(subject.silence?).to be_falsey expect(subject.num_users_who_flagged_spam_against_user).to eq(1)
expect(subject.should_autosilence?).to eq(false)
end end
it 'returns false if there have not been enough users' do it 'returns false if there have not been enough users' do
subject.stubs(:num_spam_flags_against_user).returns(2) PostAction.act(flagger, post, PostActionType.types[:spam])
subject.stubs(:num_users_who_flagged_spam_against_user).returns(1) PostAction.act(flagger, post2, PostActionType.types[:spam])
expect(subject.silence?).to be_falsey expect(subject.num_spam_flags_against_user).to eq(2)
expect(subject.num_users_who_flagged_spam_against_user).to eq(1)
expect(subject.should_autosilence?).to eq(false)
end end
it 'returns false if num_spam_flags_to_silence_new_user is 0' do it 'returns false if num_spam_flags_to_silence_new_user is 0' do
SiteSetting.num_spam_flags_to_silence_new_user = 0 SiteSetting.num_spam_flags_to_silence_new_user = 0
subject.stubs(:num_spam_flags_against_user).returns(100) PostAction.act(flagger, post, PostActionType.types[:spam])
subject.stubs(:num_users_who_flagged_spam_against_user).returns(100) PostAction.act(flagger2, post, PostActionType.types[:spam])
expect(subject.silence?).to be_falsey expect(subject.should_autosilence?).to eq(false)
end end
it 'returns false if num_users_to_silence_new_user is 0' do it 'returns false if num_users_to_silence_new_user is 0' do
SiteSetting.num_users_to_silence_new_user = 0 SiteSetting.num_users_to_silence_new_user = 0
subject.stubs(:num_spam_flags_against_user).returns(100) PostAction.act(flagger, post, PostActionType.types[:spam])
subject.stubs(:num_users_who_flagged_spam_against_user).returns(100) PostAction.act(flagger2, post, PostActionType.types[:spam])
expect(subject.silence?).to be_falsey expect(subject.should_autosilence?).to eq(false)
end end
it 'returns true when there are enough flags from enough users' do it 'returns true when there are enough flags from enough users' do
subject.stubs(:num_spam_flags_against_user).returns(2) PostAction.act(flagger, post, PostActionType.types[:spam])
subject.stubs(:num_users_who_flagged_spam_against_user).returns(2) PostAction.act(flagger2, post, PostActionType.types[:spam])
expect(subject.silence?).to be_truthy expect(subject.num_spam_flags_against_user).to eq(2)
expect(subject.num_users_who_flagged_spam_against_user).to eq(2)
expect(subject.should_autosilence?).to eq(true)
end end
context "all types of flags" do context "all types of flags" do
let(:leader1) { Fabricate(:leader) }
let(:leader2) { Fabricate(:leader) }
before do before do
SiteSetting.num_tl3_flags_to_silence_new_user = 3 SiteSetting.num_tl3_flags_to_silence_new_user = 3
SiteSetting.num_tl3_users_to_silence_new_user = 2 SiteSetting.num_tl3_users_to_silence_new_user = 2
end end
it 'returns false if there are not enough flags' do it 'returns false if there are not enough flags' do
subject.stubs(:num_tl3_flags_against_user).returns(1) PostAction.act(leader1, post, PostActionType.types[:inappropriate])
subject.stubs(:num_tl3_users_who_flagged).returns(1) expect(subject.num_tl3_flags_against_user).to eq(1)
expect(subject.silence?).to be_falsey expect(subject.num_tl3_users_who_flagged).to eq(1)
expect(subject.should_autosilence?).to be_falsey
end end
it 'returns false if enough flags but not enough users' do it 'returns false if enough flags but not enough users' do
subject.stubs(:num_tl3_flags_against_user).returns(3) PostAction.act(leader1, post, PostActionType.types[:inappropriate])
subject.stubs(:num_tl3_users_who_flagged).returns(1) PostAction.act(leader1, post2, PostActionType.types[:inappropriate])
expect(subject.silence?).to be_falsey PostAction.act(leader1, Fabricate(:post, user: user), PostActionType.types[:inappropriate])
expect(subject.num_tl3_flags_against_user).to eq(3)
expect(subject.num_tl3_users_who_flagged).to eq(1)
expect(subject.should_autosilence?).to eq(false)
end end
it 'returns true if enough flags and users' do it 'returns true if enough flags and users' do
subject.stubs(:num_tl3_flags_against_user).returns(3) PostAction.act(leader1, post, PostActionType.types[:inappropriate])
subject.stubs(:num_tl3_users_who_flagged).returns(2) PostAction.act(leader1, post2, PostActionType.types[:inappropriate])
expect(subject.silence?).to eq(true) PostAction.act(leader2, post, PostActionType.types[:inappropriate])
expect(subject.num_tl3_flags_against_user).to eq(3)
expect(subject.num_tl3_users_who_flagged).to eq(2)
expect(subject.should_autosilence?).to eq(true)
end end
end end
end end
@ -279,7 +290,7 @@ describe SpamRule::AutoSilence do
subject { described_class.new(user) } subject { described_class.new(user) }
it 'returns false' do it 'returns false' do
expect(subject.silence?).to be_truthy expect(subject.should_autosilence?).to eq(false)
end end
end end
end end

View File

@ -1,29 +0,0 @@
require 'rails_helper'
describe SpamRulesEnforcer do
before do
SystemMessage.stubs(:create)
end
describe 'enforce!' do
context 'post argument' do
subject(:enforce) { described_class.enforce!(Fabricate.build(:post)) }
it 'performs the FlagSockpuppetRule' do
SpamRule::FlagSockpuppets.any_instance.expects(:perform).once
enforce
end
end
context 'user argument' do
subject(:enforce) { described_class.enforce!(Fabricate.build(:user)) }
it 'performs the AutoSilence' do
SpamRule::AutoSilence.any_instance.expects(:perform).once
enforce
end
end
end
end

View File

@ -112,6 +112,13 @@ describe UserSilencer do
expect(post.topic.reload).to_not be_visible expect(post.topic.reload).to_not be_visible
end end
it "allows us to silence the user for a particular post" do
expect(UserSilencer.was_silenced_for?(post)).to eq(false)
UserSilencer.new(user, Discourse.system_user, post_id: post.id).silence
expect(user).to be_silenced
expect(UserSilencer.was_silenced_for?(post)).to eq(true)
end
it "doesn't hide posts if user is TL1" do it "doesn't hide posts if user is TL1" do
user.trust_level = 1 user.trust_level = 1
subject.silence subject.silence