Skip to content

Commit

Permalink
Move more tasks to tootctl (mastodon#8675)
Browse files Browse the repository at this point in the history
* Move more tasks to tootctl

- tootctl feeds build
- tootctl feeds clear
- tootctl accounts refresh

Clean up exit codes and help messages

* Move user modifying to tootctl

* Improve user modification through CLI, rename commands

add -> create
mod -> modify
del -> delete

To remove ambiguity

* Fix code style issues

* Fix not being able to unset admin/mod role
  • Loading branch information
Gargron authored and root committed Oct 12, 2018
1 parent 47607b8 commit 5529709
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 387 deletions.
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ Rails/SkipsModelValidations:
Rails/HttpStatus:
Enabled: false

Rails/Exit:
Exclude:
- 'lib/mastodon/*'
- 'lib/cli'

Style/ClassAndModuleChildren:
Enabled: false

Expand Down
5 changes: 5 additions & 0 deletions lib/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require_relative 'mastodon/media_cli'
require_relative 'mastodon/emoji_cli'
require_relative 'mastodon/accounts_cli'
require_relative 'mastodon/feeds_cli'

module Mastodon
class CLI < Thor
desc 'media SUBCOMMAND ...ARGS', 'Manage media files'
Expand All @@ -14,5 +16,8 @@ class CLI < Thor

desc 'accounts SUBCOMMAND ...ARGS', 'Manage accounts'
subcommand 'accounts', Mastodon::AccountsCLI

desc 'feeds SUBCOMMAND ...ARGS', 'Manage feeds'
subcommand 'feeds', Mastodon::FeedsCLI
end
end
114 changes: 109 additions & 5 deletions lib/mastodon/accounts_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def rotate(username = nil)
say('OK', :green)
else
say('No account(s) given', :red)
exit(1)
end
end

Expand All @@ -48,7 +49,7 @@ def rotate(username = nil)
option :role, default: 'user'
option :reattach, type: :boolean
option :force, type: :boolean
desc 'add USERNAME', 'Create a new user'
desc 'create 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.
Expand All @@ -65,7 +66,7 @@ def rotate(username = nil)
the --force option to delete the old record and reattach the
username to the new account anyway.
LONG_DESC
def add(username)
def create(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)
Expand Down Expand Up @@ -98,19 +99,75 @@ def add(username)
say(key)
say(' ' + error, :red)
end

exit(1)
end
end

desc 'del USERNAME', 'Delete a user'
option :role
option :email
option :confirm, type: :boolean
option :enable, type: :boolean
option :disable, type: :boolean
option :disable_2fa, type: :boolean
desc 'modify USERNAME', 'Modify a user'
long_desc <<-LONG_DESC
Modify a user account.
With the --role option, update the user's role to one of "user",
"moderator" or "admin".
With the --email option, update the user's e-mail address. With
the --confirm option, mark the user's e-mail as confirmed.
With the --disable option, lock the user out of their account. The
--enable option is the opposite.
With the --disable-2fa option, the two-factor authentication
requirement for the user can be removed.
LONG_DESC
def modify(username)
user = Account.find_local(username)&.user

if user.nil?
say('No user with such username', :red)
exit(1)
end

if options[:role]
user.admin = options[:role] == 'admin'
user.moderator = options[:role] == 'moderator'
end

user.email = options[:email] if options[:email]
user.disabled = false if options[:enable]
user.disabled = true if options[:disable]
user.otp_required_for_login = false if options[:disable_2fa]
user.confirm if options[:confirm]

if user.save
say('OK', :green)
else
user.errors.to_h.each do |key, error|
say('Failure/Error: ', :red)
say(key)
say(' ' + error, :red)
end

exit(1)
end
end

desc 'delete USERNAME', 'Delete a user'
long_desc <<-LONG_DESC
Remove a user account with a given USERNAME.
LONG_DESC
def del(username)
def delete(username)
account = Account.find_local(username)

if account.nil?
say('No user with such username', :red)
return
exit(1)
end

say("Deleting user with #{account.statuses_count}, this might take a while...")
Expand Down Expand Up @@ -182,9 +239,56 @@ def cull
end
end

option :all, type: :boolean
option :domain
desc 'refresh [USERNAME]', 'Fetch remote user data and files'
long_desc <<-LONG_DESC
Fetch remote user data and files for one or multiple accounts.
With the --all option, all remote accounts will be processed.
Through the --domain option, this can be narrowed down to a
specific domain only. Otherwise, a single remote account must
be specified with USERNAME.
All processing is done in the background through Sidekiq.
LONG_DESC
def refresh(username = nil)
if options[:domain] || options[:all]
queued = 0
scope = Account.remote
scope = scope.where(domain: options[:domain]) if options[:domain]

scope.select(:id).reorder(nil).find_in_batches do |accounts|
Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts.map(&:id))
queued += accounts.size
end

