The code powering m.abunchtell.com https://m.abunchtell.com
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

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