Refactor feed manager (#14761)
This commit is contained in:
		
							parent
							
								
									169f9105ef
								
							
						
					
					
						commit
						65760f59df
					
				
					 9 changed files with 236 additions and 124 deletions
				
			
		| 
						 | 
				
			
			@ -6,31 +6,54 @@ class FeedManager
 | 
			
		|||
  include Singleton
 | 
			
		||||
  include Redisable
 | 
			
		||||
 | 
			
		||||
  # Maximum number of items stored in a single feed
 | 
			
		||||
  MAX_ITEMS = 400
 | 
			
		||||
 | 
			
		||||
  # Must be <= MAX_ITEMS or the tracking sets will grow forever
 | 
			
		||||
  # Number of items in the feed since last reblog of status
 | 
			
		||||
  # before the new reblog will be inserted. Must be <= MAX_ITEMS
 | 
			
		||||
  # or the tracking sets will grow forever
 | 
			
		||||
  REBLOG_FALLOFF = 40
 | 
			
		||||
 | 
			
		||||
  # Execute block for every active account
 | 
			
		||||
  # @yield [Account]
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def with_active_accounts(&block)
 | 
			
		||||
    Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Redis key of a feed
 | 
			
		||||
  # @param [Symbol] type
 | 
			
		||||
  # @param [Integer] id
 | 
			
		||||
  # @param [Symbol] subtype
 | 
			
		||||
  # @return [String]
 | 
			
		||||
  def key(type, id, subtype = nil)
 | 
			
		||||
    return "feed:#{type}:#{id}" unless subtype
 | 
			
		||||
 | 
			
		||||
    "feed:#{type}:#{id}:#{subtype}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filter?(timeline_type, status, receiver_id)
 | 
			
		||||
    if timeline_type == :home
 | 
			
		||||
      filter_from_home?(status, receiver_id, build_crutches(receiver_id, [status]))
 | 
			
		||||
    elsif timeline_type == :mentions
 | 
			
		||||
      filter_from_mentions?(status, receiver_id)
 | 
			
		||||
  # Check if the status should not be added to a feed
 | 
			
		||||
  # @param [Symbol] timeline_type
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [Account|List] receiver
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def filter?(timeline_type, status, receiver)
 | 
			
		||||
    case timeline_type
 | 
			
		||||
    when :home
 | 
			
		||||
      filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]))
 | 
			
		||||
    when :list
 | 
			
		||||
      filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]))
 | 
			
		||||
    when :mentions
 | 
			
		||||
      filter_from_mentions?(status, receiver.id)
 | 
			
		||||
    else
 | 
			
		||||
      false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Add a status to a home feed and send a streaming API update
 | 
			
		||||
  # @param [Account] account
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def push_to_home(account, status)
 | 
			
		||||
    return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +62,10 @@ class FeedManager
 | 
			
		|||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Remove a status from a home feed and send a streaming API update
 | 
			
		||||
  # @param [Account] account
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def unpush_from_home(account, status)
 | 
			
		||||
    return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,21 +73,22 @@ class FeedManager
 | 
			
		|||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Add a status to a list feed and send a streaming API update
 | 
			
		||||
  # @param [List] list
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def push_to_list(list, status)
 | 
			
		||||
    if status.reply? && status.in_reply_to_account_id != status.account_id
 | 
			
		||||
      should_filter = status.in_reply_to_account_id != list.account_id
 | 
			
		||||
      should_filter &&= !list.show_all_replies?
 | 
			
		||||
      should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
 | 
			
		||||
      return false if should_filter
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
 | 
			
		||||
    return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
 | 
			
		||||
 | 
			
		||||
    trim(:list, list.id)
 | 
			
		||||
    PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Remove a status from a list feed and send a streaming API update
 | 
			
		||||
  # @param [List] list
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def unpush_from_list(list, status)
 | 
			
		||||
    return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -68,34 +96,11 @@ class FeedManager
 | 
			
		|||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def trim(type, account_id)
 | 
			
		||||
    timeline_key = key(type, account_id)
 | 
			
		||||
    reblog_key   = key(type, account_id, 'reblogs')
 | 
			
		||||
 | 
			
		||||
    # Remove any items past the MAX_ITEMS'th entry in our feed
 | 
			
		||||
    redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
 | 
			
		||||
 | 
			
		||||
    # Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
 | 
			
		||||
    # tracking anything after it for deduplication purposes.
 | 
			
		||||
    falloff_rank  = FeedManager::REBLOG_FALLOFF
 | 
			
		||||
    falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true)
 | 
			
		||||
    falloff_score = falloff_range&.first&.last&.to_i
 | 
			
		||||
 | 
			
		||||
    return if falloff_score.nil?
 | 
			
		||||
 | 
			
		||||
    # Get any reblogs we might have to clean up after.
 | 
			
		||||
    redis.zrangebyscore(reblog_key, 0, falloff_score).each do |reblogged_id|
 | 
			
		||||
      # Remove it from the set of reblogs we're tracking *first* to avoid races.
 | 
			
		||||
      redis.zrem(reblog_key, reblogged_id)
 | 
			
		||||
      # Just drop any set we might have created to track additional reblogs.
 | 
			
		||||
      # This means that if this reblog is deleted, we won't automatically insert
 | 
			
		||||
      # another reblog, but also that any new reblog can be inserted into the
 | 
			
		||||
      # feed.
 | 
			
		||||
      redis.del(key(type, account_id, "reblogs:#{reblogged_id}"))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def merge_into_timeline(from_account, into_account)
 | 
			
		||||
  # Fill a home feed with an account's statuses
 | 
			
		||||
  # @param [Account] from_account
 | 
			
		||||
  # @param [Account] into_account
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def merge_into_home(from_account, into_account)
 | 
			
		||||
    timeline_key = key(:home, into_account.id)
 | 
			
		||||
    aggregate    = into_account.user&.aggregates_reblogs?
 | 
			
		||||
    query        = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +122,37 @@ class FeedManager
 | 
			
		|||
    trim(:home, into_account.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unmerge_from_timeline(from_account, into_account)
 | 
			
		||||
  # Fill a list feed with an account's statuses
 | 
			
		||||
  # @param [Account] from_account
 | 
			
		||||
  # @param [List] list
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def merge_into_list(from_account, list)
 | 
			
		||||
    timeline_key = key(:list, list.id)
 | 
			
		||||
    aggregate    = list.account.user&.aggregates_reblogs?
 | 
			
		||||
    query        = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
 | 
			
		||||
 | 
			
		||||
    if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
 | 
			
		||||
      oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
 | 
			
		||||
      query = query.where('id > ?', oldest_home_score)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    statuses = query.to_a
 | 
			
		||||
    crutches = build_crutches(list.account_id, statuses)
 | 
			
		||||
 | 
			
		||||
    statuses.each do |status|
 | 
			
		||||
      next if filter_from_home?(status, list.account_id, crutches) || filter_from_list?(status, list)
 | 
			
		||||
 | 
			
		||||
      add_to_feed(:list, list.id, status, aggregate)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    trim(:list, list.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Remove an account's statuses from a home feed
 | 
			
		||||
  # @param [Account] from_account
 | 
			
		||||
  # @param [Account] into_account
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def unmerge_from_home(from_account, into_account)
 | 
			
		||||
    timeline_key      = key(:home, into_account.id)
 | 
			
		||||
    oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -126,14 +161,31 @@ class FeedManager
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def clear_from_timeline(account, target_account)
 | 
			
		||||
    # Clear from timeline all statuses from or mentionning target_account
 | 
			
		||||
  # Remove an account's statuses from a list feed
 | 
			
		||||
  # @param [Account] from_account
 | 
			
		||||
  # @param [List] list
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def unmerge_from_list(from_account, list)
 | 
			
		||||
    timeline_key      = key(:list, list.id)
 | 
			
		||||
    oldest_list_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 | 
			
		||||
 | 
			
		||||
    from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_list_score).reorder(nil).find_each do |status|
 | 
			
		||||
      remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Clear all statuses from or mentioning target_account from a home feed
 | 
			
		||||
  # @param [Account] account
 | 
			
		||||
  # @param [Account] target_account
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def clear_from_home(account, target_account)
 | 
			
		||||
    timeline_key        = key(:home, account.id)
 | 
			
		||||
    timeline_status_ids = redis.zrange(timeline_key, 0, -1)
 | 
			
		||||
    statuses            = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
 | 
			
		||||
    reblogged_ids       = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
 | 
			
		||||
    with_mentions_ids   = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
 | 
			
		||||
    target_statuses     = statuses.filter do |status|
 | 
			
		||||
 | 
			
		||||
    target_statuses = statuses.select do |status|
 | 
			
		||||
      status.account_id == target_account.id || reblogged_ids.include?(status.reblog_of_id) || with_mentions_ids.include?(status.id) || with_mentions_ids.include?(status.reblog_of_id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -142,7 +194,10 @@ class FeedManager
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def populate_feed(account)
 | 
			
		||||
  # Populate home feed of account from scratch
 | 
			
		||||
  # @param [Account] account
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def populate_home(account)
 | 
			
		||||
    limit        = FeedManager::MAX_ITEMS / 2
 | 
			
		||||
    aggregate    = account.user&.aggregates_reblogs?
 | 
			
		||||
    timeline_key = key(:home, account.id)
 | 
			
		||||
| 
						 | 
				
			
			@ -177,15 +232,59 @@ class FeedManager
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def push_update_required?(timeline_id)
 | 
			
		||||
    redis.exists?("subscribed:#{timeline_id}")
 | 
			
		||||
  # Trim a feed to maximum size by removing older items
 | 
			
		||||
  # @param [Symbol] type
 | 
			
		||||
  # @param [Integer] timeline_id
 | 
			
		||||
  # @return [void]
 | 
			
		||||
  def trim(type, timeline_id)
 | 
			
		||||
    timeline_key = key(type, timeline_id)
 | 
			
		||||
    reblog_key   = key(type, timeline_id, 'reblogs')
 | 
			
		||||
 | 
			
		||||
    # Remove any items past the MAX_ITEMS'th entry in our feed
 | 
			
		||||
    redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
 | 
			
		||||
 | 
			
		||||
    # Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
 | 
			
		||||
    # tracking anything after it for deduplication purposes.
 | 
			
		||||
    falloff_rank  = FeedManager::REBLOG_FALLOFF
 | 
			
		||||
    falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true)
 | 
			
		||||
    falloff_score = falloff_range&.first&.last&.to_i
 | 
			
		||||
 | 
			
		||||
    return if falloff_score.nil?
 | 
			
		||||
 | 
			
		||||
    # Get any reblogs we might have to clean up after.
 | 
			
		||||
    redis.zrangebyscore(reblog_key, 0, falloff_score).each do |reblogged_id|
 | 
			
		||||
      # Remove it from the set of reblogs we're tracking *first* to avoid races.
 | 
			
		||||
      redis.zrem(reblog_key, reblogged_id)
 | 
			
		||||
      # Just drop any set we might have created to track additional reblogs.
 | 
			
		||||
      # This means that if this reblog is deleted, we won't automatically insert
 | 
			
		||||
      # another reblog, but also that any new reblog can be inserted into the
 | 
			
		||||
      # feed.
 | 
			
		||||
      redis.del(key(type, timeline_id, "reblogs:#{reblogged_id}"))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Check if there is a streaming API client connected
 | 
			
		||||
  # for the given feed
 | 
			
		||||
  # @param [String] timeline_key
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def push_update_required?(timeline_key)
 | 
			
		||||
    redis.exists?("subscribed:#{timeline_key}")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Check if the account is blocking or muting any of the given accounts
 | 
			
		||||
  # @param [Integer] receiver_id
 | 
			
		||||
  # @param [Array<Integer>] account_ids
 | 
			
		||||
  # @param [Symbol] context
 | 
			
		||||
  def blocks_or_mutes?(receiver_id, account_ids, context)
 | 
			
		||||
    Block.where(account_id: receiver_id, target_account_id: account_ids).any? ||
 | 
			
		||||
      (context == :home ? Mute.where(account_id: receiver_id, target_account_id: account_ids).any? : Mute.where(account_id: receiver_id, target_account_id: account_ids, hide_notifications: true).any?)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Check if status should not be added to the home feed
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [Integer] receiver_id
 | 
			
		||||
  # @param [Hash] crutches
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def filter_from_home?(status, receiver_id, crutches)
 | 
			
		||||
    return false if receiver_id == status.account_id
 | 
			
		||||
    return true  if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
 | 
			
		||||
| 
						 | 
				
			
			@ -218,6 +317,11 @@ class FeedManager
 | 
			
		|||
    false
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Check if status should not be added to the mentions feed
 | 
			
		||||
  # @see NotifyService
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [Integer] receiver_id
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def filter_from_mentions?(status, receiver_id)
 | 
			
		||||
    return true if receiver_id == status.account_id
 | 
			
		||||
    return true if phrase_filtered?(status, receiver_id, :notifications)
 | 
			
		||||
| 
						 | 
				
			
			@ -234,6 +338,27 @@ class FeedManager
 | 
			
		|||
    should_filter
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Check if status should not be added to the list feed
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [List] list
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def filter_from_list?(status, list)
 | 
			
		||||
    if status.reply? && status.in_reply_to_account_id != status.account_id
 | 
			
		||||
      should_filter = status.in_reply_to_account_id != list.account_id
 | 
			
		||||
      should_filter &&= !list.show_all_replies?
 | 
			
		||||
      should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
 | 
			
		||||
 | 
			
		||||
      return !!should_filter
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    false
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Check if the status hits a phrase filter
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [Integer] receiver_id
 | 
			
		||||
  # @param [Symbol] context
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def phrase_filtered?(status, receiver_id, context)
 | 
			
		||||
    active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -269,6 +394,11 @@ class FeedManager
 | 
			
		|||
  # added, and false if it was not added to the feed. Note that this is
 | 
			
		||||
  # an internal helper: callers must call trim or push updates if
 | 
			
		||||
  # either action is appropriate.
 | 
			
		||||
  # @param [Symbol] timeline_type
 | 
			
		||||
  # @param [Integer] account_id
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [Boolean] aggregate_reblogs
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
 | 
			
		||||
    timeline_key = key(timeline_type, account_id)
 | 
			
		||||
    reblog_key   = key(timeline_type, account_id, 'reblogs')
 | 
			
		||||
| 
						 | 
				
			
			@ -312,6 +442,11 @@ class FeedManager
 | 
			
		|||
  # with reblogs, and returning true if a status was removed. As with
 | 
			
		||||
  # `add_to_feed`, this does not trigger push updates, so callers must
 | 
			
		||||
  # do so if appropriate.
 | 
			
		||||
  # @param [Symbol] timeline_type
 | 
			
		||||
  # @param [Integer] account_id
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
  # @param [Boolean] aggregate_reblogs
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true)
 | 
			
		||||
    timeline_key = key(timeline_type, account_id)
 | 
			
		||||
    reblog_key   = key(timeline_type, account_id, 'reblogs')
 | 
			
		||||
