The code powering m.abunchtell.com https://m.abunchtell.com
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

177 lines
6.5 KiB

  1. # frozen_string_literal: true
  2. require 'concurrent'
  3. require_relative '../../config/boot'
  4. require_relative '../../config/environment'
  5. require_relative 'cli_helper'
  6. module Mastodon
  7. class DomainsCLI < Thor
  8. def self.exit_on_failure?
  9. true
  10. end
  11. option :dry_run, type: :boolean
  12. option :whitelist_mode, type: :boolean
  13. desc 'purge [DOMAIN]', 'Remove accounts from a DOMAIN without a trace'
  14. long_desc <<-LONG_DESC
  15. Remove all accounts from a given DOMAIN without leaving behind any
  16. records. Unlike a suspension, if the DOMAIN still exists in the wild,
  17. it means the accounts could return if they are resolved again.
  18. When the --whitelist-mode option is given, instead of purging accounts
  19. from a single domain, all accounts from domains that are not whitelisted
  20. are removed from the database.
  21. LONG_DESC
  22. def purge(domain = nil)
  23. removed = 0
  24. dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
  25. scope = begin
  26. if options[:whitelist_mode]
  27. Account.remote.where.not(domain: DomainAllow.pluck(:domain))
  28. elsif domain.present?
  29. Account.remote.where(domain: domain)
  30. else
  31. say('No domain given', :red)
  32. exit(1)
  33. end
  34. end
  35. scope.find_each do |account|
  36. SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run]
  37. removed += 1
  38. say('.', :green, false)
  39. end
  40. DomainBlock.where(domain: domain).destroy_all unless options[:dry_run]
  41. say
  42. say("Removed #{removed} accounts#{dry_run}", :green)
  43. custom_emojis = CustomEmoji.where(domain: domain)
  44. custom_emojis_count = custom_emojis.count
  45. custom_emojis.destroy_all unless options[:dry_run]
  46. say("Removed #{custom_emojis_count} custom emojis", :green)
  47. end
  48. option :concurrency, type: :numeric, default: 50, aliases: [:c]
  49. option :silent, type: :boolean, default: false, aliases: [:s]
  50. option :format, type: :string, default: 'summary', aliases: [:f]
  51. option :exclude_suspended, type: :boolean, default: false, aliases: [:x]
  52. desc 'crawl [START]', 'Crawl all known peers, optionally beginning at START'
  53. long_desc <<-LONG_DESC
  54. Crawl the fediverse by using the Mastodon REST API endpoints that expose
  55. all known peers, and collect statistics from those peers, as long as those
  56. peers support those API endpoints. When no START is given, the command uses
  57. this server's own database of known peers to seed the crawl.
  58. The --concurrency (-c) option controls the number of threads performing HTTP
  59. requests at the same time. More threads means the crawl may complete faster.
  60. The --silent (-s) option controls progress output.
  61. The --format (-f) option controls how the data is displayed at the end. By
  62. default (`summary`), a summary of the statistics is returned. The other options
  63. are `domains`, which returns a newline-delimited list of all discovered peers,
  64. and `json`, which dumps all the aggregated data raw.
  65. The --exclude-suspended (-x) option means that domains that are suspended
  66. instance-wide do not appear in the output and are not included in summaries.
  67. This also excludes subdomains of any of those domains.
  68. LONG_DESC
  69. def crawl(start = nil)
  70. stats = Concurrent::Hash.new
  71. processed = Concurrent::AtomicFixnum.new(0)
  72. failed = Concurrent::AtomicFixnum.new(0)
  73. start_at = Time.now.to_f
  74. seed = start ? [start] : Account.remote.domains
  75. blocked_domains = Regexp.new('\\.?' + DomainBlock.where(severity: 1).pluck(:domain).join('|') + '$')
  76. pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0)
  77. work_unit = ->(domain) do
  78. next if stats.key?(domain)
  79. next if options[:exclude_suspended] && domain.match(blocked_domains)
  80. stats[domain] = nil
  81. processed.increment
  82. begin
  83. Request.new(:get, "https://#{domain}/api/v1/instance").perform do |res|
  84. next unless res.code == 200
  85. stats[domain] = Oj.load(res.to_s)
  86. end
  87. Request.new(:get, "https://#{domain}/api/v1/instance/peers").perform do |res|
  88. next unless res.code == 200
  89. Oj.load(res.to_s).reject { |peer| stats.key?(peer) }.each do |peer|
  90. pool.post(peer, &work_unit)
  91. end
  92. end
  93. Request.new(:get, "https://#{domain}/api/v1/instance/activity").perform do |res|
  94. next unless res.code == 200
  95. stats[domain]['activity'] = Oj.load(res.to_s)
  96. end
  97. say('.', :green, false) unless options[:silent]
  98. rescue StandardError
  99. failed.increment
  100. say('.', :red, false) unless options[:silent]
  101. end
  102. end
  103. seed.each do |domain|
  104. pool.post(domain, &work_unit)
  105. end
  106. sleep 20
  107. sleep 20 until pool.queue_length.zero?
  108. pool.shutdown
  109. pool.wait_for_termination(20)
  110. ensure
  111. pool.shutdown
  112. say unless options[:silent]
  113. case options[:format]
  114. when 'summary'
  115. stats_to_summary(stats, processed, failed, start_at)
  116. when 'domains'
  117. stats_to_domains(stats)
  118. when 'json'
  119. stats_to_json(stats)
  120. end
  121. end
  122. private
  123. def stats_to_summary(stats, processed, failed, start_at)
  124. stats.compact!
  125. total_domains = stats.size
  126. total_users = stats.reduce(0) { |sum, (_key, val)| val.is_a?(Hash) && val['stats'].is_a?(Hash) ? sum + val['stats']['user_count'].to_i : sum }
  127. total_active = stats.reduce(0) { |sum, (_key, val)| val.is_a?(Hash) && val['activity'].is_a?(Array) && val['activity'].size > 2 && val['activity'][1].is_a?(Hash) ? sum + val['activity'][1]['logins'].to_i : sum }
  128. total_joined = stats.reduce(0) { |sum, (_key, val)| val.is_a?(Hash) && val['activity'].is_a?(Array) && val['activity'].size > 2 && val['activity'][1].is_a?(Hash) ? sum + val['activity'][1]['registrations'].to_i : sum }
  129. say("Visited #{processed.value} domains, #{failed.value} failed (#{(Time.now.to_f - start_at).round}s elapsed)", :green)
  130. say("Total servers: #{total_domains}", :green)
  131. say("Total registered: #{total_users}", :green)
  132. say("Total active last week: #{total_active}", :green)
  133. say("Total joined last week: #{total_joined}", :green)
  134. end
  135. def stats_to_domains(stats)
  136. say(stats.keys.join("\n"))
  137. end
  138. def stats_to_json(stats)
  139. stats.compact!
  140. say(Oj.dump(stats))
  141. end
  142. end
  143. end