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.

463 lines
15 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)
  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. account.user.destroy!
  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. SuspendAccountService.new.call(account, reserve_email: false)
  163. say('OK', :green)
  164. end
  165. desc 'backup USERNAME', 'Request a backup for a user'
  166. long_desc <<-LONG_DESC
  167. Request a new backup for an account with a given USERNAME.
  168. The backup will be created in Sidekiq asynchronously, and
  169. the user will receive an e-mail with a link to it once
  170. it's done.
  171. LONG_DESC
  172. def backup(username)
  173. account = Account.find_local(username)
  174. if account.nil?
  175. say('No user with such username', :red)
  176. exit(1)
  177. end
  178. backup = account.user.backups.create!
  179. BackupWorker.perform_async(backup.id)
  180. say('OK', :green)
  181. end
  182. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  183. option :dry_run, type: :boolean
  184. desc 'cull', 'Remove remote accounts that no longer exist'
  185. long_desc <<-LONG_DESC
  186. Query every single remote account in the database to determine
  187. if it still exists on the origin server, and if it doesn't,
  188. remove it from the database.
  189. Accounts that have had confirmed activity within the last week
  190. are excluded from the checks.
  191. LONG_DESC
  192. def cull
  193. skip_threshold = 7.days.ago
  194. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  195. skip_domains = Concurrent::Set.new
  196. processed, culled = parallelize_with_progress(Account.remote.where(protocol: :activitypub).partitioned) do |account|
  197. next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold) || skip_domains.include?(account.domain)
  198. code = 0
  199. begin
  200. code = Request.new(:head, account.uri).perform(&:code)
  201. rescue HTTP::ConnectionError
  202. skip_domains << account.domain
  203. end
  204. if [404, 410].include?(code)
  205. SuspendAccountService.new.call(account, reserve_username: false) unless options[:dry_run]
  206. 1
  207. else
  208. # Touch account even during dry run to avoid getting the account into the window again
  209. account.touch
  210. end
  211. end
  212. say("Visited #{processed} accounts, removed #{culled}#{dry_run}", :green)
  213. unless skip_domains.empty?
  214. say('The following domains were not available during the check:', :yellow)
  215. skip_domains.each { |domain| say(' ' + domain) }
  216. end
  217. end
  218. option :all, type: :boolean
  219. option :domain
  220. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  221. option :verbose, type: :boolean, aliases: [:v]
  222. option :dry_run, type: :boolean
  223. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  224. long_desc <<-LONG_DESC
  225. Fetch remote user data and files for one or multiple accounts.
  226. With the --all option, all remote accounts will be processed.
  227. Through the --domain option, this can be narrowed down to a
  228. specific domain only. Otherwise, a single remote account must
  229. be specified with USERNAME.
  230. LONG_DESC
  231. def refresh(username = nil)
  232. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  233. if options[:domain] || options[:all]
  234. scope = Account.remote
  235. scope = scope.where(domain: options[:domain]) if options[:domain]
  236. processed, = parallelize_with_progress(scope) do |account|
  237. next if options[:dry_run]
  238. account.reset_avatar!
  239. account.reset_header!
  240. account.save
  241. end
  242. say("Refreshed #{processed} accounts#{dry_run}", :green, true)
  243. elsif username.present?
  244. username, domain = username.split('@')
  245. account = Account.find_remote(username, domain)
  246. if account.nil?
  247. say('No such account', :red)
  248. exit(1)
  249. end
  250. unless options[:dry_run]
  251. account.reset_avatar!
  252. account.reset_header!
  253. account.save
  254. end
  255. say("OK#{dry_run}", :green)
  256. else
  257. say('No account(s) given', :red)
  258. exit(1)
  259. end
  260. end
  261. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  262. option :verbose, type: :boolean, aliases: [:v]
  263. desc 'follow USERNAME', 'Make all local accounts follow account specified by USERNAME'
  264. def follow(username)
  265. target_account = Account.find_local(username)
  266. if target_account.nil?
  267. say('No such account', :red)
  268. exit(1)
  269. end
  270. processed, = parallelize_with_progress(Account.local.without_suspended) do |account|
  271. FollowService.new.call(account, target_account)
  272. end
  273. say("OK, followed target from #{processed} accounts", :green)
  274. end
  275. option :concurrency, type: :numeric, default: 5, aliases: [:c]
  276. option :verbose, type: :boolean, aliases: [:v]
  277. desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT'
  278. def unfollow(acct)
  279. target_account = Account.find_remote(*acct.split('@'))
  280. if target_account.nil?
  281. say('No such account', :red)
  282. exit(1)
  283. end
  284. parallelize_with_progress(target_account.followers.local) do |account|
  285. UnfollowService.new.call(account, target_account)
  286. end
  287. say("OK, unfollowed target from #{processed} accounts", :green)
  288. end
  289. option :follows, type: :boolean, default: false
  290. option :followers, type: :boolean, default: false
  291. desc 'reset-relationships USERNAME', 'Reset all follows and/or followers for a user'
  292. long_desc <<-LONG_DESC
  293. Reset all follows and/or followers for a user specified by USERNAME.
  294. With the --follows option, the command unfollows everyone that the account follows,
  295. and then re-follows the users that would be followed by a brand new account.
  296. With the --followers option, the command removes all followers of the account.
  297. LONG_DESC
  298. def reset_relationships(username)
  299. unless options[:follows] || options[:followers]
  300. say('Please specify either --follows or --followers, or both', :red)
  301. exit(1)
  302. end
  303. account = Account.find_local(username)
  304. if account.nil?
  305. say('No such account', :red)
  306. exit(1)
  307. end
  308. total = 0
  309. total += Account.where(id: ::Follow.where(account: account).select(:target_account_id)).count if options[:follows]
  310. total += Account.where(id: ::Follow.where(target_account: account).select(:account_id)).count if options[:followers]
  311. progress = create_progress_bar(total)
  312. processed = 0
  313. if options[:follows]
  314. scope = Account.where(id: ::Follow.where(account: account).select(:target_account_id))
  315. scope.find_each do |target_account|
  316. begin
  317. UnfollowService.new.call(account, target_account)
  318. rescue => e
  319. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  320. ensure
  321. progress.increment
  322. processed += 1
  323. end
  324. end
  325. BootstrapTimelineWorker.perform_async(account.id)
  326. end
  327. if options[:followers]
  328. scope = Account.where(id: ::Follow.where(target_account: account).select(:account_id))
  329. scope.find_each do |target_account|
  330. begin
  331. UnfollowService.new.call(target_account, account)
  332. rescue => e
  333. progress.log pastel.red("Error processing #{target_account.id}: #{e}")
  334. ensure
  335. progress.increment
  336. processed += 1
  337. end
  338. end
  339. end
  340. progress.finish
  341. say("Processed #{processed} relationships", :green, true)
  342. end
  343. option :number, type: :numeric, aliases: [:n]
  344. option :all, type: :boolean
  345. desc 'approve [USERNAME]', 'Approve pending accounts'
  346. long_desc <<~LONG_DESC
  347. When registrations require review from staff, approve pending accounts,
  348. either all of them with the --all option, or a specific number of them
  349. specified with the --number (-n) option, or only a single specific
  350. account identified by its username.
  351. LONG_DESC
  352. def approve(username = nil)
  353. if options[:all]
  354. User.pending.find_each(&:approve!)
  355. say('OK', :green)
  356. elsif options[:number]
  357. User.pending.limit(options[:number]).each(&:approve!)
  358. say('OK', :green)
  359. elsif username.present?
  360. account = Account.find_local(username)
  361. if account.nil?
  362. say('No such account', :red)
  363. exit(1)
  364. end
  365. account.user&.approve!
  366. say('OK', :green)
  367. else
  368. exit(1)
  369. end
  370. end
  371. private
  372. def rotate_keys_for_account(account, delay = 0)
  373. if account.nil?
  374. say('No such account', :red)
  375. exit(1)
  376. end
  377. old_key = account.private_key
  378. new_key = OpenSSL::PKey::RSA.new(2048)
  379. account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
  380. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  381. end
  382. end
  383. end