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.

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