The code powering m.abunchtell.com https://m.abunchtell.com
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

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