accounts.rb 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. # frozen_string_literal: true
  2. require 'set'
  3. require_relative 'base'
  4. module Mastodon::CLI
  5. class Accounts < Base
  6. option :all, type: :boolean
  7. desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
  8. long_desc <<-LONG_DESC
  9. Generate and broadcast new RSA keys as part of security
  10. maintenance.
  11. With the --all option, all local accounts will be subject
  12. to the rotation. Otherwise, and by default, only a single
  13. account specified by the USERNAME argument will be
  14. processed.
  15. LONG_DESC
  16. def rotate(username = nil)
  17. if options[:all]
  18. processed = 0
  19. delay = 0
  20. scope = Account.local.without_suspended
  21. progress = create_progress_bar(scope.count)
  22. scope.find_in_batches do |accounts|
  23. accounts.each do |account|
  24. rotate_keys_for_account(account, delay)
  25. progress.increment
  26. processed += 1
  27. end
  28. delay += 5.minutes
  29. end
  30. progress.finish
  31. say("OK, rotated keys for #{processed} accounts", :green)
  32. elsif username.present?
  33. rotate_keys_for_account(Account.find_local(username))
  34. say('OK', :green)
  35. else
  36. say('No account(s) given', :red)
  37. exit(1)
  38. end
  39. end
  40. option :email, required: true
  41. option :confirmed, type: :boolean
  42. option :role
  43. option :reattach, type: :boolean
  44. option :force, type: :boolean
  45. option :approve, type: :boolean
  46. desc 'create USERNAME', 'Create a new user account'
  47. long_desc <<-LONG_DESC
  48. Create a new user account with a given USERNAME and an
  49. e-mail address provided with --email.
  50. With the --confirmed option, the confirmation e-mail will
  51. be skipped and the account will be active straight away.
  52. With the --role option, the role can be supplied.
  53. With the --reattach option, the new user will be reattached
  54. to a given existing username of an old account. If the old
  55. account is still in use by someone else, you can supply
  56. the --force option to delete the old record and reattach the
  57. username to the new account anyway.
  58. With the --approve option, the account will be approved.
  59. LONG_DESC
  60. def create(username)
  61. role_id = nil
  62. if options[:role]
  63. role = UserRole.find_by(name: options[:role])
  64. if role.nil?
  65. say('Cannot find user role with that name', :red)
  66. exit(1)
  67. end
  68. role_id = role.id
  69. end
  70. account = Account.new(username: username)
  71. password = SecureRandom.hex
  72. user = User.new(email: options[:email], password: password, agreement: true, role_id: role_id, confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
  73. if options[:reattach]
  74. account = Account.find_local(username) || Account.new(username: username)
  75. if account.user.present? && !options[:force]
  76. say('The chosen username is currently in use', :red)
  77. say('Use --force to reattach it anyway and delete the other user')
  78. return
  79. elsif account.user.present?
  80. DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false)
  81. account = Account.new(username: username)
  82. end
  83. end
  84. account.suspended_at = nil
  85. user.account = account
  86. if user.save
  87. if options[:confirmed]
  88. user.confirmed_at = nil
  89. user.confirm!
  90. end
  91. user.approve! if options[:approve]
  92. say('OK', :green)
  93. say("New password: #{password}")
  94. else
  95. report_errors(user.errors)
  96. exit(1)
  97. end
  98. end
  99. option :role
  100. option :remove_role, type: :boolean
  101. option :email
  102. option :confirm, type: :boolean
  103. option :enable, type: :boolean
  104. option :disable, type: :boolean
  105. option :disable_2fa, type: :boolean
  106. option :approve, type: :boolean
  107. option :reset_password, type: :boolean
  108. desc 'modify USERNAME', 'Modify a user account'
  109. long_desc <<-LONG_DESC
  110. Modify a user account.
  111. With the --role option, update the user's role. To remove the user's
  112. role, i.e. demote to normal user, use --remove-role.
  113. With the --email option, update the user's e-mail address. With
  114. the --confirm option, mark the user's e-mail as confirmed.
  115. With the --disable option, lock the user out of their account. The
  116. --enable option is the opposite.
  117. With the --approve option, the account will be approved, if it was
  118. previously not due to not having open registrations.
  119. With the --disable-2fa option, the two-factor authentication
  120. requirement for the user can be removed.
  121. With the --reset-password option, the user's password is replaced by
  122. a randomly-generated one, printed in the output.
  123. LONG_DESC
  124. def modify(username)
  125. user = Account.find_local(username)&.user
  126. if user.nil?
  127. say('No user with such username', :red)
  128. exit(1)
  129. end
  130. if options[:role]
  131. role = UserRole.find_by(name: options[:role])
  132. if role.nil?
  133. say('Cannot find user role with that name', :red)
  134. exit(1)
  135. end
  136. user.role_id = role.id
  137. elsif options[:remove_role]
  138. user.role_id = nil
  139. end
  140. password = SecureRandom.hex if options[:reset_password]
  141. user.password = password if options[:reset_password]
  142. user.email = options[:email] if options[:email]
  143. user.disabled = false if options[:enable]
  144. user.disabled = true if options[:disable]
  145. user.approved = true if options[:approve]
  146. user.otp_required_for_login = false if options[:disable_2fa]
  147. if user.save
  148. user.confirm if options[:confirm]
  149. say('OK', :green)
  150. say("New password: #{password}") if options[:reset_password]
  151. else
  152. report_errors(user.errors)
  153. exit(1)
  154. end
  155. end
  156. option :email
  157. option :dry_run, type: :boolean
  158. desc 'delete [USERNAME]', 'Delete a user'
  159. long_desc <<-LONG_DESC
  160. Remove a user account with a given USERNAME.
  161. With the --email option, the user is selected based on email
  162. rather than username.
  163. LONG_DESC
  164. def delete(username = nil)
  165. if username.present? && options[:email].present?
  166. say('Use username or --email, not both', :red)
  167. exit(1)
  168. elsif username.blank? && options[:email].blank?
  169. say('No username provided', :red)
  170. exit(1)
  171. end
  172. account = nil
  173. if username.present?
  174. account = Account.find_local(username)
  175. if account.nil?
  176. say('No user with such username', :red)
  177. exit(1)
  178. end
  179. else
  180. account = Account.left_joins(:user).find_by(user: { email: options[:email] })
  181. if account.nil?
  182. say('No user with such email', :red)
  183. exit(1)
  184. end
  185. end
  186. say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run_mode_suffix}")
  187. DeleteAccountService.new.call(account, reserve_email: false) unless dry_run?
  188. say("OK#{dry_run_mode_suffix}", :green)
  189. end
  190. option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
  191. desc 'merge FROM TO', 'Merge two remote accounts into one'
  192. long_desc <<-LONG_DESC
  193. Merge two remote accounts specified by their username@domain
  194. into one, whereby the TO account is the one being merged into
  195. and kept, while the FROM one is removed. It is primarily meant
  196. to fix duplicates caused by other servers changing their domain.
  197. The command by default only works if both accounts have the same
  198. public key to prevent mistakes. To override this, use the --force.
  199. LONG_DESC
  200. def merge(from_acct, to_acct)
  201. username, domain = from_acct.split('@')
  202. from_account = Account.find_remote(username, domain)
  203. if from_account.nil? || from_account.local?
  204. say("No such account (#{from_acct})", :red)
  205. exit(1)
  206. end
  207. username, domain = to_acct.split('@')
  208. to_account = Account.find_remote(username, domain)
  209. if to_account.nil? || to_account.local?
  210. say("No such account (#{to_acct})", :red)
  211. exit(1)
  212. end
  213. if from_account.public_key != to_account.public_key && !options[:force]
  214. say("Accounts don't have the same public key, might not be duplicates!", :red)
  215. say('Override with --force', :red)
  216. exit(1)
  217. end
  218. to_account.merge_with!(from_account)
  219. from_account.destroy
  220. say('OK', :green)
  221. end
  222. desc 'fix-duplicates', 'Find duplicate remote accounts and merge them'
  223. option :dry_run, type: :boolean
  224. long_desc <<-LONG_DESC
  225. Merge known remote accounts sharing an ActivityPub actor identifier.
  226. Such duplicates can occur when a remote server admin misconfigures their
  227. domain configuration.
  228. LONG_DESC
  229. def fix_duplicates
  230. Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck(:uri).each do |uri|
  231. say("Duplicates found for #{uri}")
  232. begin
  233. ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run?
  234. rescue => e
  235. say("Error processing #{uri}: #{e}", :red)
  236. end
  237. end
  238. end
  239. desc 'backup USERNAME', 'Request a backup for a user'
  240. long_desc <<-LONG_DESC
  241. Request a new backup for an account with a given USERNAME.
  242. The backup will be created in Sidekiq asynchronously, and
  243. the user will receive an e-mail with a link to it once
  244. it's done.
  245. LONG_DESC
  246. def backup(username)
  247. account = Account.find_local(username)
  248. if account.nil?
  249. say('No user with such username', :red)
  250. exit(1)
  251. end
  252. backup = account.user.backups.create!
  253. BackupWorker.perform_async(backup.id)
  254. say('OK', :green)
  255. end
  256. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  257. option :dry_run, type: :boolean
  258. desc 'cull [DOMAIN...]', 'Remove remote accounts that no longer exist'
  259. long_desc <<-LONG_DESC
  260. Query every single remote account in the database to determine
  261. if it still exists on the origin server, and if it doesn't,
  262. remove it from the database.
  263. Accounts that have had confirmed activity within the last week
  264. are excluded from the checks.
  265. LONG_DESC
  266. def cull(*domains)
  267. skip_threshold = 7.days.ago
  268. skip_domains = Concurrent::Set.new
  269. query = Account.remote.where(protocol: :activitypub)
  270. query = query.where(domain: domains) unless domains.empty?
  271. processed, culled = parallelize_with_progress(query.partitioned) do |account|
  272. next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold) || skip_domains.include?(account.domain)
  273. code = 0
  274. begin
  275. code = Request.new(:head, account.uri).perform(&:code)
  276. rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Mastodon::PrivateNetworkAddressError
  277. skip_domains << account.domain
  278. end
  279. if [404, 410].include?(code)
  280. DeleteAccountService.new.call(account, reserve_username: false) unless dry_run?
  281. 1
  282. else
  283. # Touch account even during dry run to avoid getting the account into the window again
  284. account.touch
  285. end
  286. end
  287. say("Visited #{processed} accounts, removed #{culled}#{dry_run_mode_suffix}", :green)
  288. unless skip_domains.empty?
  289. say('The following domains were not available during the check:', :yellow)
  290. skip_domains.each { |domain| say(" #{domain}") }
  291. end
  292. end
  293. option :all, type: :boolean
  294. option :domain
  295. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  296. option :verbose, type: :boolean, aliases: [:v]
  297. option :dry_run, type: :boolean
  298. desc 'refresh [USERNAMES]', 'Fetch remote user data and files'
  299. long_desc <<-LONG_DESC
  300. Fetch remote user data and files for one or multiple accounts.
  301. With the --all option, all remote accounts will be processed.
  302. Through the --domain option, this can be narrowed down to a
  303. specific domain only. Otherwise, remote accounts must be
  304. specified with space-separated USERNAMES.
  305. LONG_DESC
  306. def refresh(*usernames)
  307. if options[:domain] || options[:all]
  308. scope = Account.remote
  309. scope = scope.where(domain: options[:domain]) if options[:domain]
  310. processed, = parallelize_with_progress(scope) do |account|
  311. next if dry_run?
  312. account.reset_avatar!
  313. account.reset_header!
  314. account.save
  315. end
  316. say("Refreshed #{processed} accounts#{dry_run_mode_suffix}", :green, true)
  317. elsif !usernames.empty?
  318. usernames.each do |user|
  319. user, domain = user.split('@')
  320. account = Account.find_remote(user, domain)
  321. if account.nil?
  322. say('No such account', :red)
  323. exit(1)
  324. end
  325. next if dry_run?
  326. begin
  327. account.reset_avatar!
  328. account.reset_header!
  329. account.save
  330. rescue Mastodon::UnexpectedResponseError
  331. say("Account failed: #{user}@#{domain}", :red)
  332. end
  333. end
  334. say("OK#{dry_run_mode_suffix}", :green)
  335. else
  336. say('No account(s) given', :red)
  337. exit(1)
  338. end
  339. end
  340. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  341. option :verbose, type: :boolean, aliases: [:v]
  342. desc 'follow USERNAME', 'Make all local accounts follow account specified by USERNAME'
  343. def follow(username)
  344. target_account = Account.find_local(username)
  345. if target_account.nil?
  346. say('No such account', :red)
  347. exit(1)
  348. end
  349. processed, = parallelize_with_progress(Account.local.without_suspended) do |account|
  350. FollowService.new.call(account, target_account, bypass_limit: true)
  351. end
  352. say("OK, followed target from #{processed} accounts", :green)
  353. end
  354. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  355. option :verbose, type: :boolean, aliases: [:v]
  356. desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT'
  357. def unfollow(acct)
  358. username, domain = acct.split('@')
  359. target_account = Account.find_remote(username, domain)
  360. if target_account.nil?
  361. say('No such account', :red)
  362. exit(1)
  363. end
  364. processed, = parallelize_with_progress(target_account.followers.local) do |account|
  365. UnfollowService.new.call(account, target_account)
  366. end
  367. say("OK, unfollowed target from #{processed} accounts", :green)
  368. end
  369. option :follows, type: :boolean, default: false
  370. option :followers, type: :boolean, default: false
  371. desc 'reset-relationships USERNAME', 'Reset all follows and/or followers for a user'
  372. long_desc <<-LONG_DESC
  373. Reset all follows and/or followers for a user specified by USERNAME.
  374. With the --follows option, the command unfollows everyone that the account follows,
  375. and then re-follows the users that would be followed by a brand new account.
  376. With the --followers option, the command removes all followers of the account.
  377. LONG_DESC
  378. def reset_relationships(username)
  379. unless options[:follows] || options[:followers]
  380. say('Please specify either --follows or --followers, or both', :red)
  381. exit(1)
  382. end
  383. account = Account.find_local(username)
  384. if account.nil?
  385. say('No such account', :red)
  386. exit(1)
  387. end
  388. total = 0
  389. total += Account.where(id: ::Follow.where(account: account).select(:target_account_id)).count if options[:follows]
  390. total += Account.where(id: ::Follow.where(target_account: account).select(:account_id)).count if options[:followers]
  391. progress = create_progress_bar(total)
  392. processed = 0
  393. if options[:follows]
  394. scope = Account.where(id: ::Follow.where(account: account).select(:target_account_id))
  395. scope.find_each do |target_account|
  396. UnfollowService.new.call(account, target_account)
  397. rescue => e
  398. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  399. ensure
  400. progress.increment
  401. processed += 1
  402. end
  403. BootstrapTimelineWorker.perform_async(account.id)
  404. end
  405. if options[:followers]
  406. scope = Account.where(id: ::Follow.where(target_account: account).select(:account_id))
  407. scope.find_each do |target_account|
  408. UnfollowService.new.call(target_account, account)
  409. rescue => e
  410. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  411. ensure
  412. progress.increment
  413. processed += 1
  414. end
  415. end
  416. progress.finish
  417. say("Processed #{processed} relationships", :green, true)
  418. end
  419. option :number, type: :numeric, aliases: [:n]
  420. option :all, type: :boolean
  421. desc 'approve [USERNAME]', 'Approve pending accounts'
  422. long_desc <<~LONG_DESC
  423. When registrations require review from staff, approve pending accounts,
  424. either all of them with the --all option, or a specific number of them
  425. specified with the --number (-n) option, or only a single specific
  426. account identified by its username.
  427. LONG_DESC
  428. def approve(username = nil)
  429. if options[:all]
  430. User.pending.find_each(&:approve!)
  431. say('OK', :green)
  432. elsif options[:number]&.positive?
  433. User.pending.order(created_at: :asc).limit(options[:number]).each(&:approve!)
  434. say('OK', :green)
  435. elsif username.present?
  436. account = Account.find_local(username)
  437. if account.nil?
  438. say('No such account', :red)
  439. exit(1)
  440. end
  441. account.user&.approve!
  442. say('OK', :green)
  443. else
  444. say('Number must be positive', :red) if options[:number]
  445. exit(1)
  446. end
  447. end
  448. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  449. option :dry_run, type: :boolean
  450. desc 'prune', 'Prune remote accounts that never interacted with local users'
  451. long_desc <<-LONG_DESC
  452. Prune remote account that
  453. - follows no local accounts
  454. - is not followed by any local accounts
  455. - has no statuses on local
  456. - has not been mentioned
  457. - has not been favourited local posts
  458. - not muted/blocked by us
  459. LONG_DESC
  460. def prune
  461. query = Account.remote.where.not(actor_type: %i(Application Service))
  462. query = query.where('NOT EXISTS (SELECT 1 FROM mentions WHERE account_id = accounts.id)')
  463. query = query.where('NOT EXISTS (SELECT 1 FROM favourites WHERE account_id = accounts.id)')
  464. query = query.where('NOT EXISTS (SELECT 1 FROM statuses WHERE account_id = accounts.id)')
  465. query = query.where('NOT EXISTS (SELECT 1 FROM follows WHERE account_id = accounts.id OR target_account_id = accounts.id)')
  466. query = query.where('NOT EXISTS (SELECT 1 FROM blocks WHERE account_id = accounts.id OR target_account_id = accounts.id)')
  467. query = query.where('NOT EXISTS (SELECT 1 FROM mutes WHERE target_account_id = accounts.id)')
  468. query = query.where('NOT EXISTS (SELECT 1 FROM reports WHERE target_account_id = accounts.id)')
  469. query = query.where('NOT EXISTS (SELECT 1 FROM follow_requests WHERE account_id = accounts.id OR target_account_id = accounts.id)')
  470. _, deleted = parallelize_with_progress(query) do |account|
  471. next if account.bot? || account.group?
  472. next if account.suspended?
  473. next if account.silenced?
  474. account.destroy unless dry_run?
  475. 1
  476. end
  477. say("OK, pruned #{deleted} accounts#{dry_run_mode_suffix}", :green)
  478. end
  479. option :force, type: :boolean
  480. option :replay, type: :boolean
  481. option :target
  482. desc 'migrate USERNAME', 'Migrate a local user to another account'
  483. long_desc <<~LONG_DESC
  484. With --replay, replay the last migration of the specified account, in
  485. case some remote server may not have properly processed the associated
  486. `Move` activity.
  487. With --target, specify another account to migrate to.
  488. With --force, perform the migration even if the selected account
  489. redirects to a different account that the one specified.
  490. LONG_DESC
  491. def migrate(username)
  492. if options[:replay].present? && options[:target].present?
  493. say('Use --replay or --target, not both', :red)
  494. exit(1)
  495. end
  496. if options[:replay].blank? && options[:target].blank?
  497. say('Use either --replay or --target', :red)
  498. exit(1)
  499. end
  500. account = Account.find_local(username)
  501. if account.nil?
  502. say("No such account: #{username}", :red)
  503. exit(1)
  504. end
  505. migration = nil
  506. if options[:replay]
  507. migration = account.migrations.last
  508. if migration.nil?
  509. say('The specified account has not performed any migration', :red)
  510. exit(1)
  511. end
  512. unless options[:force] || migration.target_account_id == account.moved_to_account_id
  513. say('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway', :red)
  514. exit(1)
  515. end
  516. end
  517. if options[:target]
  518. target_account = ResolveAccountService.new.call(options[:target])
  519. if target_account.nil?
  520. say("The specified target account could not be found: #{options[:target]}", :red)
  521. exit(1)
  522. end
  523. unless options[:force] || account.moved_to_account_id.nil? || account.moved_to_account_id == target_account.id
  524. say('The specified account is redirecting to a different target account. Use --force if you want to change the migration target', :red)
  525. exit(1)
  526. end
  527. begin
  528. migration = account.migrations.create!(acct: target_account.acct)
  529. rescue ActiveRecord::RecordInvalid => e
  530. say("Error: #{e.message}", :red)
  531. exit(1)
  532. end
  533. end
  534. MoveService.new.call(migration)
  535. say("OK, migrated #{account.acct} to #{migration.target_account.acct}", :green)
  536. end
  537. private
  538. def report_errors(errors)
  539. errors.each do |error|
  540. say('Failure/Error: ', :red)
  541. say(error.attribute)
  542. say(" #{error.type}", :red)
  543. end
  544. end
  545. def rotate_keys_for_account(account, delay = 0)
  546. if account.nil?
  547. say('No such account', :red)
  548. exit(1)
  549. end
  550. old_key = account.private_key
  551. new_key = OpenSSL::PKey::RSA.new(2048)
  552. account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
  553. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, { 'sign_with' => old_key })
  554. end
  555. end
  556. end