hometown/lib/mastodon/accounts_cli.rb
Eugen Rochko 47ea318479
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
2018-09-09 13:33:36 +02:00

195 lines
6.1 KiB
Ruby

# 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