1
0
Fork 1
forked from fedi/mastodon

Move more tasks to tootctl (#8675)

* 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
This commit is contained in:
Eugen Rochko 2018-09-14 17:42:22 +02:00 committed by GitHub
parent 1d8b693d31
commit 6a3f9b7e53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 205 additions and 387 deletions

View file

@ -77,6 +77,11 @@ Rails/SkipsModelValidations:
Rails/HttpStatus: Rails/HttpStatus:
Enabled: false Enabled: false
Rails/Exit:
Exclude:
- 'lib/mastodon/*'
- 'lib/cli'
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false

View file

@ -4,6 +4,8 @@ require 'thor'
require_relative 'mastodon/media_cli' require_relative 'mastodon/media_cli'
require_relative 'mastodon/emoji_cli' require_relative 'mastodon/emoji_cli'
require_relative 'mastodon/accounts_cli' require_relative 'mastodon/accounts_cli'
require_relative 'mastodon/feeds_cli'
module Mastodon module Mastodon
class CLI < Thor class CLI < Thor
desc 'media SUBCOMMAND ...ARGS', 'Manage media files' desc 'media SUBCOMMAND ...ARGS', 'Manage media files'
@ -14,5 +16,8 @@ module Mastodon
desc 'accounts SUBCOMMAND ...ARGS', 'Manage accounts' desc 'accounts SUBCOMMAND ...ARGS', 'Manage accounts'
subcommand 'accounts', Mastodon::AccountsCLI subcommand 'accounts', Mastodon::AccountsCLI
desc 'feeds SUBCOMMAND ...ARGS', 'Manage feeds'
subcommand 'feeds', Mastodon::FeedsCLI
end end
end end

View file

@ -40,6 +40,7 @@ module Mastodon
say('OK', :green) say('OK', :green)
else else
say('No account(s) given', :red) say('No account(s) given', :red)
exit(1)
end end
end end
@ -48,7 +49,7 @@ module Mastodon
option :role, default: 'user' option :role, default: 'user'
option :reattach, type: :boolean option :reattach, type: :boolean
option :force, type: :boolean option :force, type: :boolean
desc 'add USERNAME', 'Create a new user' desc 'create USERNAME', 'Create a new user'
long_desc <<-LONG_DESC long_desc <<-LONG_DESC
Create a new user account with a given USERNAME and an Create a new user account with a given USERNAME and an
e-mail address provided with --email. e-mail address provided with --email.
@ -65,7 +66,7 @@ module Mastodon
the --force option to delete the old record and reattach the the --force option to delete the old record and reattach the
username to the new account anyway. username to the new account anyway.
LONG_DESC LONG_DESC
def add(username) def create(username)
account = Account.new(username: username) account = Account.new(username: username)
password = SecureRandom.hex password = SecureRandom.hex
user = User.new(email: options[:email], password: password, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: Time.now.utc) user = User.new(email: options[:email], password: password, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: Time.now.utc)
@ -98,19 +99,75 @@ module Mastodon
say(key) say(key)
say(' ' + error, :red) say(' ' + error, :red)
end end
exit(1)
end end
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 long_desc <<-LONG_DESC
Remove a user account with a given USERNAME. Remove a user account with a given USERNAME.
LONG_DESC LONG_DESC
def del(username) def delete(username)
account = Account.find_local(username) account = Account.find_local(username)
if account.nil? if account.nil?
say('No user with such username', :red) say('No user with such username', :red)
return exit(1)
end end
say("Deleting user with #{account.statuses_count}, this might take a while...") say("Deleting user with #{account.statuses_count}, this might take a while...")
@ -182,9 +239,56 @@ module Mastodon
end end
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 private
def rotate_keys_for_account(account, delay = 0) def rotate_keys_for_account(account, delay = 0)
if account.nil?
say('No such account', :red)
exit(1)
end
old_key = account.private_key old_key = account.private_key
new_key = OpenSSL::PKey::RSA.new(2048).to_pem new_key = OpenSSL::PKey::RSA.new(2048).to_pem
account.update(private_key: new_key) account.update(private_key: new_key)

View file

@ -5,8 +5,6 @@ require_relative '../../config/boot'
require_relative '../../config/environment' require_relative '../../config/environment'
require_relative 'cli_helper' require_relative 'cli_helper'
# rubocop:disable Rails/Output
module Mastodon module Mastodon
class EmojiCLI < Thor class EmojiCLI < Thor
option :prefix option :prefix
@ -77,5 +75,3 @@ module Mastodon
end end
end end
end end
# rubocop:enable Rails/Output

81
lib/mastodon/feeds_cli.rb Normal file
View file

@ -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

View file

@ -4,8 +4,6 @@ require_relative '../../config/boot'
require_relative '../../config/environment' require_relative '../../config/environment'
require_relative 'cli_helper' require_relative 'cli_helper'
# rubocop:disable Rails/Output
module Mastodon module Mastodon
class MediaCLI < Thor class MediaCLI < Thor
option :days, type: :numeric, default: 7 option :days, type: :numeric, default: 7
@ -25,9 +23,10 @@ module Mastodon
it may impact other operations of the Mastodon server, and it may overload it may impact other operations of the Mastodon server, and it may overload
the underlying file storage. 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 DESC
def remove def remove
time_ago = options[:days].days.ago time_ago = options[:days].days.ago
@ -53,12 +52,10 @@ module Mastodon
say say
if options[:background] 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 else
say("Removed #{processed} media attachments #{dry_run}.", :green) say("Removed #{processed} media attachments #{dry_run}", :green, true)
end end
end end
end end
end end
# rubocop:enable Rails/Output

View file

@ -390,139 +390,6 @@ namespace :mastodon do
end end
end end
desc 'Turn a user into an admin, identified by the USERNAME environment variable'
task make_admin: :environment do
include RoutingHelper
account_username = ENV.fetch('USERNAME')
user = User.joins(:account).where(accounts: { username: account_username })
if user.present?
user.update(admin: true)
puts "Congrats! #{account_username} is now an admin. \\o/\nNavigate to #{edit_admin_settings_url} to get started"
else
puts "User could not be found; please make sure an account with the `#{account_username}` username exists."
end
end
desc 'Turn a user into a moderator, identified by the USERNAME environment variable'
task make_mod: :environment do
account_username = ENV.fetch('USERNAME')
user = User.joins(:account).where(accounts: { username: account_username })
if user.present?
user.update(moderator: true)
puts "Congrats! #{account_username} is now a moderator \\o/"
else
puts "User could not be found; please make sure an account with the `#{account_username}` username exists."
end
end
desc 'Remove admin and moderator privileges from user identified by the USERNAME environment variable'
task revoke_staff: :environment do
account_username = ENV.fetch('USERNAME')
user = User.joins(:account).where(accounts: { username: account_username })
if user.present?
user.update(moderator: false, admin: false)
puts "#{account_username} is no longer admin or moderator."
else
puts "User could not be found; please make sure an account with the `#{account_username}` username exists."
end
end
desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.'
task confirm_email: :environment do
email = ENV.fetch('USER_EMAIL')
user = User.find_by(email: email)
if user
user.update(confirmed_at: Time.now.utc)
puts "#{email} confirmed"
else
abort "#{email} not found"
end
end
desc 'Add a user by providing their email, username and initial password.' \
'The user will receive a confirmation email, then they must reset their password before logging in.'
task add_user: :environment do
disable_log_stdout!
prompt = TTY::Prompt.new
begin
email = prompt.ask('E-mail:', required: true) do |q|
q.modify :strip
end
username = prompt.ask('Username:', required: true) do |q|
q.modify :strip
end
role = prompt.select('Role:', %w(user moderator admin))
if prompt.yes?('Proceed to create the user?')
user = User.new(email: email, password: SecureRandom.hex, admin: role == 'admin', moderator: role == 'moderator', account_attributes: { username: username })
if user.save
prompt.ok 'User created and confirmation mail sent to the user\'s email address.'
prompt.ok "Here is the random password generated for the user: #{user.password}"
else
prompt.warn 'User was not created because of the following errors:'
user.errors.each do |key, val|
prompt.error "#{key}: #{val}"
end
end
else
prompt.ok 'Aborting. Bye!'
end
rescue TTY::Reader::InputInterrupt
prompt.ok 'Aborting. Bye!'
end
end
namespace :media do
desc 'Remove media attachments attributed to silenced accounts'
task remove_silenced: :environment do
nb_media_attachments = 0
MediaAttachment.where(account: Account.silenced).select(:id).reorder(nil).find_in_batches do |media_attachments|
nb_media_attachments += media_attachments.length
Maintenance::DestroyMediaWorker.push_bulk(media_attachments.map(&:id))
end
puts "Scheduled the deletion of #{nb_media_attachments} media attachments"
end
desc 'Remove cached remote media attachments that are older than NUM_DAYS. By default 7 (week)'
task remove_remote: :environment do
puts 'Please use `./bin/tootctl media remove --help` directly'.colorize(:yellow)
require_relative '../mastodon/media_cli'
cli = Mastodon::MediaCLI.new([], days: (ENV['NUM_DAYS'] || 7).to_i)
cli.invoke(:remove)
end
desc 'Set unknown attachment type for remote-only attachments'
task set_unknown: :environment do
puts 'Setting unknown attachment type for remote-only attachments...'
MediaAttachment.where(file_file_name: nil).where.not(type: :unknown).in_batches.update_all(type: :unknown)
puts 'Done!'
end
desc 'Redownload avatars/headers of remote users. Optionally limit to a particular domain with DOMAIN'
task redownload_avatars: :environment do
accounts = Account.remote
accounts = accounts.where(domain: ENV['DOMAIN']) if ENV['DOMAIN'].present?
nb_accounts = 0
accounts.select(:id).reorder(nil).find_in_batches do |accounts_batch|
nb_accounts += accounts_batch.length
Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts_batch.map(&:id))
end
puts "Scheduled the download of avatars/headers for #{nb_accounts} remote users"
end
end
namespace :push do namespace :push do
desc 'Unsubscribes from PuSH updates of feeds nobody follows locally' desc 'Unsubscribes from PuSH updates of feeds nobody follows locally'
task clear: :environment do task clear: :environment do
@ -530,28 +397,6 @@ namespace :mastodon do
end end
end end
namespace :feeds do
desc 'Clear all timelines without regenerating them'
task clear_all: :environment do
Redis.current.keys('feed:*').each { |key| Redis.current.del(key) }
end
desc 'Generates home timelines for users who logged in in the past two weeks'
task build: :environment do
User.active.select(:id, :account_id).reorder(nil).find_in_batches do |users|
RegenerationWorker.push_bulk(users.map(&:account_id))
end
end
end
namespace :users do
desc 'List e-mails of all admin users'
task admins: :environment do
puts 'Admin user emails:'
puts User.admins.map(&:email).join("\n")
end
end
namespace :settings do namespace :settings do
desc 'Open registrations on this instance' desc 'Open registrations on this instance'
task open_registrations: :environment do task open_registrations: :environment do
@ -572,221 +417,6 @@ namespace :mastodon do
puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}" puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
end end
end end
namespace :maintenance do
desc 'Update counter caches'
task update_counter_caches: :environment do
puts 'Updating counter caches for accounts...'
Account.unscoped.where.not(protocol: :activitypub).select('id').find_in_batches do |batch|
Account.where(id: batch.map(&:id)).update_all('statuses_count = (select count(*) from statuses where account_id = accounts.id), followers_count = (select count(*) from follows where target_account_id = accounts.id), following_count = (select count(*) from follows where account_id = accounts.id)')
end
puts 'Updating counter caches for statuses...'
Status.unscoped.select('id').find_in_batches do |batch|
Status.where(id: batch.map(&:id)).update_all('favourites_count = (select count(*) from favourites where favourites.status_id = statuses.id), reblogs_count = (select count(*) from statuses as reblogs where reblogs.reblog_of_id = statuses.id)')
end
puts 'Done!'
end
desc 'Generate static versions of GIF avatars/headers'
task add_static_avatars: :environment do
puts 'Generating static avatars/headers for GIF ones...'
Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
begin
account.avatar.reprocess! if account.avatar_content_type == 'image/gif' && !account.avatar.exists?(:static)
account.header.reprocess! if account.header_content_type == 'image/gif' && !account.header.exists?(:static)
rescue StandardError => e
Rails.logger.error "Error while generating static avatars/headers for account #{account.id}: #{e}"
next
end
end
puts 'Done!'
end
desc 'Ensure referencial integrity'
task prepare_for_foreign_keys: :environment do
# All the deletes:
ActiveRecord::Base.connection.execute('DELETE FROM statuses USING statuses s LEFT JOIN accounts a ON a.id = s.account_id WHERE statuses.id = s.id AND a.id IS NULL')
if ActiveRecord::Base.connection.table_exists? :account_domain_blocks
ActiveRecord::Base.connection.execute('DELETE FROM account_domain_blocks USING account_domain_blocks adb LEFT JOIN accounts a ON a.id = adb.account_id WHERE account_domain_blocks.id = adb.id AND a.id IS NULL')
end
if ActiveRecord::Base.connection.table_exists? :conversation_mutes
ActiveRecord::Base.connection.execute('DELETE FROM conversation_mutes USING conversation_mutes cm LEFT JOIN accounts a ON a.id = cm.account_id WHERE conversation_mutes.id = cm.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM conversation_mutes USING conversation_mutes cm LEFT JOIN conversations c ON c.id = cm.conversation_id WHERE conversation_mutes.id = cm.id AND c.id IS NULL')
end
ActiveRecord::Base.connection.execute('DELETE FROM favourites USING favourites f LEFT JOIN accounts a ON a.id = f.account_id WHERE favourites.id = f.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM favourites USING favourites f LEFT JOIN statuses s ON s.id = f.status_id WHERE favourites.id = f.id AND s.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM blocks USING blocks b LEFT JOIN accounts a ON a.id = b.account_id WHERE blocks.id = b.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM blocks USING blocks b LEFT JOIN accounts a ON a.id = b.target_account_id WHERE blocks.id = b.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM follow_requests USING follow_requests fr LEFT JOIN accounts a ON a.id = fr.account_id WHERE follow_requests.id = fr.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM follow_requests USING follow_requests fr LEFT JOIN accounts a ON a.id = fr.target_account_id WHERE follow_requests.id = fr.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM follows USING follows f LEFT JOIN accounts a ON a.id = f.account_id WHERE follows.id = f.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM follows USING follows f LEFT JOIN accounts a ON a.id = f.target_account_id WHERE follows.id = f.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM mutes USING mutes m LEFT JOIN accounts a ON a.id = m.account_id WHERE mutes.id = m.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM mutes USING mutes m LEFT JOIN accounts a ON a.id = m.target_account_id WHERE mutes.id = m.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM imports USING imports i LEFT JOIN accounts a ON a.id = i.account_id WHERE imports.id = i.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM mentions USING mentions m LEFT JOIN accounts a ON a.id = m.account_id WHERE mentions.id = m.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM mentions USING mentions m LEFT JOIN statuses s ON s.id = m.status_id WHERE mentions.id = m.id AND s.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM notifications USING notifications n LEFT JOIN accounts a ON a.id = n.account_id WHERE notifications.id = n.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM notifications USING notifications n LEFT JOIN accounts a ON a.id = n.from_account_id WHERE notifications.id = n.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM preview_cards USING preview_cards pc LEFT JOIN statuses s ON s.id = pc.status_id WHERE preview_cards.id = pc.id AND s.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM reports USING reports r LEFT JOIN accounts a ON a.id = r.account_id WHERE reports.id = r.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM reports USING reports r LEFT JOIN accounts a ON a.id = r.target_account_id WHERE reports.id = r.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM statuses_tags USING statuses_tags st LEFT JOIN statuses s ON s.id = st.status_id WHERE statuses_tags.tag_id = st.tag_id AND statuses_tags.status_id = st.status_id AND s.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM statuses_tags USING statuses_tags st LEFT JOIN tags t ON t.id = st.tag_id WHERE statuses_tags.tag_id = st.tag_id AND statuses_tags.status_id = st.status_id AND t.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM stream_entries USING stream_entries se LEFT JOIN accounts a ON a.id = se.account_id WHERE stream_entries.id = se.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM subscriptions USING subscriptions s LEFT JOIN accounts a ON a.id = s.account_id WHERE subscriptions.id = s.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM users USING users u LEFT JOIN accounts a ON a.id = u.account_id WHERE users.id = u.id AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM web_settings USING web_settings ws LEFT JOIN users u ON u.id = ws.user_id WHERE web_settings.id = ws.id AND u.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM oauth_access_grants USING oauth_access_grants oag LEFT JOIN users u ON u.id = oag.resource_owner_id WHERE oauth_access_grants.id = oag.id AND oag.resource_owner_id IS NOT NULL AND u.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM oauth_access_grants USING oauth_access_grants oag LEFT JOIN oauth_applications a ON a.id = oag.application_id WHERE oauth_access_grants.id = oag.id AND oag.application_id IS NOT NULL AND a.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM oauth_access_tokens USING oauth_access_tokens oat LEFT JOIN users u ON u.id = oat.resource_owner_id WHERE oauth_access_tokens.id = oat.id AND oat.resource_owner_id IS NOT NULL AND u.id IS NULL')
ActiveRecord::Base.connection.execute('DELETE FROM oauth_access_tokens USING oauth_access_tokens oat LEFT JOIN oauth_applications a ON a.id = oat.application_id WHERE oauth_access_tokens.id = oat.id AND oat.application_id IS NOT NULL AND a.id IS NULL')
# All the nullifies:
ActiveRecord::Base.connection.execute('UPDATE statuses SET in_reply_to_id = NULL FROM statuses s LEFT JOIN statuses rs ON rs.id = s.in_reply_to_id WHERE statuses.id = s.id AND s.in_reply_to_id IS NOT NULL AND rs.id IS NULL')
ActiveRecord::Base.connection.execute('UPDATE statuses SET in_reply_to_account_id = NULL FROM statuses s LEFT JOIN accounts a ON a.id = s.in_reply_to_account_id WHERE statuses.id = s.id AND s.in_reply_to_account_id IS NOT NULL AND a.id IS NULL')
ActiveRecord::Base.connection.execute('UPDATE media_attachments SET status_id = NULL FROM media_attachments ma LEFT JOIN statuses s ON s.id = ma.status_id WHERE media_attachments.id = ma.id AND ma.status_id IS NOT NULL AND s.id IS NULL')
ActiveRecord::Base.connection.execute('UPDATE media_attachments SET account_id = NULL FROM media_attachments ma LEFT JOIN accounts a ON a.id = ma.account_id WHERE media_attachments.id = ma.id AND ma.account_id IS NOT NULL AND a.id IS NULL')
ActiveRecord::Base.connection.execute('UPDATE reports SET action_taken_by_account_id = NULL FROM reports r LEFT JOIN accounts a ON a.id = r.action_taken_by_account_id WHERE reports.id = r.id AND r.action_taken_by_account_id IS NOT NULL AND a.id IS NULL')
end
desc 'Remove deprecated preview cards'
task remove_deprecated_preview_cards: :environment do
next unless ActiveRecord::Base.connection.table_exists? 'deprecated_preview_cards'
class DeprecatedPreviewCard < ActiveRecord::Base
self.inheritance_column = false
path = '/preview_cards/:attachment/:id_partition/:style/:filename'
if ENV['S3_ENABLED'] != 'true'
path = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + path
end
has_attached_file :image, styles: { original: '280x120>' }, convert_options: { all: '-quality 80 -strip' }, path: path
end
puts 'Delete records and associated files from deprecated preview cards? [y/N]: '
confirm = STDIN.gets.chomp
if confirm.casecmp('y').zero?
DeprecatedPreviewCard.in_batches.destroy_all
puts 'Drop deprecated preview cards table? [y/N]: '
confirm = STDIN.gets.chomp
if confirm.casecmp('y').zero?
ActiveRecord::Migration.drop_table :deprecated_preview_cards
end
end
end
desc 'Migrate photo preview cards made before 2.1'
task migrate_photo_preview_cards: :environment do
status_ids = Status.joins(:preview_cards)
.where(preview_cards: { embed_url: '', type: :photo })
.reorder(nil)
.group(:id)
.pluck(:id)
PreviewCard.where(embed_url: '', type: :photo).delete_all
LinkCrawlWorker.push_bulk status_ids
end
desc 'Find case-insensitive username duplicates of local users'
task find_duplicate_usernames: :environment do
include RoutingHelper
disable_log_stdout!
duplicate_masters = Account.find_by_sql('SELECT * FROM accounts WHERE id IN (SELECT min(id) FROM accounts WHERE domain IS NULL GROUP BY lower(username) HAVING count(*) > 1)')
pastel = Pastel.new
duplicate_masters.each do |account|
puts pastel.yellow('First of their name: ') + pastel.bold(account.username) + " (#{admin_account_url(account.id)})"
Account.where('lower(username) = ?', account.username.downcase).where.not(id: account.id).each do |duplicate|
puts ' ' + pastel.red('Duplicate: ') + admin_account_url(duplicate.id)
end
end
end
desc 'Remove all home feed regeneration markers'
task remove_regeneration_markers: :environment do
keys = Redis.current.keys('account:*:regeneration')
Redis.current.pipelined do
keys.each { |key| Redis.current.del(key) }
end
end
desc 'Check every known remote account and delete those that no longer exist in origin'
task purge_removed_accounts: :environment do
prepare_for_options!
options = {}
OptionParser.new do |opts|
opts.banner = 'Usage: rails mastodon:maintenance:purge_removed_accounts [options]'
opts.on('-f', '--force', 'Remove all encountered accounts without asking for confirmation') do
options[:force] = true
end
opts.on('-h', '--help', 'Display this message') do
puts opts
exit
end
end.parse!
disable_log_stdout!
total = Account.remote.where(protocol: :activitypub).count
progress_bar = ProgressBar.create(total: total, format: '%c/%C |%w>%i| %e')
Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
progress_bar.increment
begin
code = Request.new(:head, account.uri).perform(&:code)
rescue StandardError
# This could happen due to network timeout, DNS timeout, wrong SSL cert, etc,
# which should probably not lead to perceiving the account as deleted, so
# just skip till next time
next
end
if [404, 410].include?(code)
if options[:force]
SuspendAccountService.new.call(account)
account.destroy
else
progress_bar.pause
progress_bar.clear
print "\nIt seems like #{account.acct} no longer exists. Purge the account from the database? [Y/n]: ".colorize(:yellow)
confirm = STDIN.gets.chomp
puts ''
progress_bar.resume
if confirm.casecmp('n').zero?
next
else
SuspendAccountService.new.call(account)
account.destroy
end
end
end
end
end
end
end end
def disable_log_stdout! def disable_log_stdout!