The code powering m.abunchtell.com https://m.abunchtell.com
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.
 
 
 
 

488 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. 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 an account specified by ACCT. ACCT can be
  265. a simple username, in case of a local user. It can also be in the format
  266. username@domain, in case of a remote user.
  267. LONG_DESC
  268. def follow(acct)
  269. target_account = ResolveAccountService.new.call(acct)
  270. processed = 0
  271. failed = 0
  272. if target_account.nil?
  273. say("Target account (#{acct}) could not be resolved", :red)
  274. exit(1)
  275. end
  276. Account.local.without_suspended.find_each do |account|
  277. begin
  278. FollowService.new.call(account, target_account)
  279. processed += 1
  280. say('.', :green, false)
  281. rescue StandardError
  282. failed += 1
  283. say('.', :red, false)
  284. end
  285. end
  286. say("OK, followed target from #{processed} accounts, skipped #{failed}", :green)
  287. end
  288. desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT'
  289. long_desc <<-LONG_DESC
  290. Make all local accounts unfollow an account specified by ACCT. ACCT can be
  291. a simple username, in case of a local user. It can also be in the format
  292. username@domain, in case of a remote user.
  293. LONG_DESC
  294. def unfollow(acct)
  295. target_account = Account.find_remote(*acct.split('@'))
  296. processed = 0
  297. failed = 0
  298. if target_account.nil?
  299. say("Target account (#{acct}) was not found", :red)
  300. exit(1)
  301. end
  302. target_account.followers.local.find_each do |account|
  303. begin
  304. UnfollowService.new.call(account, target_account)
  305. processed += 1
  306. say('.', :green, false)
  307. rescue StandardError
  308. failed += 1
  309. say('.', :red, false)
  310. end
  311. end
  312. say("OK, unfollowed target from #{processed} accounts, skipped #{failed}", :green)
  313. end
  314. option :follows, type: :boolean, default: false
  315. option :followers, type: :boolean, default: false
  316. desc 'reset-relationships USERNAME', 'Reset all follows and/or followers for a user'
  317. long_desc <<-LONG_DESC
  318. Reset all follows and/or followers for a user specified by USERNAME.
  319. With the --follows option, the command unfollows everyone that the account follows,
  320. and then re-follows the users that would be followed by a brand new account.
  321. With the --followers option, the command removes all followers of the account.
  322. LONG_DESC
  323. def reset_relationships(username)
  324. unless options[:follows] || options[:followers]
  325. say('Please specify either --follows or --followers, or both', :red)
  326. exit(1)
  327. end
  328. account = Account.find_local(username)
  329. if account.nil?
  330. say('No user with such username', :red)
  331. exit(1)
  332. end
  333. if options[:follows]
  334. processed = 0
  335. failed = 0
  336. say("Unfollowing #{account.username}'s followees, this might take a while...")
  337. Account.where(id: ::Follow.where(account: account).select(:target_account_id)).find_each do |target_account|
  338. begin
  339. UnfollowService.new.call(account, target_account)
  340. processed += 1
  341. say('.', :green, false)
  342. rescue StandardError
  343. failed += 1
  344. say('.', :red, false)
  345. end
  346. end
  347. BootstrapTimelineWorker.perform_async(account.id)
  348. say("OK, unfollowed #{processed} followees, skipped #{failed}", :green)
  349. end
  350. if options[:followers]
  351. processed = 0
  352. failed = 0
  353. say("Removing #{account.username}'s followers, this might take a while...")
  354. Account.where(id: ::Follow.where(target_account: account).select(:account_id)).find_each do |target_account|
  355. begin
  356. UnfollowService.new.call(target_account, account)
  357. processed += 1
  358. say('.', :green, false)
  359. rescue StandardError
  360. failed += 1
  361. say('.', :red, false)
  362. end
  363. end
  364. say("OK, removed #{processed} followers, skipped #{failed}", :green)
  365. end
  366. end
  367. option :number, type: :numeric, aliases: [:n]
  368. option :all, type: :boolean
  369. desc 'approve [USERNAME]', 'Approve pending accounts'
  370. long_desc <<~LONG_DESC
  371. When registrations require review from staff, approve pending accounts,
  372. either all of them with the --all option, or a specific number of them
  373. specified with the --number (-n) option, or only a single specific
  374. account identified by its username.
  375. LONG_DESC
  376. def approve(username = nil)
  377. if options[:all]
  378. User.pending.find_each(&:approve!)
  379. say('OK', :green)
  380. elsif options[:number]
  381. User.pending.limit(options[:number]).each(&:approve!)
  382. say('OK', :green)
  383. elsif username.present?
  384. account = Account.find_local(username)
  385. if account.nil?
  386. say('No such account', :red)
  387. exit(1)
  388. end
  389. account.user&.approve!
  390. say('OK', :green)
  391. else
  392. exit(1)
  393. end
  394. end
  395. private
  396. def rotate_keys_for_account(account, delay = 0)
  397. if account.nil?
  398. say('No such account', :red)
  399. exit(1)
  400. end
  401. old_key = account.private_key
  402. new_key = OpenSSL::PKey::RSA.new(2048)
  403. account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
  404. ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
  405. end
  406. end
  407. end