th: deal with "that one spammer"
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

kouhai dev 2023-04-25 23:53:02 -07:00
parent acf635ffe8
commit 99aca5d9e6
3 changed files with 199 additions and 0 deletions

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'digest'
class ActivityPub::ProcessAccountService < BaseService
include JsonLdHelper
include DomainControlHelper
@ -87,6 +89,9 @@ class ActivityPub::ProcessAccountService < BaseService
set_immediate_protocol_attributes!
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
set_immediate_attributes! unless @account.suspended?
TreehouseAutomodExt.heuristic_auto_suspend!(@account)
set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
@account.save_with_optional_media!
@ -334,4 +339,88 @@ class ActivityPub::ProcessAccountService < BaseService
emoji.image_remote_url = image_url
emoji.save
end
module TreehouseAutomodExt
HEURISTIC_AUTO_SUSPEND_ACTIVE = ENV.fetch('TH_HEURISTIC_AUTO_SUSPEND', '') == 'that-one-spammer'
AUTOMOD_ACCOUNT_USERNAME = ENV['TH_STAFF_ACCOUNT']
# hardcoded for now
# md5 because they don't deserve more mentions
HEURISTIC_NAMES = {
'0116a9deace3289b7092e945ef5ca0a5' => Set['57d3d0b932cc9cd01be6b2f4e82c1a4a']
}
# probably mathematically impossible to collide, but just in case...
HEURISTIC_MAX_LEN = 16
COMMENT_HEADER = <<~EOS
Tracking Report - automatically created by TreehouseAutomodExt
EOS
WARNING_TEXT = <<~EOS
Tracking Infraction - automatically created by TreehouseAutomodExt
EOS
EXPLANATION = <<~EOS
This account was automatically suspended by TreehouseAutomodExt, an unsupported feature of Treehouse Social.
Currently, the heuristic should only automatically suspends any accounts with one specific username and one of several display name variants.
If this action is unexpected, please unset TH_HEURISTIC_AUTO_SUSPEND.
EOS
def self.heuristic_auto_suspend?(account)
return false unless HEURISTIC_AUTO_SUSPEND_ACTIVE
return unless account.username.length < HEURISTIC_MAX_LEN && account.display_name.length < HEURISTIC_MAX_LEN
username_md5 = Digest::MD5.hexdigest(account.username)
display_name_md5 = Digest::MD5.hexdigest(account.display_name)
HEURISTIC_NAMES[username_md5].include?(display_name_md5)
end
def self.heuristic_auto_suspend!(account)
return unless heuristic_auto_suspend?(account)
file_tracking_report!(account) unless account.suspension_origin == :local
account.suspended_at = Time.now.utc unless account.suspension_origin == :local
account.suspension_origin = :local
account.save!
end
def self.file_tracking_report!(account)
reporter = staff_account
return unless reporter
report = ReportService.new.call(
reporter,
account,
{
comment: "#{COMMENT_HEADER}\n\n#{EXPLANATION}",
th_skip_notify_staff: true,
th_skip_forward: true,
}
)
report.spam!
report.assign_to_self!(reporter)
account_action = Admin::AccountAction.new(
type: 'suspend',
report_id: report.id,
target_account: account,
current_account: reporter,
send_email_notification: false,
text: WARNING_TEXT,
)
account_action.save!
report.resolve!(reporter)
end
def self.staff_account
Account.find_local(AUTOMOD_ACCOUNT_USERNAME) if AUTOMOD_ACCOUNT_USERNAME
end
end
end

View File

@ -36,6 +36,7 @@ class ReportService < BaseService
end
def notify_staff!
return if @options[:th_skip_notify_staff]
return if @report.unresolved_siblings?
User.those_who_can(:manage_reports).includes(:account).each do |u|
@ -53,6 +54,7 @@ class ReportService < BaseService
end
def forward?
return false if @options[:th_skip_forward]
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
end

View File

@ -205,4 +205,112 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
expect { subject.call('user1', 'foo.test', payload) }.to change { Account.remote.count }.by_at_most(5)
end
end
context 'TreehouseAutomodExt' do
subject { described_class.new.call(account_username, 'foo.test', payload) }
let(:account_username) { 'evil' }
let(:account_display_name) { 'evil display name' }
let(:account_payload_suspended) { false }
let(:automod_account_username) { nil }
let(:payload) do
{
id: 'https://foo.test',
type: 'Actor',
inbox: 'https://foo.test/inbox',
suspended: account_payload_suspended,
name: account_display_name,
}.with_indifferent_access
end
let(:name_hash_hash) do
{
# 'evil' => 'evil display name'
'4034a346ccee15292d823416f7510a2f' => Set['225e44a7c4a792ee22a4ada2032da7cd']
}
end
before do
stub_const('ActivityPub::ProcessAccountService::TreehouseAutomodExt::HEURISTIC_AUTO_SUSPEND_ACTIVE', true)
stub_const('ActivityPub::ProcessAccountService::TreehouseAutomodExt::AUTOMOD_ACCOUNT_USERNAME', automod_account_username)
stub_const('ActivityPub::ProcessAccountService::TreehouseAutomodExt::HEURISTIC_NAMES', name_hash_hash)
stub_const('ActivityPub::ProcessAccountService::TreehouseAutomodExt::HEURISTIC_MAX_LEN', 20)
end
context 'new account' do
context 'heuristic matching' do
it 'suspends the user locally' do
expect(subject.suspended?).to be true
expect(subject.suspension_origin_local?).to be true
end
end
context 'heuristic not matching' do
let(:account_display_name) { '' }
it 'does nothing' do
expect(subject.suspended?).to be false
end
end
end
context 'existing account' do
let!(:account) { Fabricate(:account, username: account_username, domain: 'foo.test', display_name: account_display_name) }
before do
allow(Admin::SuspensionWorker).to receive(:perform_async)
end
context 'heuristic matching' do
it 'suspends the user locally' do
expect(subject.suspended?).to be true
expect(subject.suspension_origin_local?).to be true
end
end
context 'heuristic not matching' do
let(:account_display_name) { 'not evil display name' }
it 'does nothing' do
expect(subject.suspended?).to be false
end
context 'suspended locally' do
before do
account.suspend!(origin: :local)
end
it 'does nothing' do
expect(subject.suspended?).to be true
end
end
end
end
context 'tracking report' do
let(:automod_account_username) { 'automod_test' }
let!(:automod_user_role) { Fabricate(:user_role, name: 'Automod', permissions: UserRole::FLAGS[:administrator]) }
let!(:automod_account) do
account = Fabricate(:account, username: automod_account_username)
account.user.role_id = automod_user_role.id
account.user.save!
account
end
it 'creates report' do
expect(subject.targeted_reports.empty?).to be_falsy
report = Report.find_by(target_account_id: subject.id, account_id: automod_account.id, assigned_account_id: automod_account.id)
expect(report.comment.starts_with?('Tracking Report - automatically created by TreehouseAutomodExt')).to be_truthy
end
it 'creates account action' do
subject
expect(Admin::ActionLog.find_by(account_id: automod_account.id, target_id: subject.id)).not_to be nil
end
end
end
end