accounts_cli.rb 17 KB

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