From a5b13f7add542b35e78ca7e91be861bbfba678db Mon Sep 17 00:00:00 2001 From: Kouhai Date: Sat, 17 Feb 2024 23:53:22 -0800 Subject: [PATCH] th: automod v2.1 --- app/lib/activitypub/activity/create.rb | 2 +- app/models/invite.rb | 2 +- config/application.rb | 5 ++ lib/treehouse/automod.rb | 49 +++++++++++----- spec/fabricators/user_fabricator.rb | 4 ++ spec/fabricators/user_role_fabricator.rb | 8 +++ spec/lib/activitypub/activity/create_spec.rb | 61 ++++++++++++++++++++ spec/models/invite_spec.rb | 2 +- 8 files changed, 117 insertions(+), 16 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 27e6a8def2..dd36ccef08 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -86,10 +86,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @status = Status.create!(@params) attach_tags(@status) end + return if Treehouse::Automod.process_status!(@status) resolve_thread(@status) fetch_replies(@status) - # return if Treehouse::Automod.process_status!(@status) distribute forward_for_reply end diff --git a/app/models/invite.rb b/app/models/invite.rb index fc5c38ccde..5fde143cc7 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -56,7 +56,7 @@ class Invite < ApplicationRecord end def created_by_moderator? - self.user.moderator + self.user.can?(:manage_invites) end def th_use_invite_quota? diff --git a/config/application.rb b/config/application.rb index 6f4b8a8487..c0d38ae560 100644 --- a/config/application.rb +++ b/config/application.rb @@ -113,5 +113,10 @@ module Mastodon config.x.th_automod.automod_account_username = ENV['TH_STAFF_ACCOUNT'] config.x.th_automod.account_service_heuristic_auto_suspend_active = ENV.fetch('TH_ACCOUNT_SERVICE_HEURISTIC_AUTO_SUSPEND', '') == 'that-one-spammer' config.x.th_automod.mention_spam_heuristic_auto_limit_active = ENV.fetch('TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE', '') == 'can-spam' + config.x.th_automod.mention_spam_threshold = + begin + value = ENV.fetch('TH_MENTION_SPAM_THRESHOLD', '0').to_i + value == 0 ? Float::INFINITY : value + end end end diff --git a/lib/treehouse/automod.rb b/lib/treehouse/automod.rb index 872038a230..98c68489a5 100644 --- a/lib/treehouse/automod.rb +++ b/lib/treehouse/automod.rb @@ -8,21 +8,26 @@ module Treehouse Tracking Infraction - automatically created by TreehouseAutomod EOS + def self.silence_with_tracking_report!(account, status_ids: [], explanation: "") + account.save! + + self.file_tracking_report!(account, status_ids: status_ids, type: 'silence') unless account.suspension_origin == "local" + end + def self.suspend_with_tracking_report!(account, status_ids: [], explanation: "") account.save! - self.file_tracking_report!(account, status_ids: status_ids) unless account.suspension_origin == "local" - - account.suspend! unless account.suspension_origin == "local" + self.file_tracking_report!(account, status_ids: status_ids, type: 'suspend') unless account.suspension_origin == "local" end - def self.file_tracking_report!(account, status_ids: [], explanation: "") + def self.file_tracking_report!(target_account, status_ids: [], explanation: "", type: 'suspend') reporter = self.staff_account return if reporter.nil? + # status_ids is broken because of validation report = ReportService.new.call( reporter, - account, + target_account, { status_ids: status_ids, comment: explanation.blank? ? COMMENT_HEADER : "#{COMMENT_HEADER}\n\n#{EXPLANATION}", @@ -30,19 +35,25 @@ module Treehouse th_skip_forward: true, } ) - report.spam! + report.save! report.assign_to_self!(reporter) account_action = Admin::AccountAction.new( - type: "suspend", + type: type, report_id: report.id, - target_account: account, + target_account: target_account, current_account: reporter, send_email_notification: false, text: WARNING_TEXT, ) account_action.save! + Admin::ActionLog.create( + account: reporter, + action: account_action, + target: target_account, + ) + report.resolve!(reporter) end @@ -68,18 +79,30 @@ module Treehouse If this action is unexpected, please unset TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE. EOS - # check if the status should be considered spam - # @return true if the status was reported and the account was infracted - def self.process!(status) + def self.is_spam?(status) return false unless Rails.configuration.x.th_automod.mention_spam_heuristic_auto_limit_active account = status.account minimal_effort = account.note.blank? && account.avatar_remote_url.blank? && account.header_remote_url.blank? return false if (account.local? || - account.local_followers_account > 0 || + account.local_followers_count > 0 || !minimal_effort) # minimal effort account, check mentions and account-known age - status.mentions.size > 8 && account.created_at > (Time.now - 1.day) + has_mention_spam = status.mentions.size >= Rails.configuration.x.th_automod.mention_spam_threshold + is_new_account = account.created_at > (Time.now - 1.day) + + has_mention_spam && is_new_account + end + + # check if the status should be considered spam + # @return true if the status was reported and the account was infracted + def self.process!(status) + return false unless self.is_spam?(status) + return true if status.account.silenced? + + Automod.silence_with_tracking_report!(status.account, explanation: EXPLANATION) + + true end end diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index 9031d5cd04..927daed9c7 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -8,3 +8,7 @@ Fabricator(:user) do current_sign_in_at { Time.zone.now } agreement true end + +Fabricator(:moderator_user, :from => :user) do + role { Fabricate(:moderator_role) } +end diff --git a/spec/fabricators/user_role_fabricator.rb b/spec/fabricators/user_role_fabricator.rb index d443227605..cd220de45c 100644 --- a/spec/fabricators/user_role_fabricator.rb +++ b/spec/fabricators/user_role_fabricator.rb @@ -5,3 +5,11 @@ Fabricator(:user_role) do color '' permissions 0 end + +Fabricator(:moderator_role, :from => :user_role) do + name 'fake moderator' + permissions UserRole::Flags::DEFAULT | + UserRole::Flags::CATEGORIES[:moderation] + .map { |p| UserRole::FLAGS[p] } + .reduce(&:|) +end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 5af3615c78..c48ee9dd72 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1155,5 +1155,66 @@ RSpec.describe ActivityPub::Activity::Create do expect(sender.statuses.count).to eq 0 end end + + context 'with automod active' do + subject { described_class.new(json, sender, delivery: true) } + + let(:recipient_a) { Fabricate(:account) } + let(:recipient_b) { Fabricate(:account) } + let(:staff_user) { Fabricate(:moderator_user) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + cc: ActivityPub::TagManager.instance.uri_for(recipient_a), + tag: recipients.map do |recipient| + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + } + end, + } + end + + before do + allow(Rails.configuration.x.th_automod).to receive(:automod_account_username).and_return(staff_user.account.username) + allow(Rails.configuration.x.th_automod).to receive(:mention_spam_heuristic_auto_limit_active).and_return(true) + allow(Rails.configuration.x.th_automod).to receive(:mention_spam_threshold).and_return(2) + allow(subject).to receive(:distribute) + allow(sender).to receive(:silence!).and_call_original + subject.perform + end + + context 'and spammy message' do + let(:recipients) { [recipient_a, recipient_b] } + + it 'silences the sender' do + expect(sender).to have_received(:silence!) + expect(sender.silenced?).to be_truthy + end + + it 'skips distribution' do + expect(subject).not_to have_received(:distribute) + end + + it 'files a tracking report' do + expect(sender.previous_strikes_count).to be_truthy + end + end + + context 'and hammy message' do + let(:recipients) { [recipient_a] } + + it 'does not silence the sender' do + expect(sender.silenced?).to be_falsy + end + + it 'does not file a tracking report' do + expect(sender.reports.empty?).to be_truthy + end + end + end end end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 92df6a959d..8235b5de24 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Invite do let(:max_uses) { 25 } let(:expires_in) { 1.week.in_seconds } let(:regular_user) { Fabricate(:user) } - let(:moderator_user) { Fabricate(:user, moderator: true) } + let(:moderator_user) { Fabricate(:moderator_user) } let(:user) { regular_user } let(:created_at) { Time.at(0) } let(:expires_at) { Time.at(0) + expires_in }