diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d208395444f5e7..5f8725f6fc752d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base include DomainControlHelper include DatabaseHelper include AuthorizedFetchHelper + include SelfDestructHelper helper_method :current_account helper_method :current_session @@ -173,7 +174,7 @@ def respond_with_error(code) end def check_self_destruct! - return unless ENV.fetch('SELF_DESTRUCT', nil) && ENV['SELF_DESTRUCT'] == ENV['LOCAL_DOMAIN'] + return unless self_destruct? respond_to do |format| format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4b25b978f0fdee..5f9d7e7c484c2d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -168,10 +168,6 @@ def cdn_host? cdn_host.present? end - def self_destruct? - ENV.fetch('SELF_DESTRUCT', nil) && ENV['SELF_DESTRUCT'] == ENV['LOCAL_DOMAIN'] - end - def storage_host "https://#{storage_host_var}" end diff --git a/app/helpers/self_destruct_helper.rb b/app/helpers/self_destruct_helper.rb new file mode 100644 index 00000000000000..c77c0ccfdb9400 --- /dev/null +++ b/app/helpers/self_destruct_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SelfDestructHelper + def self.self_destruct? + ENV.fetch('SELF_DESTRUCT', nil).present? && Rails.application.message_verifier('self-destruct').verify(ENV.fetch('SELF_DESTRUCT', nil)) == ENV['LOCAL_DOMAIN'] + rescue ActiveSupport::MessageVerifier::InvalidSignature + false + end + + def self_destruct? + SelfDestructHelper.self_destruct? + end +end diff --git a/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb b/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb index b2020c7521e2a0..5c644fec7cbdfb 100644 --- a/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb +++ b/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb @@ -3,6 +3,7 @@ class Scheduler::AccountsStatusesCleanupScheduler include Sidekiq::Worker include Redisable + include SelfDestructHelper # This limit is mostly to be nice to the fediverse at large and not # generate too much traffic. @@ -35,8 +36,7 @@ class Scheduler::AccountsStatusesCleanupScheduler sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i def perform - return if ENV['SELF_DESTRUCT'] && ENV['SELF_DESTRUCT'] == ENV['LOCAL_DOMAIN'] - return if under_load? + return if self_destruct? || under_load? budget = compute_budget diff --git a/app/workers/scheduler/self_destruct_scheduler.rb b/app/workers/scheduler/self_destruct_scheduler.rb index 929edef700a579..5249650921629d 100644 --- a/app/workers/scheduler/self_destruct_scheduler.rb +++ b/app/workers/scheduler/self_destruct_scheduler.rb @@ -2,6 +2,7 @@ class Scheduler::SelfDestructScheduler include Sidekiq::Worker + include SelfDestructHelper MAX_ENQUEUED = 10_000 MAX_REDIS_MEM_USAGE = 0.5 @@ -10,7 +11,7 @@ class Scheduler::SelfDestructScheduler sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i def perform - return unless ENV['SELF_DESTRUCT'] && ENV['SELF_DESTRUCT'] == ENV['LOCAL_DOMAIN'] + return unless self_destruct? return if sidekiq_overwhelmed? delete_accounts! @@ -37,6 +38,10 @@ def delete_accounts! end end + def inboxes + @inboxes ||= Account.inboxes + end + def delete_account!(account) payload = ActiveModelSerializers::SerializableResource.new( account, diff --git a/config/navigation.rb b/config/navigation.rb index cd9a6c120a986a..1e7acf3b9c61d8 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true SimpleNavigation::Configuration.run do |navigation| - self_destruct = ENV.fetch('SELF_DESTRUCT', nil) && ENV['SELF_DESTRUCT'] == ENV['LOCAL_DOMAIN'] + self_destruct = SelfDestructHelper.self_destruct? navigation.items do |n| n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path diff --git a/lib/mastodon/cli/main.rb b/lib/mastodon/cli/main.rb index 1594eadce81e6a..5ee84657aeb762 100644 --- a/lib/mastodon/cli/main.rb +++ b/lib/mastodon/cli/main.rb @@ -65,7 +65,6 @@ class Main < Base desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' subcommand 'maintenance', Maintenance - option :dry_run, type: :boolean desc 'self-destruct', 'Erase the server from the federation' long_desc <<~LONG_DESC Erase the server from the federation by broadcasting account delete @@ -92,55 +91,37 @@ def self_destruct prompt = TTY::Prompt.new - exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain - - unless dry_run? - prompt.warn('This operation WILL NOT be reversible. It can also take a long time.') - prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.') - prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.') - - exit(1) if prompt.no?('Are you sure you want to proceed?') - end + if SelfDestructHelper.self_destruct? + prompt.ok('Self-destruct mode is already enabled for this Mastodon server') - inboxes = Account.inboxes - processed = 0 + pending_accounts = Account.local.without_suspended.count + Account.local.suspended.joins(:deletion_request).count + sidekiq_stats = Sidekiq::Stats.new - Setting.registrations_mode = 'none' unless dry_run? + if pending_accounts.positive? + prompt.warn("#{pending_accounts} accounts are still pending deletion.") + elsif sidekiq_stats.enqueued.positive? + prompt.warn('Deletion notices are still being processed') + elsif sidekiq_stats.retry_size.positive? + prompt.warn('At least one delivery attempt for each deletion notice has been made, but some have failed and are scheduled for retry') + else + prompt.ok('Every deletion notice has been sent! You can safely delete all data and decomission your servers!') + end - if inboxes.empty? - Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless dry_run? - prompt.ok('It seems like your server has not federated with anything') - prompt.ok('You can shut it down and delete it any time') - return + exit(0) end - prompt.warn('Do NOT interrupt this process...') - - delete_account = lambda do |account| - payload = ActiveModelSerializers::SerializableResource.new( - account, - serializer: ActivityPub::DeleteActorSerializer, - adapter: ActivityPub::Adapter - ).as_json - - json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account)) - - unless dry_run? - ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url| - [json, account.id, inbox_url] - end - - account.suspend!(block_email: false) - end + exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain - processed += 1 - end + prompt.warn('This operation WILL NOT be reversible. It can also take a long time.') + prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.') + prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.') - Account.local.without_suspended.find_each { |account| delete_account.call(account) } - Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) } + exit(1) if prompt.no?('Are you sure you want to proceed?') - prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run_mode_suffix}") - prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data') + self_destruct_value = Rails.application.message_verifier('self-destruct').generate(Rails.configuration.x.local_domain) + prompt.ok('To switch Mastodon to self-destruct mode, add the following line to your `.env.production` and restart all Mastodon processes:') + prompt.ok(" SELF_DESTRUCT=#{self_destruct_value}") + prompt.ok("\nYou can re-run this command to see the state of the self-destruct process.") rescue TTY::Reader::InputInterrupt exit(1) end