Files
mastodon/app/services/process_moderation_lists_service.rb

178 lines
7.3 KiB
Ruby

# frozen_string_literal: true
class ProcessModerationListsService < BaseService
Suggestion = Struct.new(:target_type, :target_key, :action, :moderation_subscription_id, :apply_automatically)
Retraction = Struct.new(:target_type, :action, :moderation_subscription_id, :retract_automatically)
def call
applicable_actions = Rails.configuration.x.mastodon.limited_federation_mode ? ['accept'] : ['reject', 'limit']
@retractions = {}
ModerationSubscription.order(priority: :desc).each do |subscription|
advisories = subscription.advisories.where(action: applicable_actions).to_a
# TODO: optimize this
advisories.delete_if { |advisory| SubscribedAdvisory.superseding_advisories(advisory).exists? }
# 1. apply automatic suggestions, if possible/needed
if subscription.apply_automatically
if subscription.preserve_relationships
domains_with_follows = Instance.with_domain_follows(advisories.filter_map { |advisory| advisory.target_key if advisory.target_type == 'domain' && advisory.action == 'reject' }).pluck(:domain)
end
advisories.delete_if do |advisory|
next false if subscription.preserve_relationships && advisory.target_type == 'domain' && domains_with_follows.include?(advisory.target_key)
next false if SubscribedAdvisory.conflicting_advisories(advisory).exists?
apply_automatic_advisory!(advisory)
end
end
# 2. upsert remaining suggestions
ModerationSuggestion.upsert_all(
advisories.map do |advisory|
{
target_type: advisory.target_type,
target_key: advisory.target_key,
action: advisory.action,
moderation_subscription_id: subscription.id,
}
end,
unique_by: [:target_type, :target_key, :action]
)
# 3. orphan suggestions
grouped_target_keys = advisories.group_by { |advisory| [advisory.target_type, advisory.action] }.transform_values { |advisories| advisories.map(&:target_key) }
SubscribedAdvisory::TARGET_TYPES.each do |target_type|
SubscribedAdvisory::ACTIONS.each do |action|
subscription
.suggestions
.where(target_type: target_type)
.where(action: action)
.where.not(target_key: grouped_target_keys.fetch([target_type, action], []))
.update_all(moderation_subscription_id: nil)
end
end
# 4. retractions
collect_retractions!(subscription)
end
# Cleanup: is it really it?
ModerationSuggestion.where(moderation_subscription_id: nil).delete_all
# Handle retractions
handle_retractions!
end
private
def collect_retractions!(subscription)
DomainAllow.where(moderation_subscription_id: subscription.id).where.not(domain: subscription.advisories.domain_target_type.accept_action.pluck(:target_key)).find_each do |domain_allow|
@retractions[domain_allow.domain] = Retraction.new(
action: 'accept',
target_type: 'domain',
retract_automatically: subscription.retract_automatically,
moderation_subscription_id: subscription.id
)
end
DomainBlock.where(moderation_subscription_id: subscription.id).where.not(domain: subscription.advisories.domain_target_type.where(action: ['limit', 'reject']).pluck(:target_key)).find_each do |domain_block|
next if domain_block.noop?
action = begin
case domain_block.severity
when 'silence'
'limit'
when 'suspend'
'reject'
end
end
@retractions[domain_block.domain] = Retraction.new(
action:,
target_type: 'domain',
retract_automatically: subscription.retract_automatically,
moderation_subscription_id: subscription.id
)
end
end
def handle_retractions!
representative = Account.representative
@retractions.each do |domain, attributes|
suggestion = ModerationSuggestion.find_by(target_type: attributes[:target_type], target_key: domain, action: attributes[:action])
if suggestion
# TODO: log
domain_block = DomainBlock.find_by(domain: domain)
domain_block&.update(moderation_subscription_id: suggestion.moderation_subscription_id)
suggestion.destroy
elsif attributes[:retract_automatically]
domain_block = DomainBlock.find_by(domain: domain)
representative.action_logs.create!(action: 'destroy', target: domain_block, moderation_subscription_id: domain_block.moderation_subscription_id)
UnblockDomainService.new.call(domain_block)
else
# TODO: what about if there is another kind of suggestion for the same target?
ModerationSuggestion.upsert(
{
target_type: attributes[:target_type],
target_key: domain,
action: 'retract',
moderation_subscription_id: attributes[:moderation_subscription_id],
},
unique_by: [:target_type, :target_key, :action]
)
end
end
end
def apply_automatic_advisory!(advisory)
representative = Account.representative
case [advisory.target_type, advisory.action]
when ['domain', 'accept']
# TODO: error handling
domain_allow = DomainAllow.create!(domain: advisory.target_key, moderation_subscription_id: advisory.moderation_subscription_id)
representative.action_logs.create!(action: 'create', target: domain_allow, moderation_subscription_id: domain_allow.moderation_subscription_id)
true
when ['domain', 'reject'], ['domain', 'limit']
existing_domain_block = DomainBlock.rule_for(advisory.target_key)
domain_block = DomainBlock.new(domain: advisory.target_key, moderation_subscription_id: advisory.moderation_subscription_id, obfuscate: false, severity: advisory.action == 'reject' ? 'suspend' : 'silence')
update = false
if existing_domain_block.present?
same_domain = existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain)
# Skip if it would downgrade a wider existing block
return unless same_domain || domain_block.stricter_than?(existing_domain_block)
if same_domain
# Allow updating existing domain blocks if they are already managed by the current subscription,
# or if the subscription is configured to be able to override existing ones
return unless existing_domain_block.moderation_subscription_id == advisory.moderation_subscription_id || (existing_domain_block.moderation_subscription_id.nil? && advisory.moderation_subscription.override_unattributed?)
# Only allow downgrades if retracting automatically
return unless domain_block.stricter_than?(existing_domain_block) || advisory.moderation_subscription.retract_automatically?
# Allow transparently upgrading a domain block
existing_domain_block.assign_attributes(moderation_subscription_id: advisory.moderation_subscription_id, severity: domain_block.severity)
domain_block = existing_domain_block
update = true
end
end
# TODO: error handling
domain_block.save!
representative.action_logs.create!(action: update ? 'update' : 'create', target: domain_block, moderation_subscription_id: domain_block.moderation_subscription_id)
DomainBlockWorker.perform_async(domain_block.id)
true
else
false
end
end
end