end
def public_statuses
- statuses = public_timeline_statuses
-
- statuses = statuses.not_local_only unless truthy_param?(:local) || truthy_param?(:allow_local_only)
-
- if truthy_param?(:only_media)
- statuses.joins(:media_attachments).group(:id)
- else
- statuses
- end
+ public_feed.get(
+ limit_param(DEFAULT_STATUSES_LIMIT),
+ params[:max_id],
+ params[:since_id],
+ params[:min_id]
+ )
end
- def public_timeline_statuses
- Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
+ def public_feed
+ PublicFeed.new(
+ current_account,
+ local: truthy_param?(:local),
+ remote: truthy_param?(:remote),
- only_media: truthy_param?(:only_media)
++ only_media: truthy_param?(:only_media),
++ allow_local_only: truthy_param?(:allow_local_only)
+ )
end
def insert_pagination_headers
"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)
- elsif timeline_type == :direct
- filter_from_direct?(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)
++ when :direct
++ filter_from_direct?(status, receiver.id)
else
false
end
true
end
- redis.publish("timeline:direct:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
- end
++ # Add a status to a linear direct message feed and send a streaming API update
++ # @param [Account] account
++ # @param [Status] status
++ # @return [Boolean]
+ def push_to_direct(account, status)
+ return false unless add_to_feed(:direct, account.id, status)
++
+ trim(:direct, account.id)
+ PushUpdateWorker.perform_async(account.id, status.id, "timeline:direct:#{account.id}")
+ true
+ end
+
++ # Remove a status from a linear direct message feed and send a streaming API update
++ # @param [List] list
++ # @param [Status] status
++ # @return [Boolean]
+ def unpush_from_direct(account, status)
+ return false unless remove_from_feed(:direct, account.id, status)
- 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 - 1
- falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true)
- falloff_score = falloff_range&.first&.last&.to_i || 0
-
- # 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
+
- def merge_into_timeline(from_account, into_account)
++ redis.publish("timeline:direct:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
++ true
+ end
+
+ # 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)
end
end
++ # Populate direct feed of account from scratch
++ # @param [Account] account
++ # @return [void]
+ def populate_direct_feed(account)
+ added = 0
+ limit = FeedManager::MAX_ITEMS / 2
+ max_id = nil
+
+ loop do
+ statuses = Status.as_direct_timeline(account, limit, max_id)
+
+ break if statuses.empty?
+
+ statuses.each do |status|
+ next if filter_from_direct?(status, account)
+ added += 1 if add_to_feed(:direct, account.id, status)
+ end
+
+ break unless added.zero?
+
+ max_id = statuses.last.id
+ end
+ end
+
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?)
should_filter
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
+
+ # 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
--- /dev/null
+ # frozen_string_literal: true
+
+ class PublicFeed < Feed
+ # @param [Account] account
+ # @param [Hash] options
+ # @option [Boolean] :with_replies
+ # @option [Boolean] :with_reblogs
+ # @option [Boolean] :local
+ # @option [Boolean] :remote
+ # @option [Boolean] :only_media
++ # @option [Boolean] :allow_local_only
+ def initialize(account, options = {})
+ @account = account
+ @options = options
+ end
+
+ # @param [Integer] limit
+ # @param [Integer] max_id
+ # @param [Integer] since_id
+ # @param [Integer] min_id
+ # @return [Array<Status>]
+ def get(limit, max_id = nil, since_id = nil, min_id = nil)
+ scope = public_scope
+
++ scope.merge!(without_local_only_scope) unless allow_local_only?
+ scope.merge!(without_replies_scope) unless with_replies?
+ scope.merge!(without_reblogs_scope) unless with_reblogs?
+ scope.merge!(local_only_scope) if local_only?
+ scope.merge!(remote_only_scope) if remote_only?
+ scope.merge!(account_filters_scope) if account?
+ scope.merge!(media_only_scope) if media_only?
+
+ scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
+ end
+
+ private
+
++ def allow_local_only?
++ local_account? && (local_only? || @options[:allow_local_only])
++ end
++
+ def with_reblogs?
+ @options[:with_reblogs]
+ end
+
+ def with_replies?
+ @options[:with_replies]
+ end
+
+ def local_only?
+ @options[:local]
+ end
+
+ def remote_only?
+ @options[:remote]
+ end
+
+ def account?
+ @account.present?
+ end
+
++ def local_account?
++ @account&.local?
++ end
++
+ def media_only?
+ @options[:only_media]
+ end
+
+ def public_scope
+ Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
+ end
+
+ def local_only_scope
+ Status.local
+ end
+
+ def remote_only_scope
+ Status.remote
+ end
+
+ def without_replies_scope
+ Status.without_replies
+ end
+
+ def without_reblogs_scope
+ Status.without_reblogs
+ end
+
+ def media_only_scope
+ Status.joins(:media_attachments).group(:id)
+ end
+
++ def without_local_only_scope
++ Status.not_local_only
++ end
++
+ def account_filters_scope
+ Status.not_excluded_by_account(@account).tap do |scope|
+ scope.merge!(Status.not_domain_blocked_by_account(@account)) unless local_only?
+ scope.merge!(Status.in_chosen_languages(@account)) if @account.chosen_languages.present?
+ end
+ end
+ end
visibilities.keys - %w(direct limited)
end
- def as_public_timeline(account = nil, local_only = false)
- query = timeline_scope(local_only)
- query = query.without_replies unless Setting.show_replies_in_public_timelines
-
- apply_timeline_filters(query, account, [:local, true].include?(local_only))
- end
-
- def as_tag_timeline(tag, account = nil, local_only = false)
- query = timeline_scope(local_only).tagged_with(tag)
-
- apply_timeline_filters(query, account, local_only)
- end
-
- def as_outbox_timeline(account)
- where(account: account, visibility: :public)
- end
-
+ def in_chosen_languages(account)
+ where(language: nil).or where(language: account.chosen_languages)
+ end
+
+ def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false)
+ # direct timeline is mix of direct message from_me and to_me.
+ # 2 queries are executed with pagination.
+ # constant expression using arel_table is required for partial index
+
+ # _from_me part does not require any timeline filters
+ query_from_me = where(account_id: account.id)
+ .where(Status.arel_table[:visibility].eq(3))
+ .limit(limit)
+ .order('statuses.id DESC')
+
+ # _to_me part requires mute and block filter.
+ # FIXME: may we check mutes.hide_notifications?
+ query_to_me = Status
+ .joins(:mentions)
+ .merge(Mention.where(account_id: account.id))
+ .where(Status.arel_table[:visibility].eq(3))
+ .limit(limit)
+ .order('mentions.status_id DESC')
+ .not_excluded_by_account(account)
+
+ if max_id.present?
+ query_from_me = query_from_me.where('statuses.id < ?', max_id)
+ query_to_me = query_to_me.where('mentions.status_id < ?', max_id)
+ end
+
+ if since_id.present?
+ query_from_me = query_from_me.where('statuses.id > ?', since_id)
+ query_to_me = query_to_me.where('mentions.status_id > ?', since_id)
+ end
+
+ if cache_ids
+ # returns array of cache_ids object that have id and updated_at
+ (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit)
+ else
+ # returns ActiveRecord.Relation
+ items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit)
+ Status.where(id: items.map(&:id))
+ end
+ end
+
def favourites_map(status_ids, account_id)
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
end
status&.distributable? ? status : nil
end.compact
end
-
- private
-
- def timeline_scope(scope = false)
- starting_scope = case scope
- when :local, true
- Status.local
- when :remote
- Status.remote
- else
- Status
- end
- starting_scope = starting_scope.with_public_visibility
- if Setting.show_reblogs_in_public_timelines
- starting_scope
- else
- starting_scope.without_reblogs
- end
- end
-
- def apply_timeline_filters(query, account, local_only)
- if account.nil?
- filter_timeline_default(query)
- else
- filter_timeline_for_account(query, account, local_only)
- end
- end
-
- def filter_timeline_for_account(query, account, local_only)
- query = query.not_excluded_by_account(account)
- query = query.not_domain_blocked_by_account(account) unless local_only
- query = query.in_chosen_languages(account) if account.chosen_languages.present?
- query.merge(account_silencing_filter(account))
- end
-
- def filter_timeline_default(query)
- query.not_local_only.excluding_silenced_accounts
- end
-
- def account_silencing_filter(account)
- if account.silenced?
- including_myself = left_outer_joins(:account).where(account_id: account.id).references(:accounts)
- excluding_silenced_accounts.or(including_myself)
- else
- excluding_silenced_accounts
- end
- end
end
+ def marked_local_only?
+ # match both with and without U+FE0F (the emoji variation selector)
+ /#{local_only_emoji}\ufe0f?\z/.match?(content)
+ end
+
+ def local_only_emoji
+ '👁'
+ end
+
def status_stat
super || build_status_stat
end
--- /dev/null
+ # frozen_string_literal: true
+
+ class TagFeed < PublicFeed
+ LIMIT_PER_MODE = 4
+
+ # @param [Tag] tag
+ # @param [Account] account
+ # @param [Hash] options
+ # @option [Enumerable<String>] :any
+ # @option [Enumerable<String>] :all
+ # @option [Enumerable<String>] :none
+ # @option [Boolean] :local
+ # @option [Boolean] :remote
+ # @option [Boolean] :only_media
+ def initialize(tag, account, options = {})
+ @tag = tag
+ @account = account
+ @options = options
+ end
+
+ # @param [Integer] limit
+ # @param [Integer] max_id
+ # @param [Integer] since_id
+ # @param [Integer] min_id
+ # @return [Array<Status>]
+ def get(limit, max_id = nil, since_id = nil, min_id = nil)
+ scope = public_scope
+
++ scope.merge!(without_local_only_scope) unless local_account?
+ scope.merge!(tagged_with_any_scope)
+ scope.merge!(tagged_with_all_scope)
+ scope.merge!(tagged_with_none_scope)
+ scope.merge!(local_only_scope) if local_only?
+ scope.merge!(remote_only_scope) if remote_only?
+ scope.merge!(account_filters_scope) if account?
+ scope.merge!(media_only_scope) if media_only?
+
+ scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
+ end
+
+ private
+
+ def tagged_with_any_scope
+ Status.group(:id).tagged_with(tags_for(Array(@tag.name) | Array(@options[:any])))
+ end
+
+ def tagged_with_all_scope
+ Status.group(:id).tagged_with_all(tags_for(@options[:all]))
+ end
+
+ def tagged_with_none_scope
+ Status.group(:id).tagged_with_none(tags_for(@options[:none]))
+ end
+
+ def tags_for(names)
+ Tag.matching_name(Array(names).take(LIMIT_PER_MODE)) if names.present?
+ end
+ end
class PrecomputeFeedService < BaseService
def call(account)
- FeedManager.instance.populate_feed(account)
+ FeedManager.instance.populate_home(account)
+ FeedManager.instance.populate_direct_feed(account)
ensure
Redis.current.del("account:#{account.id}:regeneration")
end
end
def feed_filtered?
- # Note: Lists are a variation of home, so the filtering rules
- # of home apply to both
case @type
- when :home, :list
- FeedManager.instance.filter?(:home, @status, @follower.id)
+ when :home
+ FeedManager.instance.filter?(:home, @status, @follower)
+ when :list
+ FeedManager.instance.filter?(:list, @status, @list)
+ when :direct
- FeedManager.instance.filter?(:direct, @status, @account.id)
++ FeedManager.instance.filter?(:direct, @status, @account)
end
end
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 status by followee mentioning muted account' do
+ bob.mute!(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
--- /dev/null
+ require 'rails_helper'
+
+ RSpec.describe PublicFeed, type: :model do
+ let(:account) { Fabricate(:account) }
+
+ describe '#get' do
+ subject { described_class.new(nil).get(20).map(&:id) }
+
+ it 'only includes statuses with public visibility' do
+ public_status = Fabricate(:status, visibility: :public)
+ private_status = Fabricate(:status, visibility: :private)
+
+ expect(subject).to include(public_status.id)
+ expect(subject).not_to include(private_status.id)
+ end
+
+ it 'does not include replies' do
+ status = Fabricate(:status)
+ reply = Fabricate(:status, in_reply_to_id: status.id)
+
+ expect(subject).to include(status.id)
+ expect(subject).not_to include(reply.id)
+ end
+
+ it 'does not include boosts' do
+ status = Fabricate(:status)
+ boost = Fabricate(:status, reblog_of_id: status.id)
+
+ expect(subject).to include(status.id)
+ expect(subject).not_to include(boost.id)
+ end
+
+ it 'filters out silenced accounts' do
+ account = Fabricate(:account)
+ silenced_account = Fabricate(:account, silenced: true)
+ status = Fabricate(:status, account: account)
+ silenced_status = Fabricate(:status, account: silenced_account)
+
+ expect(subject).to include(status.id)
+ expect(subject).not_to include(silenced_status.id)
+ end
+
+ context 'without local_only option' do
+ let(:viewer) { nil }
+
+ let!(:local_account) { Fabricate(:account, domain: nil) }
+ let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
+ let!(:local_status) { Fabricate(:status, account: local_account) }
+ let!(:remote_status) { Fabricate(:status, account: remote_account) }
++ let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) }
+
+ subject { described_class.new(viewer).get(20).map(&:id) }
+
+ context 'without a viewer' do
+ let(:viewer) { nil }
+
+ it 'includes remote instances statuses' do
+ expect(subject).to include(remote_status.id)
+ end
+
+ it 'includes local statuses' do
+ expect(subject).to include(local_status.id)
+ end
++
++ it 'does not include local-only statuses' do
++ expect(subject).not_to include(local_only_status.id)
++ end
+ end
+
+ context 'with a viewer' do
+ let(:viewer) { Fabricate(:account, username: 'viewer') }
+
+ it 'includes remote instances statuses' do
+ expect(subject).to include(remote_status.id)
+ end
+
+ it 'includes local statuses' do
+ expect(subject).to include(local_status.id)
+ end
++
++ it 'does not include local-only statuses' do
++ expect(subject).not_to include(local_only_status.id)
++ end
++ end
++ end
++
++ context 'without local_only option but allow_local_only' do
++ let(:viewer) { nil }
++
++ let!(:local_account) { Fabricate(:account, domain: nil) }
++ let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
++ let!(:local_status) { Fabricate(:status, account: local_account) }
++ let!(:remote_status) { Fabricate(:status, account: remote_account) }
++ let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) }
++
++ subject { described_class.new(viewer, allow_local_only: true).get(20).map(&:id) }
++
++ context 'without a viewer' do
++ let(:viewer) { nil }
++
++ it 'includes remote instances statuses' do
++ expect(subject).to include(remote_status.id)
++ end
++
++ it 'includes local statuses' do
++ expect(subject).to include(local_status.id)
++ end
++
++ it 'does not include local-only statuses' do
++ expect(subject).not_to include(local_only_status.id)
++ end
++ end
++
++ context 'with a viewer' do
++ let(:viewer) { Fabricate(:account, username: 'viewer') }
++
++ it 'includes remote instances statuses' do
++ expect(subject).to include(remote_status.id)
++ end
++
++ it 'includes local statuses' do
++ expect(subject).to include(local_status.id)
++ end
++
++ it 'includes local-only statuses' do
++ expect(subject).to include(local_only_status.id)
++ end
+ end
+ end
+
+ context 'with a local_only option set' do
+ let!(:local_account) { Fabricate(:account, domain: nil) }
+ let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
+ let!(:local_status) { Fabricate(:status, account: local_account) }
+ let!(:remote_status) { Fabricate(:status, account: remote_account) }
++ let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) }
+
+ subject { described_class.new(viewer, local: true).get(20).map(&:id) }
+
+ context 'without a viewer' do
+ let(:viewer) { nil }
+
+ it 'does not include remote instances statuses' do
+ expect(subject).to include(local_status.id)
+ expect(subject).not_to include(remote_status.id)
+ end
++
++ it 'does not include local-only statuses' do
++ expect(subject).not_to include(local_only_status.id)
++ end
+ end
+
+ context 'with a viewer' do
+ let(:viewer) { Fabricate(:account, username: 'viewer') }
+
+ it 'does not include remote instances statuses' do
+ expect(subject).to include(local_status.id)
+ expect(subject).not_to include(remote_status.id)
+ end
+
+ it 'is not affected by personal domain blocks' do
+ viewer.block_domain!('test.com')
+ expect(subject).to include(local_status.id)
+ expect(subject).not_to include(remote_status.id)
+ end
++
++ it 'includes local-only statuses' do
++ expect(subject).to include(local_only_status.id)
++ end
+ end
+ end
+
+ context 'with a remote_only option set' do
+ let!(:local_account) { Fabricate(:account, domain: nil) }
+ let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
+ let!(:local_status) { Fabricate(:status, account: local_account) }
+ let!(:remote_status) { Fabricate(:status, account: remote_account) }
+
+ subject { described_class.new(viewer, remote: true).get(20).map(&:id) }
+
+ context 'without a viewer' do
+ let(:viewer) { nil }
+
+ it 'does not include local instances statuses' do
+ expect(subject).not_to include(local_status.id)
+ expect(subject).to include(remote_status.id)
+ end
+ end
+
+ context 'with a viewer' do
+ let(:viewer) { Fabricate(:account, username: 'viewer') }
+
+ it 'does not include local instances statuses' do
+ expect(subject).not_to include(local_status.id)
+ expect(subject).to include(remote_status.id)
+ end
+ end
+ end
+
+ describe 'with an account passed in' do
+ before do
+ @account = Fabricate(:account)
+ end
+
+ subject { described_class.new(@account).get(20).map(&:id) }
+
+ it 'excludes statuses from accounts blocked by the account' do
+ blocked = Fabricate(:account)
+ @account.block!(blocked)
+ blocked_status = Fabricate(:status, account: blocked)
+
+ expect(subject).not_to include(blocked_status.id)
+ end
+
+ it 'excludes statuses from accounts who have blocked the account' do
+ blocker = Fabricate(:account)
+ blocker.block!(@account)
+ blocked_status = Fabricate(:status, account: blocker)
+
+ expect(subject).not_to include(blocked_status.id)
+ end
+
+ it 'excludes statuses from accounts muted by the account' do
+ muted = Fabricate(:account)
+ @account.mute!(muted)
+ muted_status = Fabricate(:status, account: muted)
+
+ expect(subject).not_to include(muted_status.id)
+ end
+
+ it 'excludes statuses from accounts from personally blocked domains' do
+ blocked = Fabricate(:account, domain: 'example.com')
+ @account.block_domain!(blocked.domain)
+ blocked_status = Fabricate(:status, account: blocked)
+
+ expect(subject).not_to include(blocked_status.id)
+ end
+
+ context 'with language preferences' do
+ it 'excludes statuses in languages not allowed by the account user' do
+ user = Fabricate(:user, chosen_languages: [:en, :es])
+ @account.update(user: user)
+ en_status = Fabricate(:status, language: 'en')
+ es_status = Fabricate(:status, language: 'es')
+ fr_status = Fabricate(:status, language: 'fr')
+
+ expect(subject).to include(en_status.id)
+ expect(subject).to include(es_status.id)
+ expect(subject).not_to include(fr_status.id)
+ end
+
+ it 'includes all languages when user does not have a setting' do
+ user = Fabricate(:user, chosen_languages: nil)
+ @account.update(user: user)
+
+ en_status = Fabricate(:status, language: 'en')
+ es_status = Fabricate(:status, language: 'es')
+
+ expect(subject).to include(en_status.id)
+ expect(subject).to include(es_status.id)
+ end
+
+ it 'includes all languages when account does not have a user' do
+ expect(@account.user).to be_nil
+ en_status = Fabricate(:status, language: 'en')
+ es_status = Fabricate(:status, language: 'es')
+
+ expect(subject).to include(en_status.id)
+ expect(subject).to include(es_status.id)
+ end
+ end
+ end
+ end
+ end
end
end
- describe '.as_public_timeline' do
- it 'only includes statuses with public visibility' do
- public_status = Fabricate(:status, visibility: :public)
- private_status = Fabricate(:status, visibility: :private)
-
- results = Status.as_public_timeline
- expect(results).to include(public_status)
- expect(results).not_to include(private_status)
- end
-
- it 'does not include replies' do
- status = Fabricate(:status)
- reply = Fabricate(:status, in_reply_to_id: status.id)
-
- results = Status.as_public_timeline
- expect(results).to include(status)
- expect(results).not_to include(reply)
- end
-
- it 'does not include boosts' do
- status = Fabricate(:status)
- boost = Fabricate(:status, reblog_of_id: status.id)
-
- results = Status.as_public_timeline
- expect(results).to include(status)
- expect(results).not_to include(boost)
- end
-
- it 'filters out silenced accounts' do
- account = Fabricate(:account)
- silenced_account = Fabricate(:account, silenced: true)
- status = Fabricate(:status, account: account)
- silenced_status = Fabricate(:status, account: silenced_account)
-
- results = Status.as_public_timeline
- expect(results).to include(status)
- expect(results).not_to include(silenced_status)
- end
-
- context 'without local_only option' do
- let(:viewer) { nil }
-
- let!(:local_account) { Fabricate(:account, domain: nil) }
- let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
- let!(:local_status) { Fabricate(:status, account: local_account) }
- let!(:remote_status) { Fabricate(:status, account: remote_account) }
-
- subject { Status.as_public_timeline(viewer, false) }
-
- context 'without a viewer' do
- let(:viewer) { nil }
-
- it 'includes remote instances statuses' do
- expect(subject).to include(remote_status)
- end
-
- it 'includes local statuses' do
- expect(subject).to include(local_status)
- end
- end
-
- context 'with a viewer' do
- let(:viewer) { Fabricate(:account, username: 'viewer') }
-
- it 'includes remote instances statuses' do
- expect(subject).to include(remote_status)
- end
-
- it 'includes local statuses' do
- expect(subject).to include(local_status)
- end
- end
- end
-
- context 'with a local_only option set' do
- let!(:local_account) { Fabricate(:account, domain: nil) }
- let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
- let!(:local_status) { Fabricate(:status, account: local_account) }
- let!(:remote_status) { Fabricate(:status, account: remote_account) }
-
- subject { Status.as_public_timeline(viewer, true) }
-
- context 'without a viewer' do
- let(:viewer) { nil }
-
- it 'does not include remote instances statuses' do
- expect(subject).to include(local_status)
- expect(subject).not_to include(remote_status)
- end
- end
-
- context 'with a viewer' do
- let(:viewer) { Fabricate(:account, username: 'viewer') }
-
- it 'does not include remote instances statuses' do
- expect(subject).to include(local_status)
- expect(subject).not_to include(remote_status)
- end
-
- it 'is not affected by personal domain blocks' do
- viewer.block_domain!('test.com')
- expect(subject).to include(local_status)
- expect(subject).not_to include(remote_status)
- end
- end
- end
-
- context 'with a remote_only option set' do
- let!(:local_account) { Fabricate(:account, domain: nil) }
- let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
- let!(:local_status) { Fabricate(:status, account: local_account) }
- let!(:remote_status) { Fabricate(:status, account: remote_account) }
-
- subject { Status.as_public_timeline(viewer, :remote) }
-
- context 'without a viewer' do
- let(:viewer) { nil }
-
- it 'does not include local instances statuses' do
- expect(subject).not_to include(local_status)
- expect(subject).to include(remote_status)
- end
- end
-
- context 'with a viewer' do
- let(:viewer) { Fabricate(:account, username: 'viewer') }
-
- it 'does not include local instances statuses' do
- expect(subject).not_to include(local_status)
- expect(subject).to include(remote_status)
- end
- end
- end
-
- describe 'with an account passed in' do
- before do
- @account = Fabricate(:account)
- end
-
- it 'excludes statuses from accounts blocked by the account' do
- blocked = Fabricate(:account)
- Fabricate(:block, account: @account, target_account: blocked)
- blocked_status = Fabricate(:status, account: blocked)
-
- results = Status.as_public_timeline(@account)
- expect(results).not_to include(blocked_status)
- end
-
- it 'excludes statuses from accounts who have blocked the account' do
- blocked = Fabricate(:account)
- Fabricate(:block, account: blocked, target_account: @account)
- blocked_status = Fabricate(:status, account: blocked)
-
- results = Status.as_public_timeline(@account)
- expect(results).not_to include(blocked_status)
- end
-
- it 'excludes statuses from accounts muted by the account' do
- muted = Fabricate(:account)
- Fabricate(:mute, account: @account, target_account: muted)
- muted_status = Fabricate(:status, account: muted)
-
- results = Status.as_public_timeline(@account)
- expect(results).not_to include(muted_status)
- end
-
- it 'excludes statuses from accounts from personally blocked domains' do
- blocked = Fabricate(:account, domain: 'example.com')
- @account.block_domain!(blocked.domain)
- blocked_status = Fabricate(:status, account: blocked)
-
- results = Status.as_public_timeline(@account)
- expect(results).not_to include(blocked_status)
- end
-
- context 'with language preferences' do
- it 'excludes statuses in languages not allowed by the account user' do
- user = Fabricate(:user, chosen_languages: [:en, :es])
- @account.update(user: user)
- en_status = Fabricate(:status, language: 'en')
- es_status = Fabricate(:status, language: 'es')
- fr_status = Fabricate(:status, language: 'fr')
-
- results = Status.as_public_timeline(@account)
- expect(results).to include(en_status)
- expect(results).to include(es_status)
- expect(results).not_to include(fr_status)
- end
-
- it 'includes all languages when user does not have a setting' do
- user = Fabricate(:user, chosen_languages: nil)
- @account.update(user: user)
-
- en_status = Fabricate(:status, language: 'en')
- es_status = Fabricate(:status, language: 'es')
-
- results = Status.as_public_timeline(@account)
- expect(results).to include(en_status)
- expect(results).to include(es_status)
- end
-
- it 'includes all languages when account does not have a user' do
- expect(@account.user).to be_nil
- en_status = Fabricate(:status, language: 'en')
- es_status = Fabricate(:status, language: 'es')
-
- results = Status.as_public_timeline(@account)
- expect(results).to include(en_status)
- expect(results).to include(es_status)
- end
- end
- end
-
- context 'with local-only statuses' do
- let(:status) { Fabricate(:status, local_only: true) }
-
- subject { Status.as_public_timeline(viewer) }
-
- context 'without a viewer' do
- let(:viewer) { nil }
-
- it 'excludes local-only statuses' do
- expect(subject).to_not include(status)
- end
- end
-
- context 'with a viewer' do
- let(:viewer) { Fabricate(:account, username: 'viewer') }
-
- it 'includes local-only statuses' do
- expect(subject).to include(status)
- end
- end
-
- # TODO: What happens if the viewer is remote?
- # Can the viewer be remote?
- # What prevents the viewer from being remote?
- end
- end
-
- describe '.as_tag_timeline' do
- it 'includes statuses with a tag' do
- tag = Fabricate(:tag)
- status = Fabricate(:status, tags: [tag])
- other = Fabricate(:status)
-
- results = Status.as_tag_timeline(tag)
- expect(results).to include(status)
- expect(results).not_to include(other)
- end
-
- it 'allows replies to be included' do
- original = Fabricate(:status)
- tag = Fabricate(:tag)
- status = Fabricate(:status, tags: [tag], in_reply_to_id: original.id)
-
- results = Status.as_tag_timeline(tag)
- expect(results).to include(status)
- end
-
- context 'on a local-only status' do
- let(:tag) { Fabricate(:tag) }
- let(:status) { Fabricate(:status, local_only: true, tags: [tag]) }
-
- context 'without a viewer' do
- let(:viewer) { nil }
-
- it 'filters the local-only status out of the result set' do
- expect(Status.as_tag_timeline(tag, viewer)).not_to include(status)
- end
- end
-
- context 'with a viewer' do
- let(:viewer) { Fabricate(:account, username: 'viewer', domain: nil) }
-
- it 'keeps the local-only status in the result set' do
- expect(Status.as_tag_timeline(tag, viewer)).to include(status)
- end
- end
- end
- end
-
+ describe '.as_direct_timeline' do
+ let(:account) { Fabricate(:account) }
+ let(:followed) { Fabricate(:account) }
+ let(:not_followed) { Fabricate(:account) }
+
+ before do
+ Fabricate(:follow, account: account, target_account: followed)
+
+ @self_public_status = Fabricate(:status, account: account, visibility: :public)
+ @self_direct_status = Fabricate(:status, account: account, visibility: :direct)
+ @followed_public_status = Fabricate(:status, account: followed, visibility: :public)
+ @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct)
+ @not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct)
+
+ @results = Status.as_direct_timeline(account)
+ end
+
+ it 'does not include public statuses from self' do
+ expect(@results).to_not include(@self_public_status)
+ end
+
+ it 'includes direct statuses from self' do
+ expect(@results).to include(@self_direct_status)
+ end
+
+ it 'does not include public statuses from followed' do
+ expect(@results).to_not include(@followed_public_status)
+ end
+
+ it 'does not include direct statuses not mentioning recipient from followed' do
+ expect(@results).to_not include(@followed_direct_status)
+ end
+
+ it 'does not include direct statuses not mentioning recipient from non-followed' do
+ expect(@results).to_not include(@not_followed_direct_status)
+ end
+
+ it 'includes direct statuses mentioning recipient from followed' do
+ Fabricate(:mention, account: account, status: @followed_direct_status)
+ results2 = Status.as_direct_timeline(account)
+ expect(results2).to include(@followed_direct_status)
+ end
+
+ it 'includes direct statuses mentioning recipient from non-followed' do
+ Fabricate(:mention, account: account, status: @not_followed_direct_status)
+ results2 = Status.as_direct_timeline(account)
+ expect(results2).to include(@not_followed_direct_status)
+ end
+ end
+
describe '.permitted_for' do
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
it 'can restrict to local' do
status1.account.update(domain: 'example.com')
status1.update(local: false, uri: 'example.com/toot')
- results = subject.call(tag1, { any: [tag2.name] }, nil, true)
+ results = described_class.new(tag1, nil, any: [tag2.name], local: true).get(20)
expect(results).to_not include status1
end
+
+ it 'allows replies to be included' do
+ original = Fabricate(:status)
+ status = Fabricate(:status, tags: [tag1], in_reply_to_id: original.id)
+
+ results = described_class.new(tag1, nil).get(20)
+ expect(results).to include(status)
+ end
++
++ context 'on a local-only status' do
++ let!(:status) { Fabricate(:status, tags: [tag1], local_only: true) }
++
++ it 'does not show local-only statuses without a viewer' do
++ results = described_class.new(tag1, nil).get(20)
++ expect(results).to_not include(status)
++ end
++
++ it 'shows local-only statuses given a viewer' do
++ results = described_class.new(tag1, account).get(20)
++ expect(results).to include(status)
++ end
++ end
end
end