| 
						 | 
				
			
			@ -346,6 +481,11 @@ class FeedManager
 | 
			
		|||
    redis.zrem(timeline_key, status.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Pre-fetch various objects and relationships for given statuses that
 | 
			
		||||
  # are going to be checked by the filtering methods
 | 
			
		||||
  # @param [Integer] receiver_id
 | 
			
		||||
  # @param [Array<Status>] statuses
 | 
			
		||||
  # @return [Hash]
 | 
			
		||||
  def build_crutches(receiver_id, statuses)
 | 
			
		||||
    crutches = {}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ class AfterBlockService < BaseService
 | 
			
		|||
  private
 | 
			
		||||
 | 
			
		||||
  def clear_home_feed!
 | 
			
		||||
    FeedManager.instance.clear_from_timeline(@account, @target_account)
 | 
			
		||||
    FeedManager.instance.clear_from_home(@account, @target_account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def clear_conversations!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,15 +13,13 @@ class NotifyService < BaseService
 | 
			
		|||
    push_to_conversation! if direct_message?
 | 
			
		||||
    send_email! if email_enabled?
 | 
			
		||||
  rescue ActiveRecord::RecordInvalid
 | 
			
		||||
    # rubocop:disable Style/RedundantReturn
 | 
			
		||||
    return
 | 
			
		||||
    # rubocop:enable Style/RedundantReturn
 | 
			
		||||
    nil
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def blocked_mention?
 | 
			
		||||
    FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
 | 
			
		||||
    FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def blocked_favourite?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
 | 
			
		||||
class PrecomputeFeedService < BaseService
 | 
			
		||||
  def call(account)
 | 
			
		||||
    FeedManager.instance.populate_feed(account)
 | 
			
		||||
    FeedManager.instance.populate_home(account)
 | 
			
		||||
  ensure
 | 
			
		||||
    Redis.current.del("account:#{account.id}:regeneration")
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,9 +27,12 @@ class FeedInsertWorker
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def feed_filtered?
 | 
			
		||||
    # Note: Lists are a variation of home, so the filtering rules
 | 
			
		||||
    # of home apply to both
 | 
			
		||||
    FeedManager.instance.filter?(:home, @status, @follower.id)
 | 
			
		||||
    case @type
 | 
			
		||||
    when :home
 | 
			
		||||
      FeedManager.instance.filter?(:home, @status, @follower)
 | 
			
		||||
    when :list
 | 
			
		||||
      FeedManager.instance.filter?(:list, @status, @list)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform_push
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ class MergeWorker
 | 
			
		|||
  sidekiq_options queue: 'pull'
 | 
			
		||||
 | 
			
		||||
  def perform(from_account_id, into_account_id)
 | 
			
		||||
    FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
 | 
			
		||||
    FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id))
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,8 @@ class MuteWorker
 | 
			
		|||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  def perform(account_id, target_account_id)
 | 
			
		||||
    FeedManager.instance.clear_from_timeline(
 | 
			
		||||
      Account.find(account_id),
 | 
			
		||||
      Account.find(target_account_id)
 | 
			
		||||
    )
 | 
			
		||||
    FeedManager.instance.clear_from_home(Account.find(account_id), Account.find(target_account_id))
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ class UnmergeWorker
 | 
			
		|||
  sidekiq_options queue: 'pull'
 | 
			
		||||
 | 
			
		||||
  def perform(from_account_id, into_account_id)
 | 
			
		||||
    FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
 | 
			
		||||
    FeedManager.instance.unmerge_from_home(Account.find(from_account_id), Account.find(into_account_id))
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,14 +29,14 @@ RSpec.describe FeedManager do
 | 
			
		|||
      it 'returns false for followee\'s status' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, status, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns false for reblog by followee' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: jeff)
 | 
			
		||||
        reblog = Fabricate(:status, reblog: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for reblog by followee of blocked account' do
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ RSpec.describe FeedManager do
 | 
			
		|||
        reblog = Fabricate(:status, reblog: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        bob.block!(jeff)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for reblog by followee of muted account' do
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +52,7 @@ RSpec.describe FeedManager do
 | 
			
		|||
        reblog = Fabricate(:status, reblog: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        bob.mute!(jeff)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for reblog by followee of someone who is blocking recipient' do
 | 
			
		||||
| 
						 | 
				
			
			@ -60,14 +60,14 @@ RSpec.describe FeedManager do
 | 
			
		|||
        reblog = Fabricate(:status, reblog: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        jeff.block!(bob)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for reblog from account with reblogs disabled' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: jeff)
 | 
			
		||||
        reblog = Fabricate(:status, reblog: status, account: alice)
 | 
			
		||||
        bob.follow!(alice, reblogs: false)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns false for reply by followee to another followee' do
 | 
			
		||||
| 
						 | 
				
			
			@ -75,48 +75,48 @@ RSpec.describe FeedManager do
 | 
			
		|||
        reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        bob.follow!(jeff)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns false for reply by followee to recipient' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: bob)
 | 
			
		||||
        reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns false for reply by followee to self' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: alice)
 | 
			
		||||
        reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for reply by followee to non-followed account' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: jeff)
 | 
			
		||||
        reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reply, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for the second reply by followee to a non-federated status' do
 | 
			
		||||
        reply        = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
 | 
			
		||||
        second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, second_reply, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, second_reply, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns false for status by followee mentioning another account' do
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        status = PostStatusService.new.call(alice, text: 'Hey @jeff')
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, status, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for status by followee mentioning blocked account' do
 | 
			
		||||
        bob.block!(jeff)
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        status = PostStatusService.new.call(alice, text: 'Hey @jeff')
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, status, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for reblog of a personally blocked domain' do
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +124,7 @@ RSpec.describe FeedManager do
 | 
			
		|||
        alice.follow!(jeff)
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: bob)
 | 
			
		||||
        reblog = Fabricate(:status, reblog: status, account: jeff)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'for irreversibly muted phrases' do
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +132,7 @@ RSpec.describe FeedManager do
 | 
			
		|||
          alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true)
 | 
			
		||||
          alice.follow!(jeff)
 | 
			
		||||
          status = Fabricate(:status, text: 'bobcats', account: jeff)
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice)).to be_falsy
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'returns true if phrase is contained' do
 | 
			
		||||
