tootctl accounts [add|del|cull] (#8642)
* CLI interface for creating/deleting local users - tootctl accounts add USERNAME - tootctl accounts del USERNAME * Add CLI interface for culling remote users that no longer exist - tootctl accounts cull
This commit is contained in:
		
							parent
							
								
									2288d50a7b
								
							
						
					
					
						commit
						47ea318479
					
				
					 2 changed files with 140 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -43,6 +43,145 @@ module Mastodon
 | 
			
		|||
      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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,5 +4,6 @@ dev_null = Logger.new('/dev/null')
 | 
			
		|||
 | 
			
		||||
Rails.logger                 = dev_null
 | 
			
		||||
ActiveRecord::Base.logger    = dev_null
 | 
			
		||||
ActiveJob::Base.logger       = dev_null
 | 
			
		||||
HttpLog.configuration.logger = dev_null
 | 
			
		||||
Paperclip.options[:log]      = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue