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.

142 lines
4.1 KiB

  1. # frozen_string_literal: true
  2. class Trends::Statuses < Trends::Base
  3. PREFIX = 'trending_statuses'
  4. self.default_options = {
  5. threshold: 5,
  6. review_threshold: 3,
  7. score_halflife: 2.hours.freeze,
  8. }
  9. class Query < Trends::Query
  10. def filtered_for!(account)
  11. @account = account
  12. self
  13. end
  14. def filtered_for(account)
  15. clone.filtered_for!(account)
  16. end
  17. private
  18. def apply_scopes(scope)
  19. scope.includes(:account)
  20. end
  21. def perform_queries
  22. return super if @account.nil?
  23. statuses = super
  24. account_ids = statuses.map(&:account_id)
  25. account_domains = statuses.map(&:account_domain)
  26. preloaded_relations = {
  27. blocking: Account.blocking_map(account_ids, @account.id),
  28. blocked_by: Account.blocked_by_map(account_ids, @account.id),
  29. muting: Account.muting_map(account_ids, @account.id),
  30. following: Account.following_map(account_ids, @account.id),
  31. domain_blocking_by_domain: Account.domain_blocking_map_by_domain(account_domains, @account.id),
  32. }
  33. statuses.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
  34. end
  35. end
  36. def register(status, at_time = Time.now.utc)
  37. add(status.proper, status.account_id, at_time) if eligible?(status)
  38. end
  39. def add(status, _account_id, at_time = Time.now.utc)
  40. # We rely on the total reblogs and favourites count, so we
  41. # don't record which account did the what and when here
  42. record_used_id(status.id, at_time)
  43. end
  44. def query
  45. Query.new(key_prefix, klass)
  46. end
  47. def refresh(at_time = Time.now.utc)
  48. statuses = Status.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq).includes(:account, :media_attachments)
  49. calculate_scores(statuses, at_time)
  50. trim_older_items
  51. end
  52. def request_review
  53. statuses = Status.where(id: currently_trending_ids(false, -1)).includes(:account)
  54. statuses.filter_map do |status|
  55. next unless would_be_trending?(status.id) && !status.trendable? && status.requires_review_notification?
  56. status.account.touch(:requested_review_at)
  57. status
  58. end
  59. end
  60. protected
  61. def key_prefix
  62. PREFIX
  63. end
  64. def klass
  65. Status
  66. end
  67. private
  68. def eligible?(status)
  69. original_status = status.proper
  70. original_status.public_visibility? &&
  71. original_status.account.discoverable? && !original_status.account.silenced? &&
  72. original_status.spoiler_text.blank? && !original_status.sensitive? && !original_status.reply?
  73. end
  74. def calculate_scores(statuses, at_time)
  75. redis.pipelined do
  76. statuses.each do |status|
  77. expected = 1.0
  78. observed = (status.reblogs_count + status.favourites_count).to_f
  79. score = begin
  80. if expected > observed || observed < options[:threshold]
  81. 0
  82. else
  83. ((observed - expected)**2) / expected
  84. end
  85. end
  86. decaying_score = score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f))
  87. add_to_and_remove_from_subsets(status.id, decaying_score, {
  88. all: true,
  89. allowed: status.trendable? && status.account.discoverable?,
  90. })
  91. next unless valid_locale?(status.language)
  92. add_to_and_remove_from_subsets(status.id, decaying_score, {
  93. "all:#{status.language}" => true,
  94. "allowed:#{status.language}" => status.trendable? && status.account.discoverable?,
  95. })
  96. end
  97. # Clean up localized sets by calculating the intersection with the main
  98. # set. We do this instead of just deleting the localized sets to avoid
  99. # having moments where the API returns empty results
  100. Trends.available_locales.each do |locale|
  101. redis.zinterstore("#{key_prefix}:all:#{locale}", ["#{key_prefix}:all:#{locale}", "#{key_prefix}:all"], aggregate: 'max')
  102. redis.zinterstore("#{key_prefix}:allowed:#{locale}", ["#{key_prefix}:allowed:#{locale}", "#{key_prefix}:all"], aggregate: 'max')
  103. end
  104. end
  105. end
  106. def would_be_trending?(id)
  107. score(id) > score_at_rank(options[:review_threshold] - 1)
  108. end
  109. end