Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - app/controllers/api/v1/timelines/public_controller.rb - app/lib/feed_manager.rb - app/models/status.rb - app/services/precompute_feed_service.rb - app/workers/feed_insert_worker.rb - spec/models/status_spec.rb All conflicts are due to upstream refactoring feed management and us having local-only toots on top of that. Rewrote local-only toots management for upstream's changes.
This commit is contained in:
@ -3,15 +3,15 @@
|
||||
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||
before_action :require_user!
|
||||
before_action :set_most_used_tags, only: :index
|
||||
before_action :set_recently_used_tags, only: :index
|
||||
|
||||
def index
|
||||
render json: @most_used_tags, each_serializer: REST::TagSerializer
|
||||
render json: @recently_used_tags, each_serializer: REST::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_most_used_tags
|
||||
@most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||
end
|
||||
end
|
||||
|
@ -20,28 +20,26 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
||||
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
|
||||
|
||||
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),
|
||||
allow_local_only: truthy_param?(:allow_local_only)
|
||||
)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -20,23 +20,29 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||
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
|
||||
|
@ -6,7 +6,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_featured_tags, only: :index
|
||||
before_action :set_featured_tag, except: [:index, :create]
|
||||
before_action :set_most_used_tags, only: :index
|
||||
before_action :set_recently_used_tags, only: :index
|
||||
|
||||
def index
|
||||
@featured_tag = FeaturedTag.new
|
||||
@ -20,7 +20,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||
redirect_to settings_featured_tags_path
|
||||
else
|
||||
set_featured_tags
|
||||
set_most_used_tags
|
||||
set_recently_used_tags
|
||||
|
||||
render :index
|
||||
end
|
||||
@ -41,8 +41,8 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
|
||||
end
|
||||
|
||||
def set_most_used_tags
|
||||
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||
end
|
||||
|
||||
def featured_tag_params
|
||||
|
@ -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
|
||||
|
||||
@ -26,20 +27,11 @@ class TagsController < ApplicationController
|
||||
|
||||
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
|
||||
@ -55,6 +47,15 @@ class TagsController < ApplicationController
|
||||
@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
|
||||
@ -63,16 +64,16 @@ class TagsController < ApplicationController
|
||||
@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
|
||||
|
@ -6,33 +6,56 @@ class FeedManager
|
||||
include Singleton
|
||||
include Redisable
|
||||
|
||||
# Maximum number of items stored in a single feed
|
||||
MAX_ITEMS = 400
|
||||
|
||||
# Must be <= MAX_ITEMS or the tracking sets will grow forever
|
||||
# Number of items in the feed since last reblog of status
|
||||
# before the new reblog will be inserted. Must be <= MAX_ITEMS
|
||||
# or the tracking sets will grow forever
|
||||
REBLOG_FALLOFF = 40
|
||||
|
||||
# Execute block for every active account
|
||||
# @yield [Account]
|
||||
# @return [void]
|
||||
def with_active_accounts(&block)
|
||||
Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
|
||||
end
|
||||
|
||||
# Redis key of a feed
|
||||
# @param [Symbol] type
|
||||
# @param [Integer] id
|
||||
# @param [Symbol] subtype
|
||||
# @return [String]
|
||||
def key(type, id, subtype = nil)
|
||||
return "feed:#{type}:#{id}" unless subtype
|
||||
|
||||
"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
|
||||
end
|
||||
|
||||
# Add a status to a home feed and send a streaming API update
|
||||
# @param [Account] account
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def push_to_home(account, status)
|
||||
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||
|
||||
@ -41,6 +64,10 @@ class FeedManager
|
||||
true
|
||||
end
|
||||
|
||||
# Remove a status from a home feed and send a streaming API update
|
||||
# @param [Account] account
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def unpush_from_home(account, status)
|
||||
return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||
|
||||
@ -48,21 +75,22 @@ class FeedManager
|
||||
true
|
||||
end
|
||||
|
||||
# Add a status to a list feed and send a streaming API update
|
||||
# @param [List] list
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def push_to_list(list, status)
|
||||
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 false if should_filter
|
||||
end
|
||||
|
||||
return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
|
||||
trim(:list, list.id)
|
||||
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
||||
true
|
||||
end
|
||||
|
||||
# Remove a status from a list feed and send a streaming API update
|
||||
# @param [List] list
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def unpush_from_list(list, status)
|
||||
return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
|
||||
@ -70,44 +98,34 @@ class FeedManager
|
||||
true
|
||||
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)
|
||||
|
||||
redis.publish("timeline:direct:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
||||
true
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def merge_into_timeline(from_account, into_account)
|
||||
# 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)
|
||||
@ -129,7 +147,37 @@ class FeedManager
|
||||
trim(:home, into_account.id)
|
||||
end
|
||||
|
||||
def unmerge_from_timeline(from_account, into_account)
|
||||
# Fill a list feed with an account's statuses
|
||||
# @param [Account] from_account
|
||||
# @param [List] list
|
||||
# @return [void]
|
||||
def merge_into_list(from_account, list)
|
||||
timeline_key = key(:list, list.id)
|
||||
aggregate = list.account.user&.aggregates_reblogs?
|
||||
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
||||
|
||||
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
|
||||
query = query.where('id > ?', oldest_home_score)
|
||||
end
|
||||
|
||||
statuses = query.to_a
|
||||
crutches = build_crutches(list.account_id, statuses)
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, list.account_id, crutches) || filter_from_list?(status, list)
|
||||
|
||||
add_to_feed(:list, list.id, status, aggregate)
|
||||
end
|
||||
|
||||
trim(:list, list.id)
|
||||
end
|
||||
|
||||
# Remove an account's statuses from a home feed
|
||||
# @param [Account] from_account
|
||||
# @param [Account] into_account
|
||||
# @return [void]
|
||||
def unmerge_from_home(from_account, into_account)
|
||||
timeline_key = key(:home, into_account.id)
|
||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
||||
|
||||
@ -138,14 +186,31 @@ class FeedManager
|
||||
end
|
||||
end
|
||||
|
||||
def clear_from_timeline(account, target_account)
|
||||
# Clear from timeline all statuses from or mentionning target_account
|
||||
# Remove an account's statuses from a list feed
|
||||
# @param [Account] from_account
|
||||
# @param [List] list
|
||||
# @return [void]
|
||||
def unmerge_from_list(from_account, list)
|
||||
timeline_key = key(:list, list.id)
|
||||
oldest_list_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
||||
|
||||
from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_list_score).reorder(nil).find_each do |status|
|
||||
remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
end
|
||||
end
|
||||
|
||||
# Clear all statuses from or mentioning target_account from a home feed
|
||||
# @param [Account] account
|
||||
# @param [Account] target_account
|
||||
# @return [void]
|
||||
def clear_from_home(account, target_account)
|
||||
timeline_key = key(:home, account.id)
|
||||
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
||||
statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
|
||||
reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
|
||||
with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
|
||||
target_statuses = statuses.filter do |status|
|
||||
|
||||
target_statuses = statuses.select do |status|
|
||||
status.account_id == target_account.id || reblogged_ids.include?(status.reblog_of_id) || with_mentions_ids.include?(status.id) || with_mentions_ids.include?(status.reblog_of_id)
|
||||
end
|
||||
|
||||
@ -154,7 +219,10 @@ class FeedManager
|
||||
end
|
||||
end
|
||||
|
||||
def populate_feed(account)
|
||||
# Populate home feed of account from scratch
|
||||
# @param [Account] account
|
||||
# @return [void]
|
||||
def populate_home(account)
|
||||
limit = FeedManager::MAX_ITEMS / 2
|
||||
aggregate = account.user&.aggregates_reblogs?
|
||||
timeline_key = key(:home, account.id)
|
||||
@ -187,6 +255,9 @@ class FeedManager
|
||||
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
|
||||
@ -210,15 +281,59 @@ class FeedManager
|
||||
|
||||
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?)
|
||||
end
|
||||
|
||||
# Check if status should not be added to the home feed
|
||||
# @param [Status] status
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Hash] crutches
|
||||
# @return [Boolean]
|
||||
def filter_from_home?(status, receiver_id, crutches)
|
||||
return false if receiver_id == status.account_id
|
||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||
@ -251,6 +366,11 @@ class FeedManager
|
||||
false
|
||||
end
|
||||
|
||||
# Check if status should not be added to the mentions feed
|
||||
# @see NotifyService
|
||||
# @param [Status] status
|
||||
# @param [Integer] receiver_id
|
||||
# @return [Boolean]
|
||||
def filter_from_mentions?(status, receiver_id)
|
||||
return true if receiver_id == status.account_id
|
||||
return true if phrase_filtered?(status, receiver_id, :notifications)
|
||||
@ -267,11 +387,36 @@ class FeedManager
|
||||
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
|
||||
|
||||
@ -307,6 +452,11 @@ class FeedManager
|
||||
# added, and false if it was not added to the feed. Note that this is
|
||||
# an internal helper: callers must call trim or push updates if
|
||||
# either action is appropriate.
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Integer] account_id
|
||||
# @param [Status] status
|
||||
# @param [Boolean] aggregate_reblogs
|
||||
# @return [Boolean]
|
||||
def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
|
||||
timeline_key = key(timeline_type, account_id)
|
||||
reblog_key = key(timeline_type, account_id, 'reblogs')
|
||||
@ -319,14 +469,12 @@ class FeedManager
|
||||
|
||||
return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF
|
||||
|
||||
reblog_rank = redis.zrevrank(reblog_key, status.reblog_of_id)
|
||||
|
||||
if reblog_rank.nil?
|
||||
# The ordered set at `reblog_key` holds statuses which have a reblog
|
||||
# in the top `REBLOG_FALLOFF` statuses of the timeline
|
||||
if redis.zadd(reblog_key, status.id, status.reblog_of_id, nx: true)
|
||||
# This is not something we've already seen reblogged, so we
|
||||
# can just add it to the feed (and note that we're
|
||||
# reblogging it).
|
||||
# can just add it to the feed (and note that we're reblogging it).
|
||||
redis.zadd(timeline_key, status.id, status.id)
|
||||
redis.zadd(reblog_key, status.id, status.reblog_of_id)
|
||||
else
|
||||
# Another reblog of the same status was already in the
|
||||
# REBLOG_FALLOFF most recent statuses, so we note that this
|
||||
@ -340,9 +488,7 @@ class FeedManager
|
||||
# delay of the worker deliverying the original status, the late addition
|
||||
# by merging timelines, and other reasons.
|
||||
# If such a reblog already exists, just do not re-insert it into the feed.
|
||||
rank = redis.zrevrank(reblog_key, status.id)
|
||||
|
||||
return false unless rank.nil?
|
||||
return false unless redis.zscore(reblog_key, status.id).nil?
|
||||
|
||||
redis.zadd(timeline_key, status.id, status.id)
|
||||
end
|
||||
@ -354,6 +500,11 @@ class FeedManager
|
||||
# with reblogs, and returning true if a status was removed. As with
|
||||
# `add_to_feed`, this does not trigger push updates, so callers must
|
||||
# do so if appropriate.
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Integer] account_id
|
||||
# @param [Status] status
|
||||
# @param [Boolean] aggregate_reblogs
|
||||
# @return [Boolean]
|
||||
def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true)
|
||||
timeline_key = key(timeline_type, account_id)
|
||||
reblog_key = key(timeline_type, account_id, 'reblogs')
|
||||
@ -388,6 +539,11 @@ class FeedManager
|
||||
redis.zrem(timeline_key, status.id)
|
||||
end
|
||||
|
||||
# Pre-fetch various objects and relationships for given statuses that
|
||||
# are going to be checked by the filtering methods
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Array<Status>] statuses
|
||||
# @return [Hash]
|
||||
def build_crutches(receiver_id, statuses)
|
||||
crutches = {}
|
||||
|
||||
|
104
app/models/public_feed.rb
Normal file
104
app/models/public_feed.rb
Normal file
@ -0,0 +1,104 @@
|
||||
# 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
|
@ -89,12 +89,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) }
|
||||
@ -330,23 +330,6 @@ class Status < ApplicationRecord
|
||||
end
|
||||
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 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
|
||||
@ -423,53 +406,6 @@ class Status < ApplicationRecord
|
||||
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?
|
||||
|
@ -39,7 +39,7 @@ class Tag < ApplicationRecord
|
||||
scope :listable, -> { where(listable: [true, nil]) }
|
||||
scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) }
|
||||
scope :discoverable, -> { listable.joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
||||
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
scope :matches_name, ->(value) { where(arel_table[:name].matches("#{value}%")) }
|
||||
|
||||
delegate :accounts_count,
|
||||
|
58
app/models/tag_feed.rb
Normal file
58
app/models/tag_feed.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# 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
|
@ -13,7 +13,7 @@ class AfterBlockService < BaseService
|
||||
private
|
||||
|
||||
def clear_home_feed!
|
||||
FeedManager.instance.clear_from_timeline(@account, @target_account)
|
||||
FeedManager.instance.clear_from_home(@account, @target_account)
|
||||
end
|
||||
|
||||
def clear_conversations!
|
||||
|
@ -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
|
@ -13,15 +13,13 @@ class NotifyService < BaseService
|
||||
push_to_conversation! if direct_message?
|
||||
send_email! if email_enabled?
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
# rubocop:disable Style/RedundantReturn
|
||||
return
|
||||
# rubocop:enable Style/RedundantReturn
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blocked_mention?
|
||||
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
|
||||
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
||||
end
|
||||
|
||||
def blocked_favourite?
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
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")
|
||||
|
@ -9,7 +9,7 @@
|
||||
= render 'shared/error_messages', object: @featured_tag
|
||||
|
||||
.fields-group
|
||||
= f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ')
|
||||
= f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@recently_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('featured_tags.add_new'), type: :submit
|
||||
|
@ -29,13 +29,13 @@ class FeedInsertWorker
|
||||
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
|
||||
|
||||
|
@ -6,6 +6,8 @@ class MergeWorker
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(from_account_id, into_account_id)
|
||||
FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
|
||||
FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -4,9 +4,8 @@ class MuteWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(account_id, target_account_id)
|
||||
FeedManager.instance.clear_from_timeline(
|
||||
Account.find(account_id),
|
||||
Account.find(target_account_id)
|
||||
)
|
||||
FeedManager.instance.clear_from_home(Account.find(account_id), Account.find(target_account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -6,6 +6,8 @@ class UnmergeWorker
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(from_account_id, into_account_id)
|
||||
FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
|
||||
FeedManager.instance.unmerge_from_home(Account.find(from_account_id), Account.find(into_account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user