Skip to content

Commit e52fa0e

Browse files
Gargronhiyuki2578
authored andcommitted
Improve account suspension speed and completeness (mastodon#9290)
- Some associations were missing from the clean-up - Some attributes were not reset on suspension - Skip federation and streaming deletes when purging a dead domain - Move account association definitions to concern
1 parent f709162 commit e52fa0e

6 files changed

Lines changed: 153 additions & 76 deletions

File tree

app/models/account.rb

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Account < ApplicationRecord
5050
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
5151
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
5252

53+
include AccountAssociations
5354
include AccountAvatar
5455
include AccountFinderConcern
5556
include AccountHeader
@@ -60,9 +61,6 @@ class Account < ApplicationRecord
6061

6162
enum protocol: [:ostatus, :activitypub]
6263

63-
# Local users
64-
has_one :user, inverse_of: :account
65-
6664
validates :username, presence: true
6765

6866
# Remote user validations
@@ -77,45 +75,6 @@ class Account < ApplicationRecord
7775
validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
7876
validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
7977

80-
# Timelines
81-
has_many :stream_entries, inverse_of: :account, dependent: :destroy
82-
has_many :statuses, inverse_of: :account, dependent: :destroy
83-
has_many :favourites, inverse_of: :account, dependent: :destroy
84-
has_many :mentions, inverse_of: :account, dependent: :destroy
85-
has_many :notifications, inverse_of: :account, dependent: :destroy
86-
87-
# Pinned statuses
88-
has_many :status_pins, inverse_of: :account, dependent: :destroy
89-
has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status
90-
91-
# Endorsements
92-
has_many :account_pins, inverse_of: :account, dependent: :destroy
93-
has_many :endorsed_accounts, through: :account_pins, class_name: 'Account', source: :target_account
94-
95-
# Media
96-
has_many :media_attachments, dependent: :destroy
97-
98-
# PuSH subscriptions
99-
has_many :subscriptions, dependent: :destroy
100-
101-
# Report relationships
102-
has_many :reports
103-
has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id
104-
105-
has_many :report_notes, dependent: :destroy
106-
has_many :custom_filters, inverse_of: :account, dependent: :destroy
107-
108-
# Moderation notes
109-
has_many :account_moderation_notes, dependent: :destroy
110-
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy
111-
112-
# Lists
113-
has_many :list_accounts, inverse_of: :account, dependent: :destroy
114-
has_many :lists, through: :list_accounts
115-
116-
# Account migrations
117-
belongs_to :moved_to_account, class_name: 'Account', optional: true
118-
11978
scope :remote, -> { where.not(domain: nil) }
12079
scope :local, -> { where(domain: nil) }
12180
scope :expiring, ->(time) { remote.where.not(subscription_expires_at: nil).where('subscription_expires_at < ?', time) }
@@ -453,6 +412,7 @@ def emojis
453412
before_create :generate_keys
454413
before_validation :normalize_domain
455414
before_validation :prepare_contents, if: :local?
415+
before_destroy :clean_feed_manager
456416

457417
private
458418

@@ -478,4 +438,19 @@ def normalize_domain
478438
def emojifiable_text
479439
[note, display_name, fields.map(&:value)].join(' ')
480440
end
441+
442+
def clean_feed_manager
443+
reblog_key = FeedManager.instance.key(:home, id, 'reblogs')
444+
reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
445+
446+
Redis.current.pipelined do
447+
Redis.current.del(FeedManager.instance.key(:home, id))
448+
Redis.current.del(reblog_key)
449+
450+
reblogged_id_set.each do |reblogged_id|
451+
reblog_set_key = FeedManager.instance.key(:home, id, "reblogs:#{reblogged_id}")
452+
Redis.current.del(reblog_set_key)
453+
end
454+
end
455+
end
481456
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module AccountAssociations
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
# Local users
8+
has_one :user, inverse_of: :account, dependent: :destroy
9+
10+
# Timelines
11+
has_many :stream_entries, inverse_of: :account, dependent: :destroy
12+
has_many :statuses, inverse_of: :account, dependent: :destroy
13+
has_many :favourites, inverse_of: :account, dependent: :destroy
14+
has_many :mentions, inverse_of: :account, dependent: :destroy
15+
has_many :notifications, inverse_of: :account, dependent: :destroy
16+
has_many :conversations, class_name: 'AccountConversation', dependent: :destroy, inverse_of: :account
17+
18+
# Pinned statuses
19+
has_many :status_pins, inverse_of: :account, dependent: :destroy
20+
has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status
21+
22+
# Endorsements
23+
has_many :account_pins, inverse_of: :account, dependent: :destroy
24+
has_many :endorsed_accounts, through: :account_pins, class_name: 'Account', source: :target_account
25+
26+
# Media
27+
has_many :media_attachments, dependent: :destroy
28+
29+
# PuSH subscriptions
30+
has_many :subscriptions, dependent: :destroy
31+
32+
# Report relationships
33+
has_many :reports, dependent: :destroy, inverse_of: :account
34+
has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
35+
36+
has_many :report_notes, dependent: :destroy
37+
has_many :custom_filters, inverse_of: :account, dependent: :destroy
38+
39+
# Moderation notes
40+
has_many :account_moderation_notes, dependent: :destroy, inverse_of: :account
41+
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
42+
43+
# Lists (that the account is on, not owned by the account)
44+
has_many :list_accounts, inverse_of: :account, dependent: :destroy
45+
has_many :lists, through: :list_accounts
46+
47+
# Lists (owned by the account)
48+
has_many :owned_lists, class_name: 'List', dependent: :destroy, inverse_of: :account
49+
50+
# Account migrations
51+
belongs_to :moved_to_account, class_name: 'Account', optional: true
52+
end
53+
end

app/services/batched_remove_status_service.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ class BatchedRemoveStatusService < BaseService
99
# Remove statuses from home feeds
1010
# Push delete events to streaming API for home feeds and public feeds
1111
# @param [Status] statuses A preferably batched array of statuses
12-
def call(statuses)
12+
# @param [Hash] options
13+
# @option [Boolean] :skip_side_effects
14+
def call(statuses, **options)
1315
statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }
1416

1517
@mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a }
@@ -26,6 +28,8 @@ def call(statuses)
2628
status.destroy
2729
end
2830

31+
return if options[:skip_side_effects]
32+
2933
# Batch by source account
3034
statuses.group_by(&:account_id).each_value do |account_statuses|
3135
account = account_statuses.first.account
Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,108 @@
11
# frozen_string_literal: true
22

33
class SuspendAccountService < BaseService
4+
ASSOCIATIONS_ON_SUSPEND = %w(
5+
account_pins
6+
active_relationships
7+
block_relationships
8+
blocked_by_relationships
9+
conversation_mutes
10+
conversations
11+
custom_filters
12+
domain_blocks
13+
favourites
14+
follow_requests
15+
list_accounts
16+
media_attachments
17+
mute_relationships
18+
muted_by_relationships
19+
notifications
20+
owned_lists
21+
passive_relationships
22+
report_notes
23+
status_pins
24+
stream_entries
25+
subscriptions
26+
).freeze
27+
28+
ASSOCIATIONS_ON_DESTROY = %w(
29+
reports
30+
targeted_moderation_notes
31+
targeted_reports
32+
).freeze
33+
34+
# Suspend an account and remove as much of its data as possible
35+
# @param [Account]
36+
# @param [Hash] options
37+
# @option [Boolean] :including_user Remove the user record as well
38+
# @option [Boolean] :destroy Remove the account record instead of suspending
439
def call(account, **options)
540
@account = account
641
@options = options
742

843
purge_user!
944
purge_profile!
1045
purge_content!
11-
unsubscribe_push_subscribers!
1246
end
1347

1448
private
1549

1650
def purge_user!
17-
if @options[:remove_user]
18-
@account.user&.destroy
51+
return if !@account.local? || @account.user.nil?
52+
53+
if @options[:including_user]
54+
@account.user.destroy
1955
else
20-
@account.user&.disable!
56+
@account.user.disable!
2157
end
2258
end
2359

2460
def purge_content!
25-
if @account.local?
26-
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
27-
[delete_actor_json, @account.id, inbox_url]
28-
end
29-
end
61+
distribute_delete_actor! if @account.local?
3062

3163
@account.statuses.reorder(nil).find_in_batches do |statuses|
32-
BatchedRemoveStatusService.new.call(statuses)
64+
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:destroy])
3365
end
3466

35-
[
36-
@account.media_attachments,
37-
@account.stream_entries,
38-
@account.notifications,
39-
@account.favourites,
40-
@account.active_relationships,
41-
@account.passive_relationships,
42-
].each do |association|
43-
destroy_all(association)
67+
associations_for_destruction.each do |association_name|
68+
destroy_all(@account.public_send(association_name))
4469
end
70+
71+
@account.destroy if @options[:destroy]
4572
end
4673

4774
def purge_profile!
48-
@account.suspended = true
49-
@account.display_name = ''
50-
@account.note = ''
51-
@account.statuses_count = 0
75+
# If the account is going to be destroyed
76+
# there is no point wasting time updating
77+
# its values first
78+
79+
return if @options[:destroy]
80+
81+
@account.silenced = false
82+
@account.suspended = true
83+
@account.locked = false
84+
@account.display_name = ''
85+
@account.note = ''
86+
@account.fields = {}
87+
@account.statuses_count = 0
88+
@account.followers_count = 0
89+
@account.following_count = 0
90+
@account.moved_to_account = nil
5291
@account.avatar.destroy
5392
@account.header.destroy
5493
@account.save!
5594
end
5695

57-
def unsubscribe_push_subscribers!
58-
destroy_all(@account.subscriptions)
59-
end
60-
6196
def destroy_all(association)
6297
association.in_batches.destroy_all
6398
end
6499

100+
def distribute_delete_actor!
101+
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
102+
[delete_actor_json, @account.id, inbox_url]
103+
end
104+
end
105+
65106
def delete_actor_json
66107
return @delete_actor_json if defined?(@delete_actor_json)
67108

@@ -77,4 +118,12 @@ def delete_actor_json
77118
def delivery_inboxes
78119
Account.inboxes + Relay.enabled.pluck(:inbox_url)
79120
end
121+
122+
def associations_for_destruction
123+
if @options[:destroy]
124+
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
125+
else
126+
ASSOCIATIONS_ON_SUSPEND
127+
end
128+
end
80129
end

app/workers/admin/suspension_worker.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ class Admin::SuspensionWorker
66
sidekiq_options queue: 'pull'
77

88
def perform(account_id, remove_user = false)
9-
SuspendAccountService.new.call(Account.find(account_id), remove_user: remove_user)
9+
SuspendAccountService.new.call(Account.find(account_id), including_user: remove_user)
1010
end
1111
end

lib/mastodon/domains_cli.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ def purge(domain)
2222
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
2323

2424
Account.where(domain: domain).find_each do |account|
25-
unless options[:dry_run]
26-
SuspendAccountService.new.call(account)
27-
account.destroy
28-
end
29-
25+
SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run]
3026
removed += 1
3127
say('.', :green, false)
3228
end

0 commit comments

Comments
 (0)