say("Scheduled refreshment of #{queued} accounts", :green, true)
elsif username.present?
username, domain = username.split('@')
account = Account.find_remote(username, domain)

if account.nil?
say('No such account', :red)
exit(1)
end

Maintenance::RedownloadAccountMediaWorker.perform_async(account.id)
say('OK', :green)
else
say('No account(s) given', :red)
exit(1)
end
end

private

def rotate_keys_for_account(account, delay = 0)
if account.nil?
say('No such account', :red)
exit(1)
end

old_key = account.private_key
new_key = OpenSSL::PKey::RSA.new(2048).to_pem
account.update(private_key: new_key)
Expand Down
4 changes: 0 additions & 4 deletions lib/mastodon/emoji_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
require_relative '../../config/environment'
require_relative 'cli_helper'

# rubocop:disable Rails/Output

module Mastodon
class EmojiCLI < Thor
option :prefix
Expand Down Expand Up @@ -77,5 +75,3 @@ def color(green, _yellow, red)
end
end
end

# rubocop:enable Rails/Output
81 changes: 81 additions & 0 deletions lib/mastodon/feeds_cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

require_relative '../../config/boot'
require_relative '../../config/environment'
require_relative 'cli_helper'

module Mastodon
class FeedsCLI < Thor
option :all, type: :boolean, default: false
option :background, type: :boolean, default: false
option :dry_run, type: :boolean, default: false
option :verbose, type: :boolean, default: false
desc 'build [USERNAME]', 'Build home and list feeds for one or all users'
long_desc <<-LONG_DESC
Build home and list feeds that are stored in Redis from the database.
With the --all option, all active users will be processed.
Otherwise, a single user specified by USERNAME.
With the --background option, regeneration will be queued into Sidekiq,
and the command will exit as soon as possible.
With the --dry-run option, no work will be done.
With the --verbose option, when accounts are processed sequentially in the
foreground, the IDs of the accounts will be printed.
LONG_DESC
def build(username = nil)
dry_run = options[:dry_run] ? '(DRY RUN)' : ''

if options[:all] || username.nil?
processed = 0
queued = 0

User.active.select(:id, :account_id).reorder(nil).find_in_batches do |users|
if options[:background]
RegenerationWorker.push_bulk(users.map(&:account_id)) unless options[:dry_run]
queued += users.size
else
users.each do |user|
RegenerationWorker.new.perform(user.account_id) unless options[:dry_run]
options[:verbose] ? say(user.account_id) : say('.', :green, false)
processed += 1
end
end
end

if options[:background]
say("Scheduled feed regeneration for #{queued} accounts #{dry_run}", :green, true)
else
say
say("Regenerated feeds for #{processed} accounts #{dry_run}", :green, true)
end
elsif username.present?
account = Account.find_local(username)

if options[:background]
RegenerationWorker.perform_async(account.id) unless options[:dry_run]
else
RegenerationWorker.new.perform(account.id) unless options[:dry_run]
end

say("OK #{dry_run}", :green, true)
else
say('No account(s) given', :red)
exit(1)
end
end

desc 'clear', 'Remove all home and list feeds from Redis'
def clear
keys = Redis.current.keys('feed:*')

Redis.current.pipelined do
keys.each { |key| Redis.current.del(key) }
end

say('OK', :green)
end
end
end
13 changes: 5 additions & 8 deletions lib/mastodon/media_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
require_relative '../../config/environment'
require_relative 'cli_helper'

# rubocop:disable Rails/Output

module Mastodon
class MediaCLI < Thor
option :days, type: :numeric, default: 7
Expand All @@ -25,9 +23,10 @@ class MediaCLI < Thor
it may impact other operations of the Mastodon server, and it may overload
the underlying file storage.
With the --verbose option, output deleting file ID to console (only when --background false).
With the --dry-run option, no work will be done.
With the --dry-run option, output the number of files to delete without deleting.
With the --verbose option, when media attachments are processed sequentially in the
foreground, the IDs of the media attachments will be printed.
DESC
def remove
time_ago = options[:days].days.ago
Expand All @@ -53,12 +52,10 @@ def remove
say

if options[:background]
say("Scheduled the deletion of #{queued} media attachments #{dry_run}.", :green)
say("Scheduled the deletion of #{queued} media attachments #{dry_run}", :green, true)
else
say("Removed #{processed} media attachments #{dry_run}.", :green)
say("Removed #{processed} media attachments #{dry_run}", :green, true)
end
end
end
end

# rubocop:enable Rails/Output
Loading

0 comments on commit 5529709

Please sign in to comment.