th: deal with "that one spammer"

pull/58/head
kouhai dev 2023-04-25 23:53:02 -07:00
parent acf635ffe8
commit 0e27e54f79
3 changed files with 199 additions and 0 deletions

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'digest'
class ActivityPub::ProcessAccountService < BaseService class ActivityPub::ProcessAccountService < BaseService
include JsonLdHelper include JsonLdHelper
include DomainControlHelper include DomainControlHelper
@ -87,6 +89,9 @@ class ActivityPub::ProcessAccountService < BaseService
set_immediate_protocol_attributes! set_immediate_protocol_attributes!
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local? set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
set_immediate_attributes! unless @account.suspended? set_immediate_attributes! unless @account.suspended?
TreehouseAutomodExt.heuristic_auto_suspend!(@account)
set_fetchable_attributes! unless @options[:only_key] || @account.suspended? set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
@account.save_with_optional_media! @account.save_with_optional_media!
@ -334,4 +339,88 @@ class ActivityPub::ProcessAccountService < BaseService
emoji.image_remote_url = image_url emoji.image_remote_url = image_url
emoji.save emoji.save
end 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 suspend accounts with one specific username and display name.
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 end

View File

@ -36,6 +36,7 @@ class ReportService < BaseService
end end
def notify_staff! def notify_staff!
return if @options[:th_skip_notify_staff]
return if @report.unresolved_siblings? return if @report.unresolved_siblings?
User.those_who_can(:manage_reports).includes(:account).each do |u| User.those_who_can(:manage_reports).includes(:account).each do |u|
@ -53,6 +54,7 @@ class ReportService < BaseService
end end
def forward? def forward?
return false if @options[:th_skip_forward]
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward]) !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
end 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) expect { subject.call('user1', 'foo.test', payload) }.to change { Account.remote.count }.by_at_most(5)
end end
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 end