end
def cached_public_statuses_page
- cache_collection_paginated_by_id(
- public_statuses,
- Status,
- limit_param(DEFAULT_STATUSES_LIMIT),
- params_slice(:max_id, :since_id, :min_id)
- )
+ cache_collection(public_statuses, Status)
end
def public_statuses
- statuses = public_timeline_statuses
-
- 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)
+ )
end
def insert_pagination_headers
end
def cached_tagged_statuses
- if @tag.nil?
- []
- else
- statuses = tag_timeline_statuses
- statuses = statuses.joins(:media_attachments) if truthy_param?(:only_media)
-
- cache_collection_paginated_by_id(
- statuses,
- Status,
- limit_param(DEFAULT_STATUSES_LIMIT),
- params_slice(:max_id, :since_id, :min_id)
- )
- end
+ @tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status)
end
def tag_timeline_statuses
- HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
+ tag_feed.get(
+ limit_param(DEFAULT_STATUSES_LIMIT),
+ params[:max_id],
+ params[:since_id],
+ params[:min_id]
+ )
+ end
+
+ def tag_feed
+ TagFeed.new(
+ @tag,
+ current_account,
+ any: params[:any],
+ all: params[:all],
+ none: params[:none],
+ local: truthy_param?(:local),
+ remote: truthy_param?(:remote),
+ only_media: truthy_param?(:only_media)
+ )
end
def insert_pagination_headers
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
- before_action :set_tag
before_action :set_local
+ before_action :set_tag
+ before_action :set_statuses
before_action :set_body_classes
before_action :set_instance_presenter
format.rss do
expires_in 0, public: true
-
- limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
- @statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit)
- @statuses = cache_collection(@statuses, Status)
-
render xml: RSS::TagSerializer.render(@tag, @statuses)
end
format.json do
expires_in 3.minutes, public: public_fetch_mode?
-
- @statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id])
- @statuses = cache_collection(@statuses, Status)
-
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
end
@local = truthy_param?(:local)
end
+ def set_statuses
+ case request.format&.to_sym
+ when :json
+ @statuses = cache_collection(TagFeed.new(@tag, current_account, local: @local).get(PAGE_SIZE, params[:max_id], params[:since_id], params[:min_id]), Status)
+ when :rss
+ @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
+ end
+ end
+
def set_body_classes
@body_classes = 'with-modals'
end
@instance_presenter = InstancePresenter.new
end
+ def limit_param
+ params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
+ end
+
def collection_presenter
ActivityPub::CollectionPresenter.new(
- id: tag_url(@tag, filter_params),
+ id: tag_url(@tag),
type: :ordered,
size: @tag.statuses.count,
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
)
end
-
- def filter_params
- params.slice(:any, :all, :none).permit(:any, :all, :none)
- end
end
--- /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
+ 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_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 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 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 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
scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where(local: false).where.not(uri: nil) }
scope :local, -> { where(local: true).or(where(uri: nil)) }
-
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
+ scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
visibilities.keys - %w(direct limited)
end
- def in_chosen_languages(account)
- where(language: nil).or where(language: account.chosen_languages)
- end
-
- def as_public_timeline(account = nil, local_only = false)
- query = timeline_scope(local_only).without_replies
-
- 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 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
- .with_public_visibility
- .without_reblogs
- 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.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 status_stat
--- /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!(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
+++ /dev/null
-# frozen_string_literal: true
-
-class HashtagQueryService < BaseService
- LIMIT_PER_MODE = 4
-
- def call(tag, params, account = nil, local = false)
- tags = tags_for(Array(tag.name) | Array(params[:any])).pluck(:id)
- all = tags_for(params[:all])
- none = tags_for(params[:none])
-
- Status.group(:id)
- .as_tag_timeline(tags, account, local)
- .tagged_with_all(all)
- .tagged_with_none(none)
- end
-
- private
-
- def tags_for(names)
- Tag.matching_name(Array(names).take(LIMIT_PER_MODE)) if names.present?
- end
-end
--- /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) }
+
+ 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
+ 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
+ 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 { 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
+ 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
+ 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
- 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
- end
-
describe '.permitted_for' do
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
require 'rails_helper'
-describe HashtagQueryService, type: :service do
- describe '.call' do
+describe TagFeed, type: :service do
+ describe '#get' do
let(:account) { Fabricate(:account) }
let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) }
let!(:both) { Fabricate(:status, tags: [tag1, tag2]) }
it 'can add tags in "any" mode' do
- results = subject.call(tag1, { any: [tag2.name] })
+ results = described_class.new(tag1, nil, any: [tag2.name]).get(20)
expect(results).to include status1
expect(results).to include status2
expect(results).to include both
end
it 'can remove tags in "all" mode' do
- results = subject.call(tag1, { all: [tag2.name] })
+ results = described_class.new(tag1, nil, all: [tag2.name]).get(20)
expect(results).to_not include status1
expect(results).to_not include status2
expect(results).to include both
end
it 'can remove tags in "none" mode' do
- results = subject.call(tag1, { none: [tag2.name] })
+ results = described_class.new(tag1, nil, none: [tag2.name]).get(20)
expect(results).to include status1
expect(results).to_not include status2
expect(results).to_not include both
end
it 'ignores an invalid mode' do
- results = subject.call(tag1, { wark: [tag2.name] })
+ results = described_class.new(tag1, nil, wark: [tag2.name]).get(20)
expect(results).to include status1
expect(results).to_not include status2
expect(results).to include both
end
it 'handles being passed non existant tag names' do
- results = subject.call(tag1, { any: ['wark'] })
+ results = described_class.new(tag1, nil, any: ['wark']).get(20)
expect(results).to include status1
expect(results).to_not include status2
expect(results).to include both
it 'can restrict to an account' do
BlockService.new.call(account, status1.account)
- results = subject.call(tag1, { none: [tag2.name] }, account)
+ results = described_class.new(tag1, account, none: [tag2.name]).get(20)
expect(results).to_not include status1
end
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
end
end
end
it 'delivers status to hashtag' do
- expect(Tag.find_by!(name: 'test').statuses.pluck(:id)).to include status.id
+ expect(TagFeed.new(Tag.find_by(name: 'test'), alice).get(20).map(&:id)).to include status.id
end
it 'delivers status to public timeline' do
- expect(Status.as_public_timeline(alice).map(&:id)).to include status.id
+ expect(PublicFeed.new(alice).get(20).map(&:id)).to include status.id
end
end