You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

523 lines
17 KiB

  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'
  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
  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', '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
  242. skip_threshold = 7.days.ago
  243. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  244. skip_domains = Concurrent::Set.new
  245. processed, culled = parallelize_with_progress(Account.remote.where(protocol: :activitypub).partitioned) do |account|
  246. next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold) || skip_domains.include?(account.domain)
  247. code = 0
  248. begin
  249. code = Request.new(:head, account.uri).perform(&:code)
  250. rescue HTTP::ConnectionError
  251. skip_domains << account.domain
  252. end
  253. if [404, 410].include?(code)
  254. DeleteAccountService.new.call(account, reserve_username: false) unless options[:dry_run]
  255. 1
  256. else
  257. # Touch account even during dry run to avoid getting the account into the window again
  258. account.touch
  259. end
  260. end
  261. say("Visited #{processed} accounts, removed #{culled}#{dry_run}", :green)
  262. unless skip_domains.empty?
  263. say('The following domains were not available during the check:', :yellow)
  264. skip_domains.each { |domain| say(' ' + domain) }
  265. end
  266. end
  267. option :all, type: :boolean
  268. option :domain
  269. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  270. option :verbose, type: :boolean, aliases: [:v]
  271. option :dry_run, type: :boolean
  272. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  273. long_desc <<-LONG_DESC
  274. Fetch remote user data and files for one or multiple accounts.
  275. With the --all option, all remote accounts will be processed.
  276. Through the --domain option, this can be narrowed down to a
  277. specific domain only. Otherwise, a single remote account must
  278. be specified with USERNAME.
  279. LONG_DESC
  280. def refresh(username = nil)
  281. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  282. if options[:domain] || options[:all]
  283. scope = Account.remote
  284. scope = scope.where(domain: options[:domain]) if options[:domain]
  285. processed, = parallelize_with_progress(scope) do |account|
  286. next if options[:dry_run]
  287. account.reset_avatar!
  288. account.reset_header!
  289. account.save
  290. end
  291. say("Refreshed #{processed} accounts#{dry_run}", :green, true)
  292. elsif username.present?
  293. username, domain = username.split('@')
  294. account = Account.find_remote(username, domain)
  295. if account.nil?
  296. say('No such account', :red)
  297. exit(1)
  298. end
  299. unless options[:dry_run]
  300. account.reset_avatar!
  301. account.reset_header!
  302. account.save
  303. end
  304. say("OK#{dry_run}", :green)
  305. else
  306. say('No account(s) given', :red)
  307. exit(1)
  308. end
  309. end
  310. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  311. option :verbose, type: :boolean, aliases: [:v]
  312. desc 'follow USERNAME', 'Make all local accounts follow account specified by USERNAME'
  313. def follow(username)
  314. target_account = Account.find_local(username)
  315. if target_account.nil?
  316. say('No such account', :red)
  317. exit(1)
  318. end
  319. processed, = parallelize_with_progress(Account.local.without_suspended) do |account|
  320. FollowService.new.call(account, target_account, bypass_limit: true)
  321. end
  322. say("OK, followed target from #{processed} accounts", :green)
  323. end
  324. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  325. option :verbose, type: :boolean, aliases: [:v]
  326. desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT'
  327. def unfollow(acct)
  328. username, domain = acct.split('@')
  329. target_account = Account.find_remote(username, domain)
  330. if target_account.nil?
  331. say('No such account', :red)
  332. exit(1)
  333. end
  334. processed, = parallelize_with_progress(target_account.followers.local) do |account|
  335. UnfollowService.new.call(account, target_account)
  336. end
  337. say("OK, unfollowed target from #{processed} accounts", :green)
  338. end
  339. option :follows, type: :boolean, default: false
  340. option :followers, type: :boolean, default: false
  341. desc 'reset-relationships USERNAME', 'Reset all follows and/or followers for a user'
  342. long_desc <<-LONG_DESC
  343. Reset all follows and/or followers for a user specified by USERNAME.
  344. With the --follows option, the command unfollows everyone that the account follows,
  345. and then re-follows the users that would be followed by a brand new account.
  346. With the --followers option, the command removes all followers of the account.
  347. LONG_DESC
  348. def reset_relationships(username)
  349. unless options[:follows] || options[:followers]
  350. say('Please specify either --follows or --followers, or both', :red)
  351. exit(1)
  352. end
  353. account = Account.find_local(username)
  354. if account.nil?
  355. say('No such account', :red)
  356. exit(1)
  357. end
  358. total = 0
  359. total += Account.where(id: ::Follow.where(account: account).select(:target_account_id)).count if options[:follows]
  360. total += Account.where(id: ::Follow.where(target_account: account).select(:account_id)).count if options[:followers]
  361. progress = create_progress_bar(total)
  362. processed = 0
  363. if options[:follows]
  364. scope = Account.where(id: ::Follow.where(account: account).select(:target_account_id))
  365. scope.find_each do |target_account|
  366. begin
  367. UnfollowService.new.call(account, target_account)
  368. rescue => e
  369. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  370. ensure
  371. progress.increment
  372. processed += 1
  373. end
  374. end
  375. BootstrapTimelineWorker.perform_async(account.id)
  376. end
  377. if options[:followers]
  378. scope = Account.where(id: ::Follow.where(target_account: account).select(:account_id))
  379. scope.find_each do |target_account|
  380. begin
  381. UnfollowService.new.call(target_account, account)
  382. rescue => e
  383. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  384. ensure
  385. progress.increment
  386. processed += 1
  387. end
  388. end
  389. end
  390. progress.finish
  391. say("Processed #{processed} relationships", :green, true)
  392. end
  393. option :number, type: :numeric, aliases: [:n]
  394. option :all, type: :boolean
  395. desc 'approve [USERNAME]', 'Approve pending accounts'
  396. long_desc <<~LONG_DESC
  397. When registrations require review from staff, approve pending accounts,
  398. either all of them with the --all option, or a specific number of them
  399. specified with the --number (-n) option, or only a single specific
  400. account identified by its username.
  401. LONG_DESC
  402. def approve(username = nil)
  403. if options[:all]
  404. User.pending.find_each(&:approve!)
  405. say('OK', :green)
  406. elsif options[:number]
  407. User.pending.limit(options[:number]).each(&:approve!)
  408. say('OK', :green)
  409. elsif username.present?
  410. account = Account.find_local(username)
  411. if account.nil?
  412. say('No such account', :red)
  413. exit(1)
  414. end
  415. account.user&.approve!
  416. say('OK', :green)
  417. else
  418. exit(1)
  419. end
  420. end
  421. private
  422. def rotate_keys_for_account(account, delay = 0)
  423. if account.nil?
  424. say('No such account', :red)
  425. exit(1)
  426. end
  427. old_key = account.private_key
  428. new_key = OpenSSL::PKey::RSA.new(2048)
  429. account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
  430. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  431. end
  432. end
  433. end