From 549c4670fe0d9f78aec37a001abfae36fb6c4792 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 19 Nov 2024 16:54:24 +0100 Subject: [PATCH] Add stop-gap antispam measure for current spam wave --- app/lib/antispam.rb | 45 +++++++++++++++++++++++++++++ app/services/post_status_service.rb | 8 +++++ 2 files changed, 53 insertions(+) create mode 100644 app/lib/antispam.rb diff --git a/app/lib/antispam.rb b/app/lib/antispam.rb new file mode 100644 index 0000000000..bc4841280f --- /dev/null +++ b/app/lib/antispam.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Antispam + include Redisable + + ACCOUNT_AGE_EXEMPTION = 1.week.freeze + + class SilentlyDrop < StandardError + attr_reader :status + + def initialize(status) + super() + + @status = status + + status.created_at = Time.now.utc + status.id = Mastodon::Snowflake.id_at(status.created_at) + status.in_reply_to_account_id = status.thread&.account_id + + status.delete # Make sure this is not persisted + end + end + + def local_preflight_check!(status) + return unless spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } + return unless status.thread.present? && !status.thread.account.following?(status.account) + return unless status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago + + report_if_needed!(status.account) + + raise SilentlyDrop, status + end + + private + + def spammy_texts + redis.smembers('antispam:spammy_texts') + end + + def report_if_needed!(account) + return if Report.unresolved.exists?(account: Account.representative, target_account: account) + + Report.create!(account: Account.representative, target_account: account, category: :spam, comment: 'Account automatically reported for posting a banned URL') + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ee6b18c74c..765c80723e 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -36,6 +36,8 @@ class PostStatusService < BaseService @text = @options[:text] || '' @in_reply_to = @options[:thread] + @antispam = Antispam.new + return idempotency_duplicate if idempotency_given? && idempotency_duplicate? validate_media! @@ -55,6 +57,8 @@ class PostStatusService < BaseService end @status + rescue Antispam::SilentlyDrop => e + e.status end private @@ -74,6 +78,7 @@ class PostStatusService < BaseService @status = @account.statuses.new(status_attributes) process_mentions_service.call(@status, save_records: false) safeguard_mentions!(@status) + @antispam.local_preflight_check!(@status) # The following transaction block is needed to wrap the UPDATEs to # the media attachments when the status is created @@ -95,6 +100,7 @@ class PostStatusService < BaseService def schedule_status! status_for_validation = @account.statuses.build(status_attributes) + @antispam.local_preflight_check!(status_for_validation) if status_for_validation.valid? # Marking the status as destroyed is necessary to prevent the status from being @@ -111,6 +117,8 @@ class PostStatusService < BaseService else raise ActiveRecord::RecordInvalid end + rescue Antispam::SilentlyDrop + @status = @account.scheduled_status.new(scheduled_status_attributes).tap(&:delete) end def postprocess_status!