| 
						 | 
				
			
			@ -140,14 +140,14 @@ RSpec.describe FeedManager do
 | 
			
		|||
          alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
 | 
			
		||||
          alice.follow!(jeff)
 | 
			
		||||
          status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice)).to be true
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'matches substrings if whole_word is false' do
 | 
			
		||||
          alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true)
 | 
			
		||||
          alice.follow!(jeff)
 | 
			
		||||
          status = Fabricate(:status, text: 'shiitake', account: jeff)
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice)).to be true
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        it 'returns true if phrase is contained in a poll option' do
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ RSpec.describe FeedManager do
 | 
			
		|||
          alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
 | 
			
		||||
          alice.follow!(jeff)
 | 
			
		||||
          status = Fabricate(:status, text: 'what do you prefer', poll: Fabricate(:poll, options: %w(farts POP TARts)), account: jeff)
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
 | 
			
		||||
          expect(FeedManager.instance.filter?(:home, status, alice)).to be true
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -164,27 +164,27 @@ RSpec.describe FeedManager do
 | 
			
		|||
      it 'returns true for status that mentions blocked account' do
 | 
			
		||||
        bob.block!(jeff)
 | 
			
		||||
        status = PostStatusService.new.call(alice, text: 'Hey @jeff')
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for status that replies to a blocked account' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: jeff)
 | 
			
		||||
        reply  = Fabricate(:status, text: 'Nay', thread: status, account: alice)
 | 
			
		||||
        bob.block!(jeff)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, reply, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, reply, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns true for status by silenced account who recipient is not following' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: alice)
 | 
			
		||||
        alice.silence!
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns false for status by followed silenced account' do
 | 
			
		||||
        status = Fabricate(:status, text: 'Hello world', account: alice)
 | 
			
		||||
        alice.silence!
 | 
			
		||||
        bob.follow!(alice)
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
 | 
			
		||||
        expect(FeedManager.instance.filter?(:mentions, status, bob)).to be false
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -414,52 +414,20 @@ RSpec.describe FeedManager do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#merge_into_timeline' do
 | 
			
		||||
  describe '#merge_into_home' do
 | 
			
		||||
    it "does not push source account's statuses whose reblogs are already inserted" do
 | 
			
		||||
      account = Fabricate(:account, id: 0)
 | 
			
		||||
      reblog = Fabricate(:status)
 | 
			
		||||
      status = Fabricate(:status, reblog: reblog)
 | 
			
		||||
      FeedManager.instance.push_to_home(account, status)
 | 
			
		||||
 | 
			
		||||
      FeedManager.instance.merge_into_timeline(account, reblog.account)
 | 
			
		||||
      FeedManager.instance.merge_into_home(account, reblog.account)
 | 
			
		||||
 | 
			
		||||
      expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#trim' do
 | 
			
		||||
    let(:receiver) { Fabricate(:account) }
 | 
			
		||||
 | 
			
		||||
    it 'cleans up reblog tracking keys' do
 | 
			
		||||
      reblogged      = Fabricate(:status)
 | 
			
		||||
      status         = Fabricate(:status, reblog: reblogged)
 | 
			
		||||
      another_status = Fabricate(:status, reblog: reblogged)
 | 
			
		||||
      reblogs_key    = FeedManager.instance.key('home', receiver.id, 'reblogs')
 | 
			
		||||
      reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}")
 | 
			
		||||
 | 
			
		||||
      FeedManager.instance.push_to_home(receiver, status)
 | 
			
		||||
      FeedManager.instance.push_to_home(receiver, another_status)
 | 
			
		||||
 | 
			
		||||
      # We should have a tracking set and an entry in reblogs.
 | 
			
		||||
      expect(Redis.current.exists?(reblog_set_key)).to be true
 | 
			
		||||
      expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s]
 | 
			
		||||
 | 
			
		||||
      # Push everything past the reblog falloff.
 | 
			
		||||
      FeedManager::REBLOG_FALLOFF.times do
 | 
			
		||||
        FeedManager.instance.push_to_home(receiver, Fabricate(:status))
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # `trim` should be called automatically, but do it anyway, as
 | 
			
		||||
      # we're testing `trim`, not side effects of `push`.
 | 
			
		||||
      FeedManager.instance.trim('home', receiver.id)
 | 
			
		||||
 | 
			
		||||
      # We should not have any reblog tracking data.
 | 
			
		||||
      expect(Redis.current.exists?(reblog_set_key)).to be false
 | 
			
		||||
      expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#unpush' do
 | 
			
		||||
  describe '#unpush_from_home' do
 | 
			
		||||
    let(:receiver) { Fabricate(:account) }
 | 
			
		||||
 | 
			
		||||
    it 'leaves a reblogged status if original was on feed' do
 | 
			
		||||
| 
						 | 
				
			
			@ -525,7 +493,7 @@ RSpec.describe FeedManager do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#clear_from_timeline' do
 | 
			
		||||
  describe '#clear_from_home' do
 | 
			
		||||
    let(:account)          { Fabricate(:account) }
 | 
			
		||||
    let(:followed_account) { Fabricate(:account) }
 | 
			
		||||
    let(:target_account)   { Fabricate(:account) }
 | 
			
		||||
| 
						 | 
				
			
			@ -543,8 +511,8 @@ RSpec.describe FeedManager do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'correctly cleans the timeline' do
 | 
			
		||||
      FeedManager.instance.clear_from_timeline(account, target_account)
 | 
			
		||||
    it 'correctly cleans the home timeline' do
 | 
			
		||||
      FeedManager.instance.clear_from_home(account, target_account)
 | 
			
		||||
 | 
			
		||||
      expect(Redis.current.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s]
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue