The code powering m.abunchtell.com https://m.abunchtell.com

accounts_cli.rb 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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. def self.exit_on_failure?
  9. true
  10. end
  11. option :all, type: :boolean
  12. desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
  13. long_desc <<-LONG_DESC
  14. Generate and broadcast new RSA keys as part of security
  15. maintenance.
  16. With the --all option, all local accounts will be subject
  17. to the rotation. Otherwise, and by default, only a single
  18. account specified by the USERNAME argument will be
  19. processed.
  20. LONG_DESC
  21. def rotate(username = nil)
  22. if options[:all]
  23. processed = 0
  24. delay = 0
  25. Account.local.without_suspended.find_in_batches do |accounts|
  26. accounts.each do |account|
  27. rotate_keys_for_account(account, delay)
  28. processed += 1
  29. say('.', :green, false)
  30. end
  31. delay += 5.minutes
  32. end
  33. say
  34. say("OK, rotated keys for #{processed} accounts", :green)
  35. elsif username.present?
  36. rotate_keys_for_account(Account.find_local(username))
  37. say('OK', :green)
  38. else
  39. say('No account(s) given', :red)
  40. exit(1)
  41. end
  42. end
  43. option :email, required: true
  44. option :confirmed, type: :boolean
  45. option :role, default: 'user'
  46. option :reattach, type: :boolean
  47. option :force, type: :boolean
  48. desc 'create USERNAME', 'Create a new user'
  49. long_desc <<-LONG_DESC
  50. Create a new user account with a given USERNAME and an
  51. e-mail address provided with --email.
  52. With the --confirmed option, the confirmation e-mail will
  53. be skipped and the account will be active straight away.
  54. With the --role option one of "user", "admin" or "moderator"
  55. can be supplied. Defaults to "user"
  56. With the --reattach option, the new user will be reattached
  57. to a given existing username of an old account. If the old
  58. account is still in use by someone else, you can supply
  59. the --force option to delete the old record and reattach the
  60. username to the new account anyway.
  61. LONG_DESC
  62. def create(username)
  63. account = Account.new(username: username)
  64. password = SecureRandom.hex
  65. user = User.new(email: options[:email], password: password, agreement: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil)
  66. if options[:reattach]
  67. account = Account.find_local(username) || Account.new(username: username)
  68. if account.user.present? && !options[:force]
  69. say('The chosen username is currently in use', :red)
  70. say('Use --force to reattach it anyway and delete the other user')
  71. return
  72. elsif account.user.present?
  73. account.user.destroy!
  74. end
  75. end
  76. account.suspended = false
  77. user.account = account
  78. if user.save
  79. if options[:confirmed]
  80. user.confirmed_at = nil
  81. user.confirm!
  82. end
  83. say('OK', :green)
  84. say("New password: #{password}")
  85. else
  86. user.errors.to_h.each do |key, error|
  87. say('Failure/Error: ', :red)
  88. say(key)
  89. say(' ' + error, :red)
  90. end
  91. exit(1)
  92. end
  93. end
  94. option :role
  95. option :email
  96. option :confirm, type: :boolean
  97. option :enable, type: :boolean
  98. option :disable, type: :boolean
  99. option :disable_2fa, type: :boolean
  100. desc 'modify USERNAME', 'Modify a user'
  101. long_desc <<-LONG_DESC
  102. Modify a user account.
  103. With the --role option, update the user's role to one of "user",
  104. "moderator" or "admin".
  105. With the --email option, update the user's e-mail address. With
  106. the --confirm option, mark the user's e-mail as confirmed.
  107. With the --disable option, lock the user out of their account. The
  108. --enable option is the opposite.
  109. With the --disable-2fa option, the two-factor authentication
  110. requirement for the user can be removed.
  111. LONG_DESC
  112. def modify(username)
  113. user = Account.find_local(username)&.user
  114. if user.nil?
  115. say('No user with such username', :red)
  116. exit(1)
  117. end
  118. if options[:role]
  119. user.admin = options[:role] == 'admin'
  120. user.moderator = options[:role] == 'moderator'
  121. end
  122. user.email = options[:email] if options[:email]
  123. user.disabled = false if options[:enable]
  124. user.disabled = true if options[:disable]
  125. user.otp_required_for_login = false if options[:disable_2fa]
  126. user.confirm if options[:confirm]
  127. if user.save
  128. say('OK', :green)
  129. else
  130. user.errors.to_h.each do |key, error|
  131. say('Failure/Error: ', :red)
  132. say(key)
  133. say(' ' + error, :red)
  134. end
  135. exit(1)
  136. end
  137. end
  138. desc 'delete USERNAME', 'Delete a user'
  139. long_desc <<-LONG_DESC
  140. Remove a user account with a given USERNAME.
  141. LONG_DESC
  142. def delete(username)
  143. account = Account.find_local(username)
  144. if account.nil?
  145. say('No user with such username', :red)
  146. exit(1)
  147. end
  148. say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
  149. SuspendAccountService.new.call(account, including_user: true)
  150. say('OK', :green)
  151. end
  152. desc 'backup USERNAME', 'Request a backup for a user'
  153. long_desc <<-LONG_DESC
  154. Request a new backup for an account with a given USERNAME.
  155. The backup will be created in Sidekiq asynchronously, and
  156. the user will receive an e-mail with a link to it once
  157. it's done.
  158. LONG_DESC
  159. def backup(username)
  160. account = Account.find_local(username)
  161. if account.nil?
  162. say('No user with such username', :red)
  163. exit(1)
  164. end
  165. backup = account.user.backups.create!
  166. BackupWorker.perform_async(backup.id)
  167. say('OK', :green)
  168. end
  169. option :dry_run, type: :boolean
  170. desc 'cull', 'Remove remote accounts that no longer exist'
  171. long_desc <<-LONG_DESC
  172. Query every single remote account in the database to determine
  173. if it still exists on the origin server, and if it doesn't,
  174. remove it from the database.
  175. Accounts that have had confirmed activity within the last week
  176. are excluded from the checks.
  177. Domains that are unreachable are not checked.
  178. With the --dry-run option, no deletes will actually be carried
  179. out.
  180. LONG_DESC
  181. def cull
  182. skip_threshold = 7.days.ago
  183. culled = 0
  184. skip_domains = Set.new
  185. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  186. Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
  187. next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold)
  188. unless skip_domains.include?(account.domain)
  189. begin
  190. code = Request.new(:head, account.uri).perform(&:code)
  191. rescue HTTP::ConnectionError
  192. skip_domains << account.domain
  193. rescue StandardError
  194. next
  195. end
  196. end
  197. if [404, 410].include?(code)
  198. unless options[:dry_run]
  199. SuspendAccountService.new.call(account)
  200. account.destroy
  201. end
  202. culled += 1
  203. say('+', :green, false)
  204. else
  205. account.touch # Touch account even during dry run to avoid getting the account into the window again
  206. say('.', nil, false)
  207. end
  208. end
  209. say
  210. say("Removed #{culled} accounts. #{skip_domains.size} servers skipped#{dry_run}", skip_domains.empty? ? :green : :yellow)
  211. unless skip_domains.empty?
  212. say('The following servers were not available during the check:', :yellow)
  213. skip_domains.each { |domain| say(' ' + domain) }
  214. end
  215. end
  216. option :all, type: :boolean
  217. option :domain
  218. desc 'refresh [USERNAME]', 'Fetch remote user data and files'
  219. long_desc <<-LONG_DESC
  220. Fetch remote user data and files for one or multiple accounts.
  221. With the --all option, all remote accounts will be processed.
  222. Through the --domain option, this can be narrowed down to a
  223. specific domain only. Otherwise, a single remote account must
  224. be specified with USERNAME.
  225. All processing is done in the background through Sidekiq.
  226. LONG_DESC
  227. def refresh(username = nil)
  228. if options[:domain] || options[:all]
  229. queued = 0
  230. scope = Account.remote
  231. scope = scope.where(domain: options[:domain]) if options[:domain]
  232. scope.select(:id).reorder(nil).find_in_batches do |accounts|
  233. Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts.map(&:id))
  234. queued += accounts.size
  235. end
  236. say("Scheduled refreshment of #{queued} accounts", :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. Maintenance::RedownloadAccountMediaWorker.perform_async(account.id)
  245. say('OK', :green)
  246. else
  247. say('No account(s) given', :red)
  248. exit(1)
  249. end
  250. end
  251. desc 'follow ACCT', 'Make all local accounts follow account specified by ACCT'
  252. long_desc <<-LONG_DESC
  253. Make all local accounts follow an account specified by ACCT. ACCT can be
  254. a simple username, in case of a local user. It can also be in the format
  255. username@domain, in case of a remote user.
  256. LONG_DESC
  257. def follow(acct)
  258. target_account = ResolveAccountService.new.call(acct)
  259. processed = 0
  260. failed = 0
  261. if target_account.nil?
  262. say("Target account (#{acct}) could not be resolved", :red)
  263. exit(1)
  264. end
  265. Account.local.without_suspended.find_each do |account|
  266. begin
  267. FollowService.new.call(account, target_account)
  268. processed += 1
  269. say('.', :green, false)
  270. rescue StandardError
  271. failed += 1
  272. say('.', :red, false)
  273. end
  274. end
  275. say("OK, followed target from #{processed} accounts, skipped #{failed}", :green)
  276. end
  277. desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT'
  278. long_desc <<-LONG_DESC
  279. Make all local accounts unfollow an account specified by ACCT. ACCT can be
  280. a simple username, in case of a local user. It can also be in the format
  281. username@domain, in case of a remote user.
  282. LONG_DESC
  283. def unfollow(acct)
  284. target_account = Account.find_remote(*acct.split('@'))
  285. processed = 0
  286. failed = 0
  287. if target_account.nil?
  288. say("Target account (#{acct}) was not found", :red)
  289. exit(1)
  290. end
  291. target_account.followers.local.find_each do |account|
  292. begin
  293. UnfollowService.new.call(account, target_account)
  294. processed += 1
  295. say('.', :green, false)
  296. rescue StandardError
  297. failed += 1
  298. say('.', :red, false)
  299. end
  300. end
  301. say("OK, unfollowed target from #{processed} accounts, skipped #{failed}", :green)
  302. end
  303. private
  304. def rotate_keys_for_account(account, delay = 0)
  305. if account.nil?
  306. say('No such account', :red)
  307. exit(1)
  308. end
  309. old_key = account.private_key
  310. new_key = OpenSSL::PKey::RSA.new(2048)
  311. account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
  312. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  313. end
  314. end
  315. end