diff options
Diffstat (limited to 'app/lib/feed_manager.rb')
-rw-r--r-- | app/lib/feed_manager.rb | 123 |
1 files changed, 102 insertions, 21 deletions
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 3c1f8d6e2..69009ffde 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -2,17 +2,18 @@ require 'singleton' +# rubocop:disable Metrics/ClassLength class FeedManager include Singleton include Redisable # Maximum number of items stored in a single feed - MAX_ITEMS = 400 + MAX_ITEMS = 1000 # 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 + REBLOG_FALLOFF = 50 # Execute block for every active account # @yield [Account] @@ -40,9 +41,9 @@ class FeedManager def filter?(timeline_type, status, receiver) case timeline_type when :home - filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status])) + filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), filter_options_for(receiver.id)) when :list - filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status])) + filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), filter_options_for(receiver.id)) when :mentions filter_from_mentions?(status, receiver.id) when :direct @@ -121,6 +122,25 @@ class FeedManager true end + def unpush_status(account, status) + return if account.blank? || status.blank? + + unpush_from_home(account, status) + unpush_from_direct(account, status) if status.direct_visibility? + + account.lists_for_local_distribution.select(:id, :account_id).each do |list| + unpush_from_list(list, status) + end + end + + def unpush_conversation(account, conversation) + return if account.blank? || conversation.blank? + + conversation.statuses.reorder(nil).find_each do |status| + unpush_status(account, status) + end + end + # Fill a home feed with an account's statuses # @param [Account] from_account # @param [Account] into_account @@ -137,9 +157,10 @@ class FeedManager statuses = query.to_a crutches = build_crutches(into_account.id, statuses) + filter_options = filter_options_for(into_account.id) statuses.each do |status| - next if filter_from_home?(status, into_account.id, crutches) + next if filter_from_home?(status, into_account.id, crutches, filter_options) add_to_feed(:home, into_account.id, status, aggregate) end @@ -242,11 +263,12 @@ class FeedManager next if last_status_score < oldest_home_score end - statuses = target_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(limit) + statuses = target_account.statuses.published.without_replies.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(limit) crutches = build_crutches(account.id, statuses) + filter_options = filter_options_for(account.id) statuses.each do |status| - next if filter_from_home?(status, account.id, crutches) + next if filter_from_home?(status, account.id, crutches, filter_options) add_to_feed(:home, account.id, status, aggregate) end @@ -270,6 +292,7 @@ class FeedManager statuses.each do |status| next if filter_from_direct?(status, account) + added += 1 if add_to_feed(:direct, account.id, status) end @@ -333,37 +356,75 @@ class FeedManager # @param [Status] status # @param [Integer] receiver_id # @param [Hash] crutches + # @param [Hash] filter_options # @return [Boolean] - def filter_from_home?(status, receiver_id, crutches) + def filter_from_home?(status, receiver_id, crutches, filter_options) + conversation = status.conversation + reblog_conversation = status.reblog&.conversation + return false if receiver_id == status.account_id + return true unless status.published? + return true if crutches[:hiding_thread][status.conversation_id] if conversation.present? return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) return true if phrase_filtered?(status, receiver_id, :home) check_for_blocks = crutches[:active_mentions][status.id] || [] - check_for_blocks.concat([status.account_id]) + check_for_blocks.concat([status.account_id, conversation&.account_id]) + check_for_blocks.concat([status.in_reply_to_account_id]) if status.reply? if status.reblog? - check_for_blocks.concat([status.reblog.account_id]) + check_for_blocks.concat([status.reblog.account_id, reblog_conversation&.account_id]) check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || []) + check_for_blocks.concat([status.reblog.in_reply_to_account_id]) if status.reblog.reply? end + check_for_blocks.uniq! + check_for_blocks.compact! return true if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } - if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply - should_filter = !crutches[:following][status.in_reply_to_account_id] # and I'm not following the person it's a reply to - should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me - should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply + # Filter if... + if status.reply? # ...it's a reply and... + # ...you're not following the author... + should_filter = !crutches[:following][status.in_reply_to_account_id] + # (optional) ...or the owner(s) of the thread... + should_filter ||= !crutches[:following][conversation.account_id] if filter_options[:to_unknown] && conversation&.account_id.present? + # ...and the author isn't replying to a post you wrote... + should_filter &&= receiver_id != status.in_reply_to_account_id + # ...and the author isn't mentioning you. + should_filter &&= !crutches[:active_mentions][receiver_id] return !!should_filter - elsif status.reblog? # Filter out a reblog - should_filter = crutches[:hiding_reblogs][status.account_id] # if the reblogger's reblogs are suppressed - should_filter ||= crutches[:blocked_by][status.reblog.account_id] # or if the author of the reblogged status is blocking me - should_filter ||= crutches[:domain_blocking][status.reblog.account.domain] # or the author's domain is blocked + elsif status.reblog? # ...it's a boost and... + should_filter = false + + # ...it's a reply... + if status.reblog.reply? && !status.reblog.in_reply_to_account_id.nil? + # ...and you don't follow the author if: + # - you're filtering replies to parent authors you don't follow + # - they're silenced on this server + should_filter ||= !crutches[:following][status.reblog.in_reply_to_account_id] if filter_options[:to_unknown] || status.reblog.in_reply_to_account.silenced? + # - you're filtering replies to threads whose owners you don't follow + should_filter ||= !crutches[:following][reblog_conversation.account_id] if filter_options[:to_unknown] && reblog_conversation&.account_id.present? + # ...or you're blocking their domain... + should_filter ||= crutches[:domain_blocking][status.reblog.thread.account.domain] if status.reblog.thread.present? + end + + # ...or it's a post from a thread's trunk and you don't follow the author if: + # - you're filtering boosts of authors you don't follow + # - they're silenced on this server + should_filter ||= !crutches[:following][status.reblog.account_id] if filter_options[:from_unknown] || status.reblog.account.silenced? + + # ..or you're hiding boosts from them... + should_filter ||= crutches[:hiding_reblogs][status.account_id] + # ...or they're blocking you... + should_filter ||= crutches[:blocked_by][status.reblog.account_id] + # ...or you're blocking their domain... + should_filter ||= crutches[:domain_blocking][status.reblog.account.domain] return !!should_filter end - false + crutches[:following][status.account_id] end # Check if status should not be added to the mentions feed @@ -381,18 +442,25 @@ class FeedManager check_for_blocks = status.active_mentions.pluck(:account_id) check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil? - should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) - should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them + should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) + should_filter ||= (status.account.silenced? && !relationship_exists?(receiver_id, status.account_id)) should_filter end + def relationship_exists?(account_id, target_account_id) + Follow.where(account_id: account_id, target_account_id: target_account_id) + .or(Follow.where(account_id: target_account_id, target_account_id: account_id)) + .exists? + end + # Check if status should not be added to the linear direct message feed # @param [Status] status # @param [Integer] receiver_id # @return [Boolean] def filter_from_direct?(status, receiver_id) return false if receiver_id == status.account_id + filter_from_mentions?(status, receiver_id) end @@ -539,6 +607,17 @@ class FeedManager redis.zrem(timeline_key, status.id) end + def filter_options_for(receiver_id) + Rails.cache.fetch("filter_settings:#{receiver_id}", expires_in: 1.month) do + return {} if (settings = User.find_by(account_id: receiver_id)&.settings).blank? + + { + to_unknown: settings.filter_to_unknown, + from_unknown: settings.filter_from_unknown, + } + end + end + # Pre-fetch various objects and relationships for given statuses that # are going to be checked by the filtering methods # @param [Integer] receiver_id @@ -567,7 +646,9 @@ class FeedManager crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.reblog&.account&.domain }.compact).pluck(:domain).each_with_object({}) { |domain, mapping| mapping[domain] = true } crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:hiding_thread] = ConversationMute.where(account_id: receiver_id, conversation_id: statuses.map(&:conversation_id).compact).pluck(:conversation_id).each_with_object({}) { |id, mapping| mapping[id] = true } crutches end end +# rubocop:enable Metrics/ClassLength |