forked from treehouse/mastodon
Refactor feed manager (#14761)
parent
169f9105ef
commit
65760f59df
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue