forked from fedi/mastodon
Add tootctl domains purge
options to select subdomains and keep domain blocks (#22063)
* Add --include-subdomains option to tootctl domains purge * Add support for '*.' subdomain wildcard patterns in `tootctl domains purge` * Fix custom emojis deletion not following subdomain and URI options * Change `tootctl domains purge` to not purge domain blocks unless --purge-domain-blocks is passed * Refactor `tootctl domains purge` * Add feedback on deleted domain blocks
This commit is contained in:
parent
68dcbcb7bf
commit
cb4e28f405
|
@ -18,6 +18,8 @@ module Mastodon
|
||||||
option :dry_run, type: :boolean
|
option :dry_run, type: :boolean
|
||||||
option :limited_federation_mode, type: :boolean
|
option :limited_federation_mode, type: :boolean
|
||||||
option :by_uri, type: :boolean
|
option :by_uri, type: :boolean
|
||||||
|
option :include_subdomains, type: :boolean
|
||||||
|
option :purge_domain_blocks, type: :boolean
|
||||||
desc 'purge [DOMAIN...]', 'Remove accounts from a DOMAIN without a trace'
|
desc 'purge [DOMAIN...]', 'Remove accounts from a DOMAIN without a trace'
|
||||||
long_desc <<-LONG_DESC
|
long_desc <<-LONG_DESC
|
||||||
Remove all accounts from a given DOMAIN without leaving behind any
|
Remove all accounts from a given DOMAIN without leaving behind any
|
||||||
|
@ -33,40 +35,75 @@ module Mastodon
|
||||||
that has the handle `foo@bar.com` but whose profile is at the URL
|
that has the handle `foo@bar.com` but whose profile is at the URL
|
||||||
`https://mastodon-bar.com/users/foo`, would be purged by either
|
`https://mastodon-bar.com/users/foo`, would be purged by either
|
||||||
`tootctl domains purge bar.com` or `tootctl domains purge --by-uri mastodon-bar.com`.
|
`tootctl domains purge bar.com` or `tootctl domains purge --by-uri mastodon-bar.com`.
|
||||||
|
|
||||||
|
When the --include-subdomains option is given, not only DOMAIN is deleted, but all
|
||||||
|
subdomains as well. Note that this may be considerably slower.
|
||||||
|
|
||||||
|
When the --purge-domain-blocks option is given, also purge matching domain blocks.
|
||||||
LONG_DESC
|
LONG_DESC
|
||||||
def purge(*domains)
|
def purge(*domains)
|
||||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||||
|
domains = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
|
||||||
|
account_scope = Account.none
|
||||||
|
domain_block_scope = DomainBlock.none
|
||||||
|
emoji_scope = CustomEmoji.none
|
||||||
|
|
||||||
scope = begin
|
# Sanity check on command arguments
|
||||||
if options[:limited_federation_mode]
|
if options[:limited_federation_mode] && !domains.empty?
|
||||||
Account.remote.where.not(domain: DomainAllow.pluck(:domain))
|
say('DOMAIN parameter not supported with --limited-federation-mode', :red)
|
||||||
elsif !domains.empty?
|
exit(1)
|
||||||
if options[:by_uri]
|
elsif domains.empty? && !options[:limited_federation_mode]
|
||||||
domains.map { |domain| Account.remote.where(Account.arel_table[:uri].matches("https://#{domain}/%", false, true)) }.reduce(:or)
|
say('No domain(s) given', :red)
|
||||||
else
|
exit(1)
|
||||||
Account.remote.where(domain: domains)
|
end
|
||||||
end
|
|
||||||
|
# Build scopes from command arguments
|
||||||
|
if options[:limited_federation_mode]
|
||||||
|
account_scope = Account.remote.where.not(domain: DomainAllow.select(:domain))
|
||||||
|
emoji_scope = CustomEmoji.remote.where.not(domain: DomainAllow.select(:domain))
|
||||||
|
else
|
||||||
|
# Handle wildcard subdomains
|
||||||
|
subdomain_patterns = domains.filter_map { |domain| "%.#{Account.sanitize_sql_like(domain[2..])}" if domain.start_with?('*.') }
|
||||||
|
domains = domains.filter { |domain| !domain.start_with?('*.') }
|
||||||
|
# Handle --include-subdomains
|
||||||
|
subdomain_patterns += domains.map { |domain| "%.#{Account.sanitize_sql_like(domain)}" } if options[:include_subdomains]
|
||||||
|
uri_patterns = (domains.map { |domain| Account.sanitize_sql_like(domain) } + subdomain_patterns).map { |pattern| "https://#{pattern}/%" }
|
||||||
|
|
||||||
|
if options[:purge_domain_blocks]
|
||||||
|
domain_block_scope = DomainBlock.where(domain: domains)
|
||||||
|
domain_block_scope = domain_block_scope.or(DomainBlock.where(DomainBlock.arel_table[:domain].matches_any(subdomain_patterns))) unless subdomain_patterns.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:by_uri]
|
||||||
|
account_scope = Account.remote.where(Account.arel_table[:uri].matches_any(uri_patterns, false, true))
|
||||||
|
emoji_scope = CustomEmoji.remote.where(CustomEmoji.arel_table[:uri].matches_any(uri_patterns, false, true))
|
||||||
else
|
else
|
||||||
say('No domain(s) given', :red)
|
account_scope = Account.remote.where(domain: domains)
|
||||||
exit(1)
|
account_scope = account_scope.or(Account.remote.where(Account.arel_table[:domain].matches_any(subdomain_patterns))) unless subdomain_patterns.empty?
|
||||||
|
emoji_scope = CustomEmoji.where(domain: domains)
|
||||||
|
emoji_scope = emoji_scope.or(CustomEmoji.remote.where(CustomEmoji.arel_table[:uri].matches_any(subdomain_patterns))) unless subdomain_patterns.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
processed, = parallelize_with_progress(scope) do |account|
|
# Actually perform the deletions
|
||||||
|
processed, = parallelize_with_progress(account_scope) do |account|
|
||||||
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
|
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
|
||||||
end
|
end
|
||||||
|
|
||||||
DomainBlock.where(domain: domains).destroy_all unless options[:dry_run]
|
|
||||||
|
|
||||||
say("Removed #{processed} accounts#{dry_run}", :green)
|
say("Removed #{processed} accounts#{dry_run}", :green)
|
||||||
|
|
||||||
custom_emojis = CustomEmoji.where(domain: domains)
|
if options[:purge_domain_blocks]
|
||||||
custom_emojis_count = custom_emojis.count
|
domain_block_count = domain_block_scope.count
|
||||||
custom_emojis.destroy_all unless options[:dry_run]
|
domain_block_scope.in_batches.destroy_all unless options[:dry_run]
|
||||||
|
say("Removed #{domain_block_count} domain blocks#{dry_run}", :green)
|
||||||
|
end
|
||||||
|
|
||||||
|
custom_emojis_count = emoji_scope.count
|
||||||
|
emoji_scope.in_batches.destroy_all unless options[:dry_run]
|
||||||
|
|
||||||
Instance.refresh unless options[:dry_run]
|
Instance.refresh unless options[:dry_run]
|
||||||
|
|
||||||
say("Removed #{custom_emojis_count} custom emojis", :green)
|
say("Removed #{custom_emojis_count} custom emojis#{dry_run}", :green)
|
||||||
end
|
end
|
||||||
|
|
||||||
option :concurrency, type: :numeric, default: 50, aliases: [:c]
|
option :concurrency, type: :numeric, default: 50, aliases: [:c]
|
||||||
|
|
Loading…
Reference in a new issue