accounts_cli.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. # frozen_string_literal: true
  2. require 'set'
  3. require_relative '../../config/boot'
  4. require_relative '../../config/environment'
  5. require_relative 'cli_helper'
  6. module Mastodon
  7. class AccountsCLI < Thor
  8. include CLIHelper
  9. def self.exit_on_failure?
  10. true
  11. end
  12. option :all, type: :boolean
  13. desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
  14. long_desc <<-LONG_DESC
  15. Generate and broadcast new RSA keys as part of security
  16. maintenance.
  17. With the --all option, all local accounts will be subject
  18. to the rotation. Otherwise, and by default, only a single
  19. account specified by the USERNAME argument will be
  20. processed.
  21. LONG_DESC
  22. def rotate(username = nil)
  23. if options[:all]
  24. processed = 0
  25. delay = 0
  26. scope = Account.local.without_suspended
  27. progress = create_progress_bar(scope.count)
  28. scope.find_in_batches do |accounts|
  29. accounts.each do |account|
  30. rotate_keys_for_account(account, delay)
  31. progress.increment
  32. processed += 1
  33. end
  34. delay += 5.minutes
  35. end
  36. progress.finish
  37. say("OK, rotated keys for #{processed} accounts", :green)
  38. elsif username.present?
  39. rotate_keys_for_account(Account.find_local(username))
  40. say('OK', :green)
  41. else
  42. say('No account(s) given', :red)
  43. exit(1)
  44. end
  45. end
  46. option :email, required: true
  47. option :confirmed, type: :boolean
  48. option :role, default: 'user', enum: %w(user moderator admin)
  49. option :reattach, type: :boolean
  50. option :force, type: :boolean
  51. desc 'create USERNAME', 'Create a new user'
  52. long_desc <<-LONG_DESC
  53. Create a new user account with a given USERNAME and an
  54. e-mail address provided with --email.
  55. With the --confirmed option, the confirmation e-mail will
  56. be skipped and the account will be active straight away.
  57. With the --role option one of "user", "admin" or "moderator"
  58. can be supplied. Defaults to "user"
  59. With the --reattach option, the new user will be reattached
  60. to a given existing username of an old account. If the old
  61. account is still in use by someone else, you can supply
  62. the --force option to delete the old record and reattach the
  63. username to the new account anyway.
  64. LONG_DESC
  65. def create(username)
  66. account = Account.new(username: username)
  67. password = SecureRandom.hex
  68. user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
  69. if options[:reattach]
  70. account = Account.find_local(username) || Account.new(username: username)
  71. if account.user.present? && !options[:force]
  72. say('The chosen username is currently in use', :red)
  73. say('Use --force to reattach it anyway and delete the other user')
  74. return
  75. elsif account.user.present?
  76. DeleteAccountService.new.call(account, reserve_email: false)
  77. end
  78. end
  79. account.suspended_at = nil
  80. user.account = account
  81. if user.save
  82. if options[:confirmed]
  83. user.confirmed_at = nil
  84. user.confirm!
  85. end
  86. say('OK', :green)
  87. say("New password: #{password}")
  88. else
  89. user.errors.to_h.each do |key, error|
  90. say('Failure/Error: ', :red)
  91. say(key)
  92. say(' ' + error, :red)
  93. end
  94. exit(1)
  95. end
  96. end
  97. option :role, enum: %w(user moderator admin)
  98. option :email
  99. option :confirm, type: :boolean
  100. option :enable, type: :boolean
  101. option :disable, type: :boolean
  102. option :disable_2fa, type: :boolean
  103. option :approve, type: :boolean
  104. option :reset_password, type: :boolean
  105. desc 'modify USERNAME', 'Modify a user'
  106. long_desc <<-LONG_DESC
  107. Modify a user account.
  108. With the --role option, update the user's role to one of "user",
  109. "moderator" or "admin".
  110. With the --email option, update the user's e-mail address. With
  111. the --confirm option, mark the user's e-mail as confirmed.
  112. With the --disable option, lock the user out of their account. The
  113. --enable option is the opposite.
  114. With the --approve option, the account will be approved, if it was
  115. previously not due to not having open registrations.
  116. With the --disable-2fa option, the two-factor authentication
  117. requirement for the user can be removed.
  118. With the --reset-password option, the user's password is replaced by
  119. a randomly-generated one, printed in the output.
  120. LONG_DESC
  121. def modify(username)
  122. user = Account.find_local(username)&.user
  123. if user.nil?
  124. say('No user with such username', :red)
  125. exit(1)
  126. end
  127. if options[:role]
  128. user.admin = options[:role] == 'admin'
  129. user.moderator = options[:role] == 'moderator'
  130. end
  131. password = SecureRandom.hex if options[:reset_password]
  132. user.password = password if options[:reset_password]
  133. user.email = options[:email] if options[:email]
  134. user.disabled = false if options[:enable]
  135. user.disabled = true if options[:disable]
  136. user.approved = true if options[:approve]
  137. user.otp_required_for_login = false if options[:disable_2fa]
  138. user.confirm if options[:confirm]
  139. if user.save
  140. say('OK', :green)
  141. say("New password: #{password}") if options[:reset_password]
  142. else
  143. user.errors.to_h.each do |key, error|
  144. say('Failure/Error: ', :red)
  145. say(key)
  146. say(' ' + error, :red)
  147. end
  148. exit(1)
  149. end
  150. end
  151. desc 'delete USERNAME', 'Delete a user'
  152. long_desc <<-LONG_DESC
  153. Remove a user account with a given USERNAME.
  154. LONG_DESC
  155. def delete(username)
  156. account = Account.find_local(username)
  157. if account.nil?
  158. say('No user with such username', :red)
  159. exit(1)
  160. end
  161. say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
  162. DeleteAccountService.new.call(account, reserve_email: false)
  163. say('OK', :green)
  164. end
  165. option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
  166. desc 'merge FROM TO', 'Merge two remote accounts into one'
  167. long_desc <<-LONG_DESC
  168. Merge two remote accounts specified by their username@domain
  169. into one, whereby the TO account is the one being merged into
  170. and kept, while the FROM one is removed. It is primarily meant
  171. to fix duplicates caused by other servers changing their domain.
  172. The command by default only works if both accounts have the same
  173. public key to prevent mistakes. To override this, use the --force.
  174. LONG_DESC
  175. def merge(from_acct, to_acct)
  176. username, domain = from_acct.split('@')
  177. from_account = Account.find_remote(username, domain)
  178. if from_account.nil? || from_account.local?
  179. say("No such account (#{from_acct})", :red)
  180. exit(1)
  181. end
  182. username, domain = to_acct.split('@')
  183. to_account = Account.find_remote(username, domain)
  184. if to_account.nil? || to_account.local?
  185. say("No such account (#{to_acct})", :red)
  186. exit(1)
  187. end
  188. if from_account.public_key != to_account.public_key && !options[:force]
  189. say("Accounts don't have the same public key, might not be duplicates!", :red)
  190. say('Override with --force', :red)
  191. exit(1)
  192. end
  193. to_account.merge_with!(from_account)
  194. from_account.destroy
  195. say('OK', :green)
  196. end
  197. desc 'fix-duplicates', 'Find duplicate remote accounts and merge them'
  198. option :dry_run, type: :boolean
  199. long_desc <<-LONG_DESC
  200. Merge known remote accounts sharing an ActivityPub actor identifier.
  201. Such duplicates can occur when a remote server admin misconfigures their
  202. domain configuration.
  203. LONG_DESC
  204. def fix_duplicates
  205. Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck(:uri).each do |uri|
  206. say("Duplicates found for #{uri}")
  207. begin
  208. ActivityPub::FetchRemoteAccountService.new.call(uri) unless options[:dry_run]
  209. rescue => e
  210. say("Error processing #{uri}: #{e}", :red)
  211. end
  212. end
  213. end
  214. desc 'backup USERNAME', 'Request a backup for a user'
  215. long_desc <<-LONG_DESC
  216. Request a new backup for an account with a given USERNAME.
  217. The backup will be created in Sidekiq asynchronously, and
  218. the user will receive an e-mail with a link to it once
  219. it's done.
  220. LONG_DESC
  221. def backup(username)
  222. account = Account.find_local(username)
  223. if account.nil?
  224. say('No user with such username', :red)
  225. exit(1)
  226. end
  227. backup = account.user.backups.create!
  228. BackupWorker.perform_async(backup.id)
  229. say('OK', :green)
  230. end
  231. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  232. option :dry_run, type: :boolean
  233. desc 'cull [DOMAIN...]', 'Remove remote accounts that no longer exist'
  234. long_desc <<-LONG_DESC
  235. Query every single remote account in the database to determine
  236. if it still exists on the origin server, and if it doesn't,
  237. remove it from the database.
  238. Accounts that have had confirmed activity within the last week
  239. are excluded from the checks.
  240. LONG_DESC
  241. def cull(*domains)
  242. skip_threshold = 7.days.ago
  243. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  244. skip_domains = Concurrent::Set.new
  245. query = Account.remote.where(protocol: :activitypub)
  246. query = query.where(domain: domains) unless domains.empty?
  247. processed, culled = parallelize_with_progress(query.partitioned) do |account|
  248. next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold) || skip_domains.include?(account.domain)
  249. code = 0
  250. begin
  251. code = Request.new(:head, account.uri).perform(&:code)
  252. rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
  253. skip_domains << account.domain
  254. end
  255. if [404, 410].include?(code)
  256. DeleteAccountService.new.call(account, reserve_username: false) unless options[:dry_run]
  257. 1
  258. else
  259. # Touch account even during dry run to avoid getting the account into the window again
  260. account.touch
  261. end
  262. end
  263. say("Visited #{processed} accounts, removed #{culled}#{dry_run}", :green)
  264. unless skip_domains.empty?
  265. say('The following domains were not available during the check:', :yellow)
  266. skip_domains.each { |domain| say(' ' + domain) }
  267. end
  268. end
  269. option :all, type: :boolean
  270. option :domain
  271. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  272. option :verbose, type: :boolean, aliases: [:v]
  273. option :dry_run, type: :boolean
  274. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  275. long_desc <<-LONG_DESC
  276. Fetch remote user data and files for one or multiple accounts.
  277. With the --all option, all remote accounts will be processed.
  278. Through the --domain option, this can be narrowed down to a
  279. specific domain only. Otherwise, a single remote account must
  280. be specified with USERNAME.
  281. LONG_DESC
  282. def refresh(username = nil)
  283. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  284. if options[:domain] || options[:all]
  285. scope = Account.remote
  286. scope = scope.where(domain: options[:domain]) if options[:domain]
  287. processed, = parallelize_with_progress(scope) do |account|
  288. next if options[:dry_run]
  289. account.reset_avatar!
  290. account.reset_header!
  291. account.save
  292. end
  293. say("Refreshed #{processed} accounts#{dry_run}", :green, true)
  294. elsif username.present?
  295. username, domain = username.split('@')
  296. account = Account.find_remote(username, domain)
  297. if account.nil?
  298. say('No such account', :red)
  299. exit(1)
  300. end
  301. unless options[:dry_run]
  302. account.reset_avatar!
  303. account.reset_header!
  304. account.save
  305. end
  306. say("OK#{dry_run}", :green)
  307. else
  308. say('No account(s) given', :red)
  309. exit(1)
  310. end
  311. end
  312. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  313. option :verbose, type: :boolean, aliases: [:v]
  314. desc 'follow USERNAME', 'Make all local accounts follow account specified by USERNAME'
  315. def follow(username)
  316. target_account = Account.find_local(username)
  317. if target_account.nil?
  318. say('No such account', :red)
  319. exit(1)
  320. end
  321. processed, = parallelize_with_progress(Account.local.without_suspended) do |account|
  322. FollowService.new.call(account, target_account, bypass_limit: true)
  323. end
  324. say("OK, followed target from #{processed} accounts", :green)
  325. end
  326. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  327. option :verbose, type: :boolean, aliases: [:v]
  328. desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT'
  329. def unfollow(acct)
  330. username, domain = acct.split('@')
  331. target_account = Account.find_remote(username, domain)
  332. if target_account.nil?
  333. say('No such account', :red)
  334. exit(1)
  335. end
  336. processed, = parallelize_with_progress(target_account.followers.local) do |account|
  337. UnfollowService.new.call(account, target_account)
  338. end
  339. say("OK, unfollowed target from #{processed} accounts", :green)
  340. end
  341. option :follows, type: :boolean, default: false
  342. option :followers, type: :boolean, default: false
  343. desc 'reset-relationships USERNAME', 'Reset all follows and/or followers for a user'
  344. long_desc <<-LONG_DESC
  345. Reset all follows and/or followers for a user specified by USERNAME.
  346. With the --follows option, the command unfollows everyone that the account follows,
  347. and then re-follows the users that would be followed by a brand new account.
  348. With the --followers option, the command removes all followers of the account.
  349. LONG_DESC
  350. def reset_relationships(username)
  351. unless options[:follows] || options[:followers]
  352. say('Please specify either --follows or --followers, or both', :red)
  353. exit(1)
  354. end
  355. account = Account.find_local(username)
  356. if account.nil?
  357. say('No such account', :red)
  358. exit(1)
  359. end
  360. total = 0
  361. total += Account.where(id: ::Follow.where(account: account).select(:target_account_id)).count if options[:follows]
  362. total += Account.where(id: ::Follow.where(target_account: account).select(:account_id)).count if options[:followers]
  363. progress = create_progress_bar(total)
  364. processed = 0
  365. if options[:follows]
  366. scope = Account.where(id: ::Follow.where(account: account).select(:target_account_id))
  367. scope.find_each do |target_account|
  368. begin
  369. UnfollowService.new.call(account, target_account)
  370. rescue => e
  371. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  372. ensure
  373. progress.increment
  374. processed += 1
  375. end
  376. end
  377. BootstrapTimelineWorker.perform_async(account.id)
  378. end
  379. if options[:followers]
  380. scope = Account.where(id: ::Follow.where(target_account: account).select(:account_id))
  381. scope.find_each do |target_account|
  382. begin
  383. UnfollowService.new.call(target_account, account)
  384. rescue => e
  385. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  386. ensure
  387. progress.increment
  388. processed += 1
  389. end
  390. end
  391. end
  392. progress.finish
  393. say("Processed #{processed} relationships", :green, true)
  394. end
  395. option :number, type: :numeric, aliases: [:n]
  396. option :all, type: :boolean
  397. desc 'approve [USERNAME]', 'Approve pending accounts'
  398. long_desc <<~LONG_DESC
  399. When registrations require review from staff, approve pending accounts,
  400. either all of them with the --all option, or a specific number of them
  401. specified with the --number (-n) option, or only a single specific
  402. account identified by its username.
  403. LONG_DESC
  404. def approve(username = nil)
  405. if options[:all]
  406. User.pending.find_each(&:approve!)
  407. say('OK', :green)
  408. elsif options[:number]
  409. User.pending.order(created_at: :asc).limit(options[:number]).each(&:approve!)
  410. say('OK', :green)
  411. elsif username.present?
  412. account = Account.find_local(username)
  413. if account.nil?
  414. say('No such account', :red)
  415. exit(1)
  416. end
  417. account.user&.approve!
  418. say('OK', :green)
  419. else
  420. exit(1)
  421. end
  422. end
  423. private
  424. def rotate_keys_for_account(account, delay = 0)
  425. if account.nil?
  426. say('No such account', :red)
  427. exit(1)
  428. end
  429. old_key = account.private_key
  430. new_key = OpenSSL::PKey::RSA.new(2048)
  431. account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
  432. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  433. end
  434. end
  435. end