Skip to content

Commit

Permalink
Refactor how public and tag timelines are queried (mastodon#14728)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron authored and thenameisnigel-old committed Sep 7, 2020
1 parent 39dc1d2 commit b2a61c0
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 378 deletions.
29 changes: 14 additions & 15 deletions app/controllers/api/v1/timelines/public_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,25 @@ def load_statuses
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
Expand Down
34 changes: 20 additions & 14 deletions app/controllers/api/v1/timelines/tag_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,29 @@ def load_statuses
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
Expand Down
31 changes: 16 additions & 15 deletions app/controllers/tags_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ class TagsController < ApplicationController

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

Expand All @@ -25,20 +26,11 @@ def show

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
Expand All @@ -54,6 +46,15 @@ def set_local
@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
Expand All @@ -62,16 +63,16 @@ def set_instance_presenter
@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
90 changes: 90 additions & 0 deletions app/models/public_feed.rb
Original file line number Diff line number Diff line change
@@ -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
67 changes: 1 addition & 66 deletions app/models/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ class Status < ApplicationRecord
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) }
Expand Down Expand Up @@ -277,26 +277,6 @@ def selectable_visibilities
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
Expand Down Expand Up @@ -373,51 +353,6 @@ def from_text(text)
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
Expand Down
57 changes: 57 additions & 0 deletions app/models/tag_feed.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b2a61c0

Please sign in to comment.