cbd0ee1d07
* Update devise-two-factor to unreleased fork for Rails 6 support Update tests to match new `rotp` version. * Update nsa gem to unreleased fork for Rails 6 support * Update rails to 6.1.3 and rails-i18n to 6.0 * Update to unreleased fork of pluck_each for Ruby 6 support * Run "rails app:update" * Add missing ActiveStorage config file * Use config.ssl_options instead of removed ApplicationController#force_ssl Disabled force_ssl-related tests as they do not seem to be easily testable anymore. * Fix nonce directives by removing Rails 5 specific monkey-patching * Fix fixture_file_upload deprecation warning * Fix yield-based test failing with Rails 6 * Use Rails 6's index_with when possible * Use ActiveRecord::Cache::Store#delete_multi from Rails 6 This will yield better performances when deleting an account * Disable Rails 6.1's automatic preload link headers Since Rails 6.1, ActionView adds preload links for javascript files in the Links header per default. In our case, that will bloat headers too much and potentially cause issues with reverse proxies. Furhermore, we don't need those links, as we already output them as HTML link tags. * Switch to Rails 6.0 default config * Switch to Rails 6.1 default config * Do not include autoload paths in the load path
306 lines
8.8 KiB
Ruby
306 lines
8.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class DeleteAccountService < BaseService
|
|
include Payloadable
|
|
|
|
ASSOCIATIONS_ON_SUSPEND = %w(
|
|
account_pins
|
|
active_relationships
|
|
aliases
|
|
block_relationships
|
|
blocked_by_relationships
|
|
conversation_mutes
|
|
conversations
|
|
custom_filters
|
|
devices
|
|
domain_blocks
|
|
featured_tags
|
|
follow_requests
|
|
identity_proofs
|
|
list_accounts
|
|
migrations
|
|
mute_relationships
|
|
muted_by_relationships
|
|
notifications
|
|
owned_lists
|
|
passive_relationships
|
|
report_notes
|
|
scheduled_statuses
|
|
status_pins
|
|
).freeze
|
|
|
|
# The following associations have no important side-effects
|
|
# in callbacks and all of their own associations are secured
|
|
# by foreign keys, making them safe to delete without loading
|
|
# into memory
|
|
ASSOCIATIONS_WITHOUT_SIDE_EFFECTS = %w(
|
|
account_pins
|
|
aliases
|
|
conversation_mutes
|
|
conversations
|
|
custom_filters
|
|
devices
|
|
domain_blocks
|
|
featured_tags
|
|
follow_requests
|
|
identity_proofs
|
|
list_accounts
|
|
migrations
|
|
mute_relationships
|
|
muted_by_relationships
|
|
notifications
|
|
owned_lists
|
|
scheduled_statuses
|
|
status_pins
|
|
)
|
|
|
|
ASSOCIATIONS_ON_DESTROY = %w(
|
|
reports
|
|
targeted_moderation_notes
|
|
targeted_reports
|
|
).freeze
|
|
|
|
# Suspend or remove an account and remove as much of its data
|
|
# as possible. If it's a local account and it has not been confirmed
|
|
# or never been approved, then side effects are skipped and both
|
|
# the user and account records are removed fully. Otherwise,
|
|
# it is controlled by options.
|
|
# @param [Account]
|
|
# @param [Hash] options
|
|
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
|
|
# @option [Boolean] :reserve_username Keep account record
|
|
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
|
|
# @option [Boolean] :skip_activitypub Skip sending ActivityPub payloads. Implied by :skip_side_effects
|
|
# @option [Time] :suspended_at Only applicable when :reserve_username is true
|
|
def call(account, **options)
|
|
@account = account
|
|
@options = { reserve_username: true, reserve_email: true }.merge(options)
|
|
|
|
if @account.local? && @account.user_unconfirmed_or_pending?
|
|
@options[:reserve_email] = false
|
|
@options[:reserve_username] = false
|
|
@options[:skip_side_effects] = true
|
|
end
|
|
|
|
@options[:skip_activitypub] = true if @options[:skip_side_effects]
|
|
|
|
distribute_activities!
|
|
purge_content!
|
|
fulfill_deletion_request!
|
|
end
|
|
|
|
private
|
|
|
|
def distribute_activities!
|
|
return if skip_activitypub?
|
|
|
|
if @account.local?
|
|
delete_actor!
|
|
elsif @account.activitypub?
|
|
reject_follows!
|
|
undo_follows!
|
|
end
|
|
end
|
|
|
|
def reject_follows!
|
|
# When deleting a remote account, the account obviously doesn't
|
|
# actually become deleted on its origin server, i.e. unlike a
|
|
# locally deleted account it continues to have access to its home
|
|
# feed and other content. To prevent it from being able to continue
|
|
# to access toots it would receive because it follows local accounts,
|
|
# we have to force it to unfollow them.
|
|
|
|
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
|
[Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
|
|
end
|
|
end
|
|
|
|
def undo_follows!
|
|
# When deleting a remote account, the account obviously doesn't
|
|
# actually become deleted on its origin server, but following relationships
|
|
# are severed on our end. Therefore, make the remote server aware that the
|
|
# follow relationships are severed to avoid confusion and potential issues
|
|
# if the remote account gets un-suspended.
|
|
|
|
ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow|
|
|
[Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url]
|
|
end
|
|
end
|
|
|
|
def purge_user!
|
|
return if !@account.local? || @account.user.nil?
|
|
|
|
if keep_user_record?
|
|
@account.user.disable!
|
|
@account.user.invites.where(uses: 0).destroy_all
|
|
else
|
|
@account.user.destroy
|
|
end
|
|
end
|
|
|
|
def purge_content!
|
|
purge_user!
|
|
purge_profile!
|
|
purge_statuses!
|
|
purge_mentions!
|
|
purge_media_attachments!
|
|
purge_polls!
|
|
purge_generated_notifications!
|
|
purge_favourites!
|
|
purge_bookmarks!
|
|
purge_feeds!
|
|
purge_other_associations!
|
|
|
|
@account.destroy unless keep_account_record?
|
|
end
|
|
|
|
def purge_statuses!
|
|
@account.statuses.reorder(nil).where.not(id: reported_status_ids).in_batches do |statuses|
|
|
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: skip_side_effects?)
|
|
end
|
|
end
|
|
|
|
def purge_mentions!
|
|
@account.mentions.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
|
|
end
|
|
|
|
def purge_media_attachments!
|
|
@account.media_attachments.reorder(nil).find_each do |media_attachment|
|
|
next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id)
|
|
|
|
media_attachment.destroy
|
|
end
|
|
end
|
|
|
|
def purge_polls!
|
|
@account.polls.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
|
|
end
|
|
|
|
def purge_generated_notifications!
|
|
# By deleting polls and statuses without callbacks, we've left behind
|
|
# polymorphically associated notifications generated by this account
|
|
|
|
Notification.where(from_account: @account).in_batches.delete_all
|
|
end
|
|
|
|
def purge_favourites!
|
|
@account.favourites.in_batches do |favourites|
|
|
ids = favourites.pluck(:status_id)
|
|
StatusStat.where(status_id: ids).update_all('favourites_count = GREATEST(0, favourites_count - 1)')
|
|
Chewy.strategy.current.update(StatusesIndex::Status, ids) if Chewy.enabled?
|
|
Rails.cache.delete_multi(ids.map { |id| "statuses/#{id}" })
|
|
favourites.delete_all
|
|
end
|
|
end
|
|
|
|
def purge_bookmarks!
|
|
@account.bookmarks.in_batches do |bookmarks|
|
|
Chewy.strategy.current.update(StatusesIndex::Status, bookmarks.pluck(:status_id)) if Chewy.enabled?
|
|
bookmarks.delete_all
|
|
end
|
|
end
|
|
|
|
def purge_other_associations!
|
|
associations_for_destruction.each do |association_name|
|
|
purge_association(association_name)
|
|
end
|
|
end
|
|
|
|
def purge_feeds!
|
|
return unless @account.local?
|
|
|
|
FeedManager.instance.clean_feeds!(:home, [@account.id])
|
|
FeedManager.instance.clean_feeds!(:list, @account.owned_lists.pluck(:id))
|
|
end
|
|
|
|
def purge_profile!
|
|
# If the account is going to be destroyed
|
|
# there is no point wasting time updating
|
|
# its values first
|
|
|
|
return unless keep_account_record?
|
|
|
|
@account.silenced_at = nil
|
|
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
|
@account.suspension_origin = :local
|
|
@account.locked = false
|
|
@account.memorial = false
|
|
@account.discoverable = false
|
|
@account.display_name = ''
|
|
@account.note = ''
|
|
@account.fields = []
|
|
@account.statuses_count = 0
|
|
@account.followers_count = 0
|
|
@account.following_count = 0
|
|
@account.moved_to_account = nil
|
|
@account.also_known_as = []
|
|
@account.trust_level = :untrusted
|
|
@account.avatar.destroy
|
|
@account.header.destroy
|
|
@account.save!
|
|
end
|
|
|
|
def fulfill_deletion_request!
|
|
@account.deletion_request&.destroy
|
|
end
|
|
|
|
def purge_association(association_name)
|
|
association = @account.public_send(association_name)
|
|
|
|
if ASSOCIATIONS_WITHOUT_SIDE_EFFECTS.include?(association_name)
|
|
association.in_batches.delete_all
|
|
else
|
|
association.in_batches.destroy_all
|
|
end
|
|
end
|
|
|
|
def delete_actor!
|
|
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
|
[delete_actor_json, @account.id, inbox_url]
|
|
end
|
|
|
|
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
|
|
[delete_actor_json, @account.id, inbox_url]
|
|
end
|
|
end
|
|
|
|
def delete_actor_json
|
|
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
|
|
end
|
|
|
|
def delivery_inboxes
|
|
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
|
|
end
|
|
|
|
def low_priority_delivery_inboxes
|
|
Account.inboxes - delivery_inboxes
|
|
end
|
|
|
|
def reported_status_ids
|
|
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
|
|
end
|
|
|
|
def associations_for_destruction
|
|
if keep_account_record?
|
|
ASSOCIATIONS_ON_SUSPEND
|
|
else
|
|
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
|
|
end
|
|
end
|
|
|
|
def keep_user_record?
|
|
@options[:reserve_email]
|
|
end
|
|
|
|
def keep_account_record?
|
|
@options[:reserve_username]
|
|
end
|
|
|
|
def skip_side_effects?
|
|
@options[:skip_side_effects]
|
|
end
|
|
|
|
def skip_activitypub?
|
|
@options[:skip_activitypub]
|
|
end
|
|
end
|