Refactor how public and tag timelines are queried (#14728)
parent
a6121a159c
commit
e8bc187845
@ -0,0 +1,90 @@
|
||||
# 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
|
@ -0,0 +1,57 @@
|
||||
# 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
|
@ -1,22 +0,0 @@
|
||||
# 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
|
@ -0,0 +1,212 @@
|
||||
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
|
Loading…
Reference in new issue