about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/concerns/account_interactions.rb11
-rw-r--r--app/models/favourite.rb25
-rw-r--r--app/models/glitch/keyword_mute.rb72
-rw-r--r--app/models/status.rb51
-rw-r--r--app/models/trending_tags.rb23
5 files changed, 124 insertions, 58 deletions
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index a064248d9..d067415fd 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -187,4 +187,15 @@ module AccountInteractions
   def pinned?(status)
     status_pins.where(status: status).exists?
   end
+
+  def followers_for_local_distribution
+    followers.local
+             .joins(:user)
+             .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
+  end
+
+  def lists_for_local_distribution
+    lists.joins(account: :user)
+         .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
+  end
 end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index c998a67eb..0fce82f6f 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -16,7 +16,7 @@ class Favourite < ApplicationRecord
   update_index('statuses#status', :status) if Chewy.enabled?
 
   belongs_to :account, inverse_of: :favourites
-  belongs_to :status,  inverse_of: :favourites, counter_cache: true
+  belongs_to :status,  inverse_of: :favourites
 
   has_one :notification, as: :activity, dependent: :destroy
 
@@ -25,4 +25,27 @@ class Favourite < ApplicationRecord
   before_validation do
     self.status = status.reblog if status&.reblog?
   end
+
+  after_create :increment_cache_counters
+  after_destroy :decrement_cache_counters
+
+  private
+
+  def increment_cache_counters
+    if association(:status).loaded?
+      status.update_attribute(:favourites_count, status.favourites_count + 1)
+    else
+      Status.where(id: status_id).update_all('favourites_count = COALESCE(favourites_count, 0) + 1')
+    end
+  end
+
+  def decrement_cache_counters
+    return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?)
+
+    if association(:status).loaded?
+      status.update_attribute(:favourites_count, [status.favourites_count - 1, 0].max)
+    else
+      Status.where(id: status_id).update_all('favourites_count = GREATEST(COALESCE(favourites_count, 0) - 1, 0)')
+    end
+  end
 end
diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb
index 17ebc5b5e..7d0002afd 100644
--- a/app/models/glitch/keyword_mute.rb
+++ b/app/models/glitch/keyword_mute.rb
@@ -33,68 +33,74 @@ class Glitch::KeywordMute < ApplicationRecord
     Rails.cache.delete(TagMatcher.cache_key(account_id))
   end
 
-  class RegexpMatcher
+  class CachedKeywordMute
+    attr_reader :keyword
+    attr_reader :whole_word
+
+    def initialize(keyword, whole_word)
+      @keyword = keyword
+      @whole_word = whole_word
+    end
+
+    def boundary_regex_for_keyword
+      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
+      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
+
+      /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
+    end
+
+    def matches?(str)
+      str =~ (whole_word ? boundary_regex_for_keyword : /#{Regexp.escape(keyword)}/i)
+    end
+  end
+
+  class Matcher
     attr_reader :account_id
-    attr_reader :regex
+    attr_reader :words
 
     def initialize(account_id)
       @account_id = account_id
-      regex_text = Rails.cache.fetch(self.class.cache_key(account_id)) { make_regex_text }
-      @regex = /#{regex_text}/
+      @words = Rails.cache.fetch(self.class.cache_key(account_id)) { fetch_keywords }
     end
 
     protected
 
-    def keywords
-      Glitch::KeywordMute.where(account_id: account_id).pluck(:whole_word, :keyword)
+    def fetch_keywords
+      Glitch::KeywordMute.where(account_id: account_id).pluck(:whole_word, :keyword).map do |whole_word, keyword|
+        CachedKeywordMute.new(transform_keyword(keyword), whole_word)
+      end
     end
 
-    def boundary_regex_for_keyword(keyword)
-      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
-      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
-
-      /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
+    def transform_keyword(keyword)
+      keyword
     end
   end
 
-  class TextMatcher < RegexpMatcher
+  class TextMatcher < Matcher
     def self.cache_key(account_id)
       format('keyword_mutes:regex:text:%s', account_id)
     end
 
     def matches?(str)
-      !!(regex =~ str)
-    end
-
-    private
-
-    def make_regex_text
-      kws = keywords.map! do |whole_word, keyword|
-        whole_word ? boundary_regex_for_keyword(keyword) : /(?i:#{Regexp.escape(keyword)})/
-      end
-
-      Regexp.union(kws).source
+      words.any? { |w| w.matches?(str) }
     end
   end
 
-  class TagMatcher < RegexpMatcher
+  class TagMatcher < Matcher
     def self.cache_key(account_id)
       format('keyword_mutes:regex:tag:%s', account_id)
     end
 
     def matches?(tags)
-      tags.pluck(:name).any? { |n| regex =~ n }
+      tags.pluck(:name).any? do |n|
+        words.any? { |w| w.matches?(n) }
+      end
     end
 
-    private
-
-    def make_regex_text
-      kws = keywords.map! do |whole_word, keyword|
-        term = (Tag::HASHTAG_RE =~ keyword) ? $1 : keyword
-        whole_word ? boundary_regex_for_keyword(term) : term
-      end
+    protected
 
-      Regexp.union(kws).source
+    def transform_keyword(kw)
+      Tag::HASHTAG_RE =~ kw ? $1 : kw
     end
   end
 end
diff --git a/app/models/status.rb b/app/models/status.rb
index c6d6453df..69fae2eb6 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -43,12 +43,12 @@ class Status < ApplicationRecord
 
   belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
 
-  belongs_to :account, inverse_of: :statuses, counter_cache: true
+  belongs_to :account, inverse_of: :statuses
   belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
   belongs_to :conversation, optional: true
 
   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
-  belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, counter_cache: :reblogs_count, optional: true
+  belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
 
   has_many :favourites, inverse_of: :status, dependent: :destroy
   has_many :bookmarks, inverse_of: :status, dependent: :destroy
@@ -172,6 +172,17 @@ class Status < ApplicationRecord
     @emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
   end
 
+  def mark_for_mass_destruction!
+    @marked_for_mass_destruction = true
+  end
+
+  def marked_for_mass_destruction?
+    @marked_for_mass_destruction
+  end
+
+  after_create  :increment_counter_caches
+  after_destroy :decrement_counter_caches
+
   after_create_commit :store_uri, if: :local?
   after_create_commit :update_statistics, if: :local?
 
@@ -414,4 +425,40 @@ class Status < ApplicationRecord
     return unless public_visibility? || unlisted_visibility?
     ActivityTracker.increment('activity:statuses:local')
   end
+
+  def increment_counter_caches
+    return if direct_visibility?
+
+    if association(:account).loaded?
+      account.update_attribute(:statuses_count, account.statuses_count + 1)
+    else
+      Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1')
+    end
+
+    return unless reblog?
+
+    if association(:reblog).loaded?
+      reblog.update_attribute(:reblogs_count, reblog.reblogs_count + 1)
+    else
+      Status.where(id: reblog_of_id).update_all('reblogs_count = COALESCE(reblogs_count, 0) + 1')
+    end
+  end
+
+  def decrement_counter_caches
+    return if direct_visibility? || marked_for_mass_destruction?
+
+    if association(:account).loaded?
+      account.update_attribute(:statuses_count, [account.statuses_count - 1, 0].max)
+    else
+      Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)')
+    end
+
+    return unless reblog?
+
+    if association(:reblog).loaded?
+      reblog.update_attribute(:reblogs_count, [reblog.reblogs_count - 1, 0].max)
+    else
+      Status.where(id: reblog_of_id).update_all('reblogs_count = GREATEST(COALESCE(reblogs_count, 0) - 1, 0)')
+    end
+  end
 end
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index eedd92644..c3641d7fd 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -1,33 +1,18 @@
 # frozen_string_literal: true
 
 class TrendingTags
-  KEY                  = 'trending_tags'
-  HALF_LIFE            = 1.day.to_i
-  MAX_ITEMS            = 500
   EXPIRE_HISTORY_AFTER = 7.days.seconds
 
   class << self
     def record_use!(tag, account, at_time = Time.now.utc)
-      return if disallowed_hashtags.include?(tag.name) || account.silenced?
+      return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot?
 
-      increment_vote!(tag.id, at_time)
       increment_historical_use!(tag.id, at_time)
       increment_unique_use!(tag.id, account.id, at_time)
     end
 
-    def get(limit)
-      tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i)
-      tags    = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h
-      tag_ids.map { |tag_id| tags[tag_id] }.compact
-    end
-
     private
 
-    def increment_vote!(tag_id, at_time)
-      redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s)
-      redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS)
-    end
-
     def increment_historical_use!(tag_id, at_time)
       key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}"
       redis.incrby(key, 1)
@@ -40,12 +25,6 @@ class TrendingTags
       redis.expire(key, EXPIRE_HISTORY_AFTER)
     end
 
-    # The epoch needs to be 2.5 years in the future if the half-life is one day
-    # While dynamic, it will always be the same within one year
-    def epoch
-      @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i
-    end
-
     def disallowed_hashtags
       return @disallowed_hashtags if defined?(@disallowed_hashtags)