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