# frozen_string_literal: true require 'rubygems/package' require_relative '../../config/boot' require_relative '../../config/environment' require_relative 'cli_helper' module Mastodon class AccountsCLI < Thor option :all, type: :boolean desc 'rotate [USERNAME]', 'Generate and broadcast new keys' long_desc <<-LONG_DESC Generate and broadcast new RSA keys as part of security maintenance. With the --all option, all local accounts will be subject to the rotation. Otherwise, and by default, only a single account specified by the USERNAME argument will be processed. LONG_DESC def rotate(username = nil) if options[:all] processed = 0 delay = 0 Account.local.without_suspended.find_in_batches do |accounts| accounts.each do |account| rotate_keys_for_account(account, delay) processed += 1 say('.', :green, false) end delay += 5.minutes end say say("OK, rotated keys for #{processed} accounts", :green) elsif username.present? rotate_keys_for_account(Account.find_local(username)) say('OK', :green) else say('No account(s) given', :red) end end option :email, required: true option :confirmed, type: :boolean option :role, default: 'user' option :reattach, type: :boolean option :force, type: :boolean desc 'add USERNAME', 'Create a new user' long_desc <<-LONG_DESC Create a new user account with a given USERNAME and an e-mail address provided with --email. With the --confirmed option, the confirmation e-mail will be skipped and the account will be active straight away. With the --role option one of "user", "admin" or "moderator" can be supplied. Defaults to "user" With the --reattach option, the new user will be reattached to a given existing username of an old account. If the old account is still in use by someone else, you can supply the --force option to delete the old record and reattach the username to the new account anyway. LONG_DESC def add(username) account = Account.new(username: username) password = SecureRandom.hex user = User.new(email: options[:email], password: password, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: Time.now.utc) if options[:reattach] account = Account.find_local(username) || Account.new(username: username) if account.user.present? && !options[:force] say('The chosen username is currently in use', :red) say('Use --force to reattach it anyway and delete the other user') return elsif account.user.present? account.user.destroy! end end user.account = account if user.save if options[:confirmed] user.confirmed_at = nil user.confirm! end say('OK', :green) say("New password: #{password}") else user.errors.to_h.each do |key, error| say('Failure/Error: ', :red) say(key) say(' ' + error, :red) end end end desc 'del USERNAME', 'Delete a user' long_desc <<-LONG_DESC Remove a user account with a given USERNAME. LONG_DESC def del(username) account = Account.find_local(username) if account.nil? say('No user with such username', :red) return end say("Deleting user with #{account.statuses_count}, this might take a while...") SuspendAccountService.new.call(account, remove_user: true) say('OK', :green) end option :dry_run, type: :boolean desc 'cull', 'Remove remote accounts that no longer exist' long_desc <<-LONG_DESC Query every single remote account in the database to determine if it still exists on the origin server, and if it doesn't, remove it from the database. Accounts that have had confirmed activity within the last week are excluded from the checks. If 10 or more accounts from the same domain cannot be queried due to a connection error (such as missing DNS records) then the domain is considered dead, and all other accounts from it are deleted without further querying. With the --dry-run option, no deletes will actually be carried out. LONG_DESC def cull domain_thresholds = Hash.new { |hash, key| hash[key] = 0 } skip_threshold = 7.days.ago culled = 0 dead_servers = [] dry_run = options[:dry_run] ? ' (DRY RUN)' : '' Account.remote.where(protocol: :activitypub).partitioned.find_each do |account| next if account.updated_at >= skip_threshold || account.last_webfingered_at >= skip_threshold unless dead_servers.include?(account.domain) begin code = Request.new(:head, account.uri).perform(&:code) rescue HTTP::ConnectionError domain_thresholds[account.domain] += 1 if domain_thresholds[account.domain] >= 10 dead_servers << account.domain end rescue StandardError next end end if [404, 410].include?(code) || dead_servers.include?(account.domain) unless options[:dry_run] SuspendAccountService.new.call(account) account.destroy end culled += 1 say('.', :green, false) else say('.', nil, false) end end say say("Removed #{culled} accounts (#{dead_servers.size} dead servers)#{dry_run}", :green) unless dead_servers.empty? say('R.I.P.:', :yellow) dead_servers.each { |domain| say(' ' + domain) } end end private def rotate_keys_for_account(account, delay = 0) old_key = account.private_key new_key = OpenSSL::PKey::RSA.new(2048).to_pem account.update(private_key: new_key) ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key) end end end