forked from treehouse/mastodon
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.rebase/4.0.0rc2
commit
9748f074a3
4
Gemfile
4
Gemfile
|
@ -142,8 +142,8 @@ group :development do
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 1.4'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.88', require: false
|
gem 'rubocop', '~> 0.90', require: false
|
||||||
gem 'rubocop-rails', '~> 2.6', require: false
|
gem 'rubocop-rails', '~> 2.8', require: false
|
||||||
gem 'brakeman', '~> 4.9', require: false
|
gem 'brakeman', '~> 4.9', require: false
|
||||||
gem 'bundler-audit', '~> 0.7', require: false
|
gem 'bundler-audit', '~> 0.7', require: false
|
||||||
|
|
||||||
|
|
40
Gemfile.lock
40
Gemfile.lock
|
@ -79,7 +79,7 @@ GEM
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.363.0)
|
aws-partitions (1.365.0)
|
||||||
aws-sdk-core (3.105.0)
|
aws-sdk-core (3.105.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
|
@ -94,7 +94,7 @@ GEM
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.2.2)
|
aws-sigv4 (1.2.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
bcrypt (3.1.15)
|
bcrypt (3.1.16)
|
||||||
better_errors (2.7.1)
|
better_errors (2.7.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
|
@ -106,7 +106,7 @@ GEM
|
||||||
ffi (~> 1.10.0)
|
ffi (~> 1.10.0)
|
||||||
bootsnap (1.4.8)
|
bootsnap (1.4.8)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.9.0)
|
brakeman (4.9.1)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.0)
|
bullet (6.1.0)
|
||||||
|
@ -355,7 +355,7 @@ GEM
|
||||||
mimemagic (0.3.5)
|
mimemagic (0.3.5)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.14.1)
|
minitest (5.14.2)
|
||||||
msgpack (1.3.3)
|
msgpack (1.3.3)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
|
@ -363,7 +363,7 @@ GEM
|
||||||
net-scp (3.0.0)
|
net-scp (3.0.0)
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.2)
|
nio4r (2.5.3)
|
||||||
nokogiri (1.10.10)
|
nokogiri (1.10.10)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
nokogumbo (2.0.2)
|
nokogumbo (2.0.2)
|
||||||
|
@ -373,7 +373,7 @@ GEM
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (>= 3.5)
|
sidekiq (>= 3.5)
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.10.13)
|
oj (3.10.14)
|
||||||
omniauth (1.9.1)
|
omniauth (1.9.1)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
|
@ -387,7 +387,7 @@ GEM
|
||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
openssl-signature_algorithm (0.4.0)
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.13.2)
|
ox (2.13.3)
|
||||||
paperclip (6.0.0)
|
paperclip (6.0.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
@ -426,8 +426,8 @@ GEM
|
||||||
pry (~> 0.13.0)
|
pry (~> 0.13.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.5)
|
public_suffix (4.0.6)
|
||||||
puma (4.3.5)
|
puma (4.3.6)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -476,7 +476,7 @@ GEM
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.1)
|
rake (13.0.1)
|
||||||
rdf (3.1.5)
|
rdf (3.1.6)
|
||||||
hamster (~> 3.0)
|
hamster (~> 3.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.4.0)
|
||||||
|
@ -536,21 +536,21 @@ GEM
|
||||||
rspec-support (3.9.3)
|
rspec-support (3.9.3)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.4.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (0.88.0)
|
rubocop (0.90.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.1.1)
|
parser (>= 2.7.1.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.7)
|
regexp_parser (>= 1.7)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.1.0, < 1.0)
|
rubocop-ast (>= 0.3.0, < 1.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-ast (0.3.0)
|
rubocop-ast (0.3.0)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
rubocop-rails (2.6.0)
|
rubocop-rails (2.8.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.82.0)
|
rubocop (>= 0.87.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
ruby-saml (1.11.0)
|
ruby-saml (1.11.0)
|
||||||
nokogiri (>= 1.5.10)
|
nokogiri (>= 1.5.10)
|
||||||
|
@ -578,10 +578,10 @@ GEM
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
thwait
|
thwait
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (6.0.22)
|
sidekiq-unique-jobs (6.0.23)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 4.0, < 7.0)
|
sidekiq (>= 4.0, < 7.0)
|
||||||
thor (~> 0)
|
thor (>= 0.20, < 2.0)
|
||||||
simple-navigation (4.1.0)
|
simple-navigation (4.1.0)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
simple_form (5.0.2)
|
simple_form (5.0.2)
|
||||||
|
@ -642,8 +642,8 @@ GEM
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.7.7)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.7.0)
|
||||||
uniform_notifier (1.13.0)
|
uniform_notifier (1.13.0)
|
||||||
warden (1.2.8)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.9)
|
||||||
webauthn (3.0.0.alpha1)
|
webauthn (3.0.0.alpha1)
|
||||||
android_key_attestation (~> 0.3.0)
|
android_key_attestation (~> 0.3.0)
|
||||||
awrence (~> 1.1)
|
awrence (~> 1.1)
|
||||||
|
@ -780,8 +780,8 @@ DEPENDENCIES
|
||||||
rspec-rails (~> 4.0)
|
rspec-rails (~> 4.0)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.4)
|
||||||
rubocop (~> 0.88)
|
rubocop (~> 0.90)
|
||||||
rubocop-rails (~> 2.6)
|
rubocop-rails (~> 2.8)
|
||||||
ruby-progressbar (~> 1.10)
|
ruby-progressbar (~> 1.10)
|
||||||
sanitize (~> 5.2)
|
sanitize (~> 5.2)
|
||||||
sidekiq (~> 6.1)
|
sidekiq (~> 6.1)
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_most_used_tags, only: :index
|
before_action :set_recently_used_tags, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @most_used_tags, each_serializer: REST::TagSerializer
|
render json: @recently_used_tags, each_serializer: REST::TagSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_most_used_tags
|
def set_recently_used_tags
|
||||||
@most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,28 +20,26 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_public_statuses_page
|
def cached_public_statuses_page
|
||||||
cache_collection_paginated_by_id(
|
cache_collection(public_statuses, Status)
|
||||||
public_statuses,
|
|
||||||
Status,
|
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_statuses
|
def public_statuses
|
||||||
statuses = public_timeline_statuses
|
public_feed.get(
|
||||||
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
statuses = statuses.not_local_only unless truthy_param?(:local) || truthy_param?(:allow_local_only)
|
params[:max_id],
|
||||||
|
params[:since_id],
|
||||||
if truthy_param?(:only_media)
|
params[:min_id]
|
||||||
statuses.joins(:media_attachments).group(:id)
|
)
|
||||||
else
|
|
||||||
statuses
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_timeline_statuses
|
def public_feed
|
||||||
Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
|
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
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
|
|
@ -20,23 +20,29 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_tagged_statuses
|
def cached_tagged_statuses
|
||||||
if @tag.nil?
|
@tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status)
|
||||||
[]
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_timeline_statuses
|
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
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_featured_tags, only: :index
|
before_action :set_featured_tags, only: :index
|
||||||
before_action :set_featured_tag, except: [:index, :create]
|
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
|
def index
|
||||||
@featured_tag = FeaturedTag.new
|
@featured_tag = FeaturedTag.new
|
||||||
|
@ -20,7 +20,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
redirect_to settings_featured_tags_path
|
redirect_to settings_featured_tags_path
|
||||||
else
|
else
|
||||||
set_featured_tags
|
set_featured_tags
|
||||||
set_most_used_tags
|
set_recently_used_tags
|
||||||
|
|
||||||
render :index
|
render :index
|
||||||
end
|
end
|
||||||
|
@ -41,8 +41,8 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
|
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_most_used_tags
|
def set_recently_used_tags
|
||||||
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||||
end
|
end
|
||||||
|
|
||||||
def featured_tag_params
|
def featured_tag_params
|
||||||
|
|
|
@ -10,8 +10,9 @@ class TagsController < ApplicationController
|
||||||
|
|
||||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
before_action :authenticate_user!, if: :whitelist_mode?
|
||||||
before_action :set_tag
|
|
||||||
before_action :set_local
|
before_action :set_local
|
||||||
|
before_action :set_tag
|
||||||
|
before_action :set_statuses
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
|
@ -26,20 +27,11 @@ class TagsController < ApplicationController
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
expires_in 0, public: true
|
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)
|
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in 3.minutes, public: public_fetch_mode?
|
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'
|
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -55,6 +47,15 @@ class TagsController < ApplicationController
|
||||||
@local = truthy_param?(:local)
|
@local = truthy_param?(:local)
|
||||||
end
|
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
|
def set_body_classes
|
||||||
@body_classes = 'with-modals'
|
@body_classes = 'with-modals'
|
||||||
end
|
end
|
||||||
|
@ -63,16 +64,16 @@ class TagsController < ApplicationController
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def limit_param
|
||||||
|
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||||
|
end
|
||||||
|
|
||||||
def collection_presenter
|
def collection_presenter
|
||||||
ActivityPub::CollectionPresenter.new(
|
ActivityPub::CollectionPresenter.new(
|
||||||
id: tag_url(@tag, filter_params),
|
id: tag_url(@tag),
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
size: @tag.statuses.count,
|
size: @tag.statuses.count,
|
||||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
|
||||||
params.slice(:any, :all, :none).permit(:any, :all, :none)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,33 +6,56 @@ class FeedManager
|
||||||
include Singleton
|
include Singleton
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
|
# Maximum number of items stored in a single feed
|
||||||
MAX_ITEMS = 400
|
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
|
REBLOG_FALLOFF = 40
|
||||||
|
|
||||||
|
# Execute block for every active account
|
||||||
|
# @yield [Account]
|
||||||
|
# @return [void]
|
||||||
def with_active_accounts(&block)
|
def with_active_accounts(&block)
|
||||||
Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
|
Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Redis key of a feed
|
||||||
|
# @param [Symbol] type
|
||||||
|
# @param [Integer] id
|
||||||
|
# @param [Symbol] subtype
|
||||||
|
# @return [String]
|
||||||
def key(type, id, subtype = nil)
|
def key(type, id, subtype = nil)
|
||||||
return "feed:#{type}:#{id}" unless subtype
|
return "feed:#{type}:#{id}" unless subtype
|
||||||
|
|
||||||
"feed:#{type}:#{id}:#{subtype}"
|
"feed:#{type}:#{id}:#{subtype}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter?(timeline_type, status, receiver_id)
|
# Check if the status should not be added to a feed
|
||||||
if timeline_type == :home
|
# @param [Symbol] timeline_type
|
||||||
filter_from_home?(status, receiver_id, build_crutches(receiver_id, [status]))
|
# @param [Status] status
|
||||||
elsif timeline_type == :mentions
|
# @param [Account|List] receiver
|
||||||
filter_from_mentions?(status, receiver_id)
|
# @return [Boolean]
|
||||||
elsif timeline_type == :direct
|
def filter?(timeline_type, status, receiver)
|
||||||
filter_from_direct?(status, receiver_id)
|
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
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
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)
|
def push_to_home(account, status)
|
||||||
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||||
|
|
||||||
|
@ -41,6 +64,10 @@ class FeedManager
|
||||||
true
|
true
|
||||||
end
|
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)
|
def unpush_from_home(account, status)
|
||||||
return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||||
|
|
||||||
|
@ -48,21 +75,22 @@ class FeedManager
|
||||||
true
|
true
|
||||||
end
|
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)
|
def push_to_list(list, status)
|
||||||
if status.reply? && status.in_reply_to_account_id != status.account_id
|
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||||
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?)
|
|
||||||
|
|
||||||
trim(:list, list.id)
|
trim(:list, list.id)
|
||||||
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
||||||
true
|
true
|
||||||
end
|
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)
|
def unpush_from_list(list, status)
|
||||||
return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||||
|
|
||||||
|
@ -70,44 +98,34 @@ class FeedManager
|
||||||
true
|
true
|
||||||
end
|
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)
|
def push_to_direct(account, status)
|
||||||
return false unless add_to_feed(:direct, account.id, status)
|
return false unless add_to_feed(:direct, account.id, status)
|
||||||
|
|
||||||
trim(:direct, account.id)
|
trim(:direct, account.id)
|
||||||
PushUpdateWorker.perform_async(account.id, status.id, "timeline:direct:#{account.id}")
|
PushUpdateWorker.perform_async(account.id, status.id, "timeline:direct:#{account.id}")
|
||||||
true
|
true
|
||||||
end
|
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)
|
def unpush_from_direct(account, status)
|
||||||
return false unless remove_from_feed(:direct, account.id, 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))
|
redis.publish("timeline:direct:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def trim(type, account_id)
|
# Fill a home feed with an account's statuses
|
||||||
timeline_key = key(type, account_id)
|
# @param [Account] from_account
|
||||||
reblog_key = key(type, account_id, 'reblogs')
|
# @param [Account] into_account
|
||||||
|
# @return [void]
|
||||||
# Remove any items past the MAX_ITEMS'th entry in our feed
|
def merge_into_home(from_account, into_account)
|
||||||
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)
|
|
||||||
timeline_key = key(:home, into_account.id)
|
timeline_key = key(:home, into_account.id)
|
||||||
aggregate = into_account.user&.aggregates_reblogs?
|
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)
|
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)
|
trim(:home, into_account.id)
|
||||||
end
|
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)
|
timeline_key = key(:home, into_account.id)
|
||||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_from_timeline(account, target_account)
|
# Remove an account's statuses from a list feed
|
||||||
# Clear from timeline all statuses from or mentionning target_account
|
# @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_key = key(:home, account.id)
|
||||||
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
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
|
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)
|
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)
|
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)
|
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
|
end
|
||||||
|
|
||||||
|
@ -154,7 +219,10 @@ class FeedManager
|
||||||
end
|
end
|
||||||
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
|
limit = FeedManager::MAX_ITEMS / 2
|
||||||
aggregate = account.user&.aggregates_reblogs?
|
aggregate = account.user&.aggregates_reblogs?
|
||||||
timeline_key = key(:home, account.id)
|
timeline_key = key(:home, account.id)
|
||||||
|
@ -187,6 +255,9 @@ class FeedManager
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Populate direct feed of account from scratch
|
||||||
|
# @param [Account] account
|
||||||
|
# @return [void]
|
||||||
def populate_direct_feed(account)
|
def populate_direct_feed(account)
|
||||||
added = 0
|
added = 0
|
||||||
limit = FeedManager::MAX_ITEMS / 2
|
limit = FeedManager::MAX_ITEMS / 2
|
||||||
|
@ -210,15 +281,59 @@ class FeedManager
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def push_update_required?(timeline_id)
|
# Trim a feed to maximum size by removing older items
|
||||||
redis.exists?("subscribed:#{timeline_id}")
|
# @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
|
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)
|
def blocks_or_mutes?(receiver_id, account_ids, context)
|
||||||
Block.where(account_id: receiver_id, target_account_id: account_ids).any? ||
|
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?)
|
(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
|
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)
|
def filter_from_home?(status, receiver_id, crutches)
|
||||||
return false if receiver_id == status.account_id
|
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?)
|
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
|
false
|
||||||
end
|
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)
|
def filter_from_mentions?(status, receiver_id)
|
||||||
return true if receiver_id == status.account_id
|
return true if receiver_id == status.account_id
|
||||||
return true if phrase_filtered?(status, receiver_id, :notifications)
|
return true if phrase_filtered?(status, receiver_id, :notifications)
|
||||||
|
@ -267,11 +387,36 @@ class FeedManager
|
||||||
should_filter
|
should_filter
|
||||||
end
|
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)
|
def filter_from_direct?(status, receiver_id)
|
||||||
return false if receiver_id == status.account_id
|
return false if receiver_id == status.account_id
|
||||||
filter_from_mentions?(status, receiver_id)
|
filter_from_mentions?(status, receiver_id)
|
||||||
end
|
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)
|
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
|
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
|
# 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
|
# an internal helper: callers must call trim or push updates if
|
||||||
# either action is appropriate.
|
# 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)
|
def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
|
||||||
timeline_key = key(timeline_type, account_id)
|
timeline_key = key(timeline_type, account_id)
|
||||||
reblog_key = key(timeline_type, account_id, 'reblogs')
|
reblog_key = key(timeline_type, account_id, 'reblogs')
|
||||||
|
@ -319,14 +469,12 @@ class FeedManager
|
||||||
|
|
||||||
return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF
|
return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF
|
||||||
|
|
||||||
reblog_rank = redis.zrevrank(reblog_key, status.reblog_of_id)
|
# The ordered set at `reblog_key` holds statuses which have a reblog
|
||||||
|
# in the top `REBLOG_FALLOFF` statuses of the timeline
|
||||||
if reblog_rank.nil?
|
if redis.zadd(reblog_key, status.id, status.reblog_of_id, nx: true)
|
||||||
# This is not something we've already seen reblogged, so we
|
# This is not something we've already seen reblogged, so we
|
||||||
# can just add it to the feed (and note that we're
|
# can just add it to the feed (and note that we're reblogging it).
|
||||||
# reblogging it).
|
|
||||||
redis.zadd(timeline_key, status.id, status.id)
|
redis.zadd(timeline_key, status.id, status.id)
|
||||||
redis.zadd(reblog_key, status.id, status.reblog_of_id)
|
|
||||||
else
|
else
|
||||||
# Another reblog of the same status was already in the
|
# Another reblog of the same status was already in the
|
||||||
# REBLOG_FALLOFF most recent statuses, so we note that this
|
# 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
|
# delay of the worker deliverying the original status, the late addition
|
||||||
# by merging timelines, and other reasons.
|
# by merging timelines, and other reasons.
|
||||||
# If such a reblog already exists, just do not re-insert it into the feed.
|
# 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 redis.zscore(reblog_key, status.id).nil?
|
||||||
|
|
||||||
return false unless rank.nil?
|
|
||||||
|
|
||||||
redis.zadd(timeline_key, status.id, status.id)
|
redis.zadd(timeline_key, status.id, status.id)
|
||||||
end
|
end
|
||||||
|
@ -354,6 +500,11 @@ class FeedManager
|
||||||
# with reblogs, and returning true if a status was removed. As with
|
# with reblogs, and returning true if a status was removed. As with
|
||||||
# `add_to_feed`, this does not trigger push updates, so callers must
|
# `add_to_feed`, this does not trigger push updates, so callers must
|
||||||
# do so if appropriate.
|
# 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)
|
def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true)
|
||||||
timeline_key = key(timeline_type, account_id)
|
timeline_key = key(timeline_type, account_id)
|
||||||
reblog_key = key(timeline_type, account_id, 'reblogs')
|
reblog_key = key(timeline_type, account_id, 'reblogs')
|
||||||
|
@ -388,6 +539,11 @@ class FeedManager
|
||||||
redis.zrem(timeline_key, status.id)
|
redis.zrem(timeline_key, status.id)
|
||||||
end
|
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)
|
def build_crutches(receiver_id, statuses)
|
||||||
crutches = {}
|
crutches = {}
|
||||||
|
|
||||||
|
|
|
@ -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 :recent, -> { reorder(id: :desc) }
|
||||||
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
||||||
scope :local, -> { where(local: true).or(where(uri: nil)) }
|
scope :local, -> { where(local: true).or(where(uri: nil)) }
|
||||||
|
|
||||||
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
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_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 :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
|
||||||
scope :with_public_visibility, -> { where(visibility: :public) }
|
scope :with_public_visibility, -> { where(visibility: :public) }
|
||||||
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
|
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 :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 :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) }
|
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
|
||||||
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)
|
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 }
|
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
|
end
|
||||||
|
@ -423,53 +406,6 @@ class Status < ApplicationRecord
|
||||||
status&.distributable? ? status : nil
|
status&.distributable? ? status : nil
|
||||||
end.compact
|
end.compact
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def marked_local_only?
|
def marked_local_only?
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Tag < ApplicationRecord
|
||||||
scope :listable, -> { where(listable: [true, nil]) }
|
scope :listable, -> { where(listable: [true, nil]) }
|
||||||
scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) }
|
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 :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}%")) }
|
scope :matches_name, ->(value) { where(arel_table[:name].matches("#{value}%")) }
|
||||||
|
|
||||||
delegate :accounts_count,
|
delegate :accounts_count,
|
||||||
|
|
|
@ -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
|
private
|
||||||
|
|
||||||
def clear_home_feed!
|
def clear_home_feed!
|
||||||
FeedManager.instance.clear_from_timeline(@account, @target_account)
|
FeedManager.instance.clear_from_home(@account, @target_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_conversations!
|
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?
|
push_to_conversation! if direct_message?
|
||||||
send_email! if email_enabled?
|
send_email! if email_enabled?
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid
|
||||||
# rubocop:disable Style/RedundantReturn
|
nil
|
||||||
return
|
|
||||||
# rubocop:enable Style/RedundantReturn
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def blocked_mention?
|
def blocked_mention?
|
||||||
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
|
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocked_favourite?
|
def blocked_favourite?
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class PrecomputeFeedService < BaseService
|
class PrecomputeFeedService < BaseService
|
||||||
def call(account)
|
def call(account)
|
||||||
FeedManager.instance.populate_feed(account)
|
FeedManager.instance.populate_home(account)
|
||||||
FeedManager.instance.populate_direct_feed(account)
|
FeedManager.instance.populate_direct_feed(account)
|
||||||
ensure
|
ensure
|
||||||
Redis.current.del("account:#{account.id}:regeneration")
|
Redis.current.del("account:#{account.id}:regeneration")
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
= render 'shared/error_messages', object: @featured_tag
|
= render 'shared/error_messages', object: @featured_tag
|
||||||
|
|
||||||
.fields-group
|
.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
|
.actions
|
||||||
= f.button :button, t('featured_tags.add_new'), type: :submit
|
= f.button :button, t('featured_tags.add_new'), type: :submit
|
||||||
|
|
|
@ -29,13 +29,13 @@ class FeedInsertWorker
|
||||||
end
|
end
|
||||||
|
|
||||||
def feed_filtered?
|
def feed_filtered?
|
||||||
# Note: Lists are a variation of home, so the filtering rules
|
|
||||||
# of home apply to both
|
|
||||||
case @type
|
case @type
|
||||||
when :home, :list
|
when :home
|
||||||
FeedManager.instance.filter?(:home, @status, @follower.id)
|
FeedManager.instance.filter?(:home, @status, @follower)
|
||||||
|
when :list
|
||||||
|
FeedManager.instance.filter?(:list, @status, @list)
|
||||||
when :direct
|
when :direct
|
||||||
FeedManager.instance.filter?(:direct, @status, @account.id)
|
FeedManager.instance.filter?(:direct, @status, @account)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ class MergeWorker
|
||||||
sidekiq_options queue: 'pull'
|
sidekiq_options queue: 'pull'
|
||||||
|
|
||||||
def perform(from_account_id, into_account_id)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,9 +4,8 @@ class MuteWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(account_id, target_account_id)
|
def perform(account_id, target_account_id)
|
||||||
FeedManager.instance.clear_from_timeline(
|
FeedManager.instance.clear_from_home(Account.find(account_id), Account.find(target_account_id))
|
||||||
Account.find(account_id),
|
rescue ActiveRecord::RecordNotFound
|
||||||
Account.find(target_account_id)
|
true
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,8 @@ class UnmergeWorker
|
||||||
sidekiq_options queue: 'pull'
|
sidekiq_options queue: 'pull'
|
||||||
|
|
||||||
def perform(from_account_id, into_account_id)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,11 +60,11 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.11.1",
|
"@babel/core": "^7.11.6",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-decorators": "^7.10.5",
|
"@babel/plugin-proposal-decorators": "^7.10.5",
|
||||||
"@babel/plugin-transform-react-inline-elements": "^7.10.4",
|
"@babel/plugin-transform-react-inline-elements": "^7.10.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.11.0",
|
"@babel/plugin-transform-runtime": "^7.11.5",
|
||||||
"@babel/preset-env": "^7.11.0",
|
"@babel/preset-env": "^7.11.0",
|
||||||
"@babel/preset-react": "^7.10.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"@babel/runtime": "^7.11.2",
|
"@babel/runtime": "^7.11.2",
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.26.10",
|
"sass": "^1.26.10",
|
||||||
"sass-loader": "^9.0.3",
|
"sass-loader": "^10.0.2",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"stringz": "^2.1.0",
|
"stringz": "^2.1.0",
|
||||||
"substring-trie": "^1.0.2",
|
"substring-trie": "^1.0.2",
|
||||||
|
|
|
@ -29,14 +29,14 @@ RSpec.describe FeedManager do
|
||||||
it 'returns false for followee\'s status' do
|
it 'returns false for followee\'s status' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, status, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reblog by followee' do
|
it 'returns false for reblog by followee' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog by followee of blocked account' do
|
it 'returns true for reblog by followee of blocked account' do
|
||||||
|
@ -44,7 +44,7 @@ RSpec.describe FeedManager do
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog by followee of muted account' do
|
it 'returns true for reblog by followee of muted account' do
|
||||||
|
@ -52,7 +52,7 @@ RSpec.describe FeedManager do
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
bob.mute!(jeff)
|
bob.mute!(jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog by followee of someone who is blocking recipient' do
|
it 'returns true for reblog by followee of someone who is blocking recipient' do
|
||||||
|
@ -60,14 +60,14 @@ RSpec.describe FeedManager do
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
jeff.block!(bob)
|
jeff.block!(bob)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog from account with reblogs disabled' do
|
it 'returns true for reblog from account with reblogs disabled' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||||
bob.follow!(alice, reblogs: false)
|
bob.follow!(alice, reblogs: false)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reply by followee to another followee' do
|
it 'returns false for reply by followee to another followee' do
|
||||||
|
@ -75,55 +75,55 @@ RSpec.describe FeedManager do
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
bob.follow!(jeff)
|
bob.follow!(jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reply by followee to recipient' do
|
it 'returns false for reply by followee to recipient' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for reply by followee to self' do
|
it 'returns false for reply by followee to self' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reply by followee to non-followed account' do
|
it 'returns true for reply by followee to non-followed account' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reply, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for the second reply by followee to a non-federated status' do
|
it 'returns true for the second reply by followee to a non-federated status' do
|
||||||
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
|
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
|
||||||
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
|
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:home, second_reply, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, second_reply, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for status by followee mentioning another account' do
|
it 'returns false for status by followee mentioning another account' do
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, status, bob)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status by followee mentioning blocked account' do
|
it 'returns true for status by followee mentioning blocked account' do
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status by followee mentioning muted account' do
|
it 'returns true for status by followee mentioning muted account' do
|
||||||
bob.mute!(jeff)
|
bob.mute!(jeff)
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:home, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for reblog of a personally blocked domain' do
|
it 'returns true for reblog of a personally blocked domain' do
|
||||||
|
@ -131,7 +131,7 @@ RSpec.describe FeedManager do
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for irreversibly muted phrases' do
|
context 'for irreversibly muted phrases' do
|
||||||
|
@ -139,7 +139,7 @@ RSpec.describe FeedManager do
|
||||||
alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true)
|
alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true)
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'bobcats', account: jeff)
|
status = Fabricate(:status, text: 'bobcats', account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be_falsy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true if phrase is contained' do
|
it 'returns true if phrase is contained' do
|
||||||
|
@ -147,14 +147,14 @@ RSpec.describe FeedManager do
|
||||||
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
|
status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches substrings if whole_word is false' do
|
it 'matches substrings if whole_word is false' do
|
||||||
alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true)
|
alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true)
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'shiitake', account: jeff)
|
status = Fabricate(:status, text: 'shiitake', account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true if phrase is contained in a poll option' do
|
it 'returns true if phrase is contained in a poll option' do
|
||||||
|
@ -162,7 +162,7 @@ RSpec.describe FeedManager do
|
||||||
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'what do you prefer', poll: Fabricate(:poll, options: %w(farts POP TARts)), account: jeff)
|
status = Fabricate(:status, text: 'what do you prefer', poll: Fabricate(:poll, options: %w(farts POP TARts)), account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -171,27 +171,27 @@ RSpec.describe FeedManager do
|
||||||
it 'returns true for status that mentions blocked account' do
|
it 'returns true for status that mentions blocked account' do
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status that replies to a blocked account' do
|
it 'returns true for status that replies to a blocked account' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||||
bob.block!(jeff)
|
bob.block!(jeff)
|
||||||
expect(FeedManager.instance.filter?(:mentions, reply, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:mentions, reply, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true for status by silenced account who recipient is not following' do
|
it 'returns true for status by silenced account who recipient is not following' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
alice.silence!
|
alice.silence!
|
||||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false for status by followed silenced account' do
|
it 'returns false for status by followed silenced account' do
|
||||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||||
alice.silence!
|
alice.silence!
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:mentions, status, bob)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -421,52 +421,20 @@ RSpec.describe FeedManager do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#merge_into_timeline' do
|
describe '#merge_into_home' do
|
||||||
it "does not push source account's statuses whose reblogs are already inserted" do
|
it "does not push source account's statuses whose reblogs are already inserted" do
|
||||||
account = Fabricate(:account, id: 0)
|
account = Fabricate(:account, id: 0)
|
||||||
reblog = Fabricate(:status)
|
reblog = Fabricate(:status)
|
||||||
status = Fabricate(:status, reblog: reblog)
|
status = Fabricate(:status, reblog: reblog)
|
||||||
FeedManager.instance.push_to_home(account, status)
|
FeedManager.instance.push_to_home(account, status)
|
||||||
|
|
||||||
FeedManager.instance.merge_into_timeline(account, reblog.account)
|
FeedManager.instance.merge_into_home(account, reblog.account)
|
||||||
|
|
||||||
expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil
|
expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#trim' do
|
describe '#unpush_from_home' do
|
||||||
let(:receiver) { Fabricate(:account) }
|
|
||||||
|
|
||||||
it 'cleans up reblog tracking keys' do
|
|
||||||
reblogged = Fabricate(:status)
|
|
||||||
status = Fabricate(:status, reblog: reblogged)
|
|
||||||
another_status = Fabricate(:status, reblog: reblogged)
|
|
||||||
reblogs_key = FeedManager.instance.key('home', receiver.id, 'reblogs')
|
|
||||||
reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}")
|
|
||||||
|
|
||||||
FeedManager.instance.push_to_home(receiver, status)
|
|
||||||
FeedManager.instance.push_to_home(receiver, another_status)
|
|
||||||
|
|
||||||
# We should have a tracking set and an entry in reblogs.
|
|
||||||
expect(Redis.current.exists?(reblog_set_key)).to be true
|
|
||||||
expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s]
|
|
||||||
|
|
||||||
# Push everything off the end of the feed.
|
|
||||||
FeedManager::MAX_ITEMS.times do
|
|
||||||
FeedManager.instance.push_to_home(receiver, Fabricate(:status))
|
|
||||||
end
|
|
||||||
|
|
||||||
# `trim` should be called automatically, but do it anyway, as
|
|
||||||
# we're testing `trim`, not side effects of `push`.
|
|
||||||
FeedManager.instance.trim('home', receiver.id)
|
|
||||||
|
|
||||||
# We should not have any reblog tracking data.
|
|
||||||
expect(Redis.current.exists?(reblog_set_key)).to be false
|
|
||||||
expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#unpush' do
|
|
||||||
let(:receiver) { Fabricate(:account) }
|
let(:receiver) { Fabricate(:account) }
|
||||||
|
|
||||||
it 'leaves a reblogged status if original was on feed' do
|
it 'leaves a reblogged status if original was on feed' do
|
||||||
|
@ -532,7 +500,7 @@ RSpec.describe FeedManager do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#clear_from_timeline' do
|
describe '#clear_from_home' do
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate(:account) }
|
||||||
let(:followed_account) { Fabricate(:account) }
|
let(:followed_account) { Fabricate(:account) }
|
||||||
let(:target_account) { Fabricate(:account) }
|
let(:target_account) { Fabricate(:account) }
|
||||||
|
@ -550,8 +518,8 @@ RSpec.describe FeedManager do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly cleans the timeline' do
|
it 'correctly cleans the home timeline' do
|
||||||
FeedManager.instance.clear_from_timeline(account, target_account)
|
FeedManager.instance.clear_from_home(account, target_account)
|
||||||
|
|
||||||
expect(Redis.current.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s]
|
expect(Redis.current.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s]
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,274 @@
|
||||||
|
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) }
|
||||||
|
let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) }
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
it 'does not include local-only statuses' do
|
||||||
|
expect(subject).not_to include(local_only_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
|
||||||
|
|
||||||
|
it 'does not include local-only statuses' do
|
||||||
|
expect(subject).not_to include(local_only_status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without local_only option but allow_local_only' 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) }
|
||||||
|
let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) }
|
||||||
|
|
||||||
|
subject { described_class.new(viewer, allow_local_only: true).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
|
||||||
|
|
||||||
|
it 'does not include local-only statuses' do
|
||||||
|
expect(subject).not_to include(local_only_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
|
||||||
|
|
||||||
|
it 'includes local-only statuses' do
|
||||||
|
expect(subject).to include(local_only_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) }
|
||||||
|
let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) }
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
it 'does not include local-only statuses' do
|
||||||
|
expect(subject).not_to include(local_only_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
|
||||||
|
|
||||||
|
it 'includes local-only statuses' do
|
||||||
|
expect(subject).to include(local_only_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
|
|
@ -354,288 +354,6 @@ RSpec.describe Status, type: :model do
|
||||||
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
|
|
||||||
|
|
||||||
context 'with local-only statuses' do
|
|
||||||
let(:status) { Fabricate(:status, local_only: true) }
|
|
||||||
|
|
||||||
subject { Status.as_public_timeline(viewer) }
|
|
||||||
|
|
||||||
context 'without a viewer' do
|
|
||||||
let(:viewer) { nil }
|
|
||||||
|
|
||||||
it 'excludes local-only statuses' do
|
|
||||||
expect(subject).to_not include(status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a viewer' do
|
|
||||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
|
||||||
|
|
||||||
it 'includes local-only statuses' do
|
|
||||||
expect(subject).to include(status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: What happens if the viewer is remote?
|
|
||||||
# Can the viewer be remote?
|
|
||||||
# What prevents the viewer from being remote?
|
|
||||||
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
|
|
||||||
|
|
||||||
context 'on a local-only status' do
|
|
||||||
let(:tag) { Fabricate(:tag) }
|
|
||||||
let(:status) { Fabricate(:status, local_only: true, tags: [tag]) }
|
|
||||||
|
|
||||||
context 'without a viewer' do
|
|
||||||
let(:viewer) { nil }
|
|
||||||
|
|
||||||
it 'filters the local-only status out of the result set' do
|
|
||||||
expect(Status.as_tag_timeline(tag, viewer)).not_to include(status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a viewer' do
|
|
||||||
let(:viewer) { Fabricate(:account, username: 'viewer', domain: nil) }
|
|
||||||
|
|
||||||
it 'keeps the local-only status in the result set' do
|
|
||||||
expect(Status.as_tag_timeline(tag, viewer)).to include(status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.permitted_for' do
|
describe '.permitted_for' do
|
||||||
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
|
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe HashtagQueryService, type: :service do
|
describe TagFeed, type: :service do
|
||||||
describe '.call' do
|
describe '#get' do
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate(:account) }
|
||||||
let(:tag1) { Fabricate(:tag) }
|
let(:tag1) { Fabricate(:tag) }
|
||||||
let(:tag2) { Fabricate(:tag) }
|
let(:tag2) { Fabricate(:tag) }
|
||||||
|
@ -10,35 +10,35 @@ describe HashtagQueryService, type: :service do
|
||||||
let!(:both) { Fabricate(:status, tags: [tag1, tag2]) }
|
let!(:both) { Fabricate(:status, tags: [tag1, tag2]) }
|
||||||
|
|
||||||
it 'can add tags in "any" mode' do
|
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 status1
|
||||||
expect(results).to include status2
|
expect(results).to include status2
|
||||||
expect(results).to include both
|
expect(results).to include both
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can remove tags in "all" mode' do
|
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 status1
|
||||||
expect(results).to_not include status2
|
expect(results).to_not include status2
|
||||||
expect(results).to include both
|
expect(results).to include both
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can remove tags in "none" mode' do
|
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 include status1
|
||||||
expect(results).to_not include status2
|
expect(results).to_not include status2
|
||||||
expect(results).to_not include both
|
expect(results).to_not include both
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'ignores an invalid mode' do
|
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 include status1
|
||||||
expect(results).to_not include status2
|
expect(results).to_not include status2
|
||||||
expect(results).to include both
|
expect(results).to include both
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'handles being passed non existant tag names' do
|
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 include status1
|
||||||
expect(results).to_not include status2
|
expect(results).to_not include status2
|
||||||
expect(results).to include both
|
expect(results).to include both
|
||||||
|
@ -46,15 +46,37 @@ describe HashtagQueryService, type: :service do
|
||||||
|
|
||||||
it 'can restrict to an account' do
|
it 'can restrict to an account' do
|
||||||
BlockService.new.call(account, status1.account)
|
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
|
expect(results).to_not include status1
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can restrict to local' do
|
it 'can restrict to local' do
|
||||||
status1.account.update(domain: 'example.com')
|
status1.account.update(domain: 'example.com')
|
||||||
status1.update(local: false, uri: 'example.com/toot')
|
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
|
expect(results).to_not include status1
|
||||||
end
|
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
|
||||||
|
|
||||||
|
context 'on a local-only status' do
|
||||||
|
let!(:status) { Fabricate(:status, tags: [tag1], local_only: true) }
|
||||||
|
|
||||||
|
it 'does not show local-only statuses without a viewer' do
|
||||||
|
results = described_class.new(tag1, nil).get(20)
|
||||||
|
expect(results).to_not include(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows local-only statuses given a viewer' do
|
||||||
|
results = described_class.new(tag1, account).get(20)
|
||||||
|
expect(results).to include(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -28,10 +28,10 @@ RSpec.describe FanOutOnWriteService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'delivers status to hashtag' do
|
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
|
end
|
||||||
|
|
||||||
it 'delivers status to public timeline' do
|
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
|
||||||
end
|
end
|
||||||
|
|
212
yarn.lock
212
yarn.lock
|
@ -18,19 +18,19 @@
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
semver "^5.5.0"
|
semver "^5.5.0"
|
||||||
|
|
||||||
"@babel/core@^7.1.0", "@babel/core@^7.7.5":
|
"@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
|
||||||
version "7.11.4"
|
version "7.11.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.4.tgz#4301dfdfafa01eeb97f1896c5501a3f0655d4229"
|
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
|
||||||
integrity sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg==
|
integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.10.4"
|
"@babel/code-frame" "^7.10.4"
|
||||||
"@babel/generator" "^7.11.4"
|
"@babel/generator" "^7.11.6"
|
||||||
"@babel/helper-module-transforms" "^7.11.0"
|
"@babel/helper-module-transforms" "^7.11.0"
|
||||||
"@babel/helpers" "^7.10.4"
|
"@babel/helpers" "^7.10.4"
|
||||||
"@babel/parser" "^7.11.4"
|
"@babel/parser" "^7.11.5"
|
||||||
"@babel/template" "^7.10.4"
|
"@babel/template" "^7.10.4"
|
||||||
"@babel/traverse" "^7.11.0"
|
"@babel/traverse" "^7.11.5"
|
||||||
"@babel/types" "^7.11.0"
|
"@babel/types" "^7.11.5"
|
||||||
convert-source-map "^1.7.0"
|
convert-source-map "^1.7.0"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
gensync "^1.0.0-beta.1"
|
gensync "^1.0.0-beta.1"
|
||||||
|
@ -40,34 +40,12 @@
|
||||||
semver "^5.4.1"
|
semver "^5.4.1"
|
||||||
source-map "^0.5.0"
|
source-map "^0.5.0"
|
||||||
|
|
||||||
"@babel/core@^7.11.1", "@babel/core@^7.7.2":
|
"@babel/generator@^7.11.5", "@babel/generator@^7.11.6":
|
||||||
version "7.11.1"
|
version "7.11.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643"
|
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
|
||||||
integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ==
|
integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.10.4"
|
"@babel/types" "^7.11.5"
|
||||||
"@babel/generator" "^7.11.0"
|
|
||||||
"@babel/helper-module-transforms" "^7.11.0"
|
|
||||||
"@babel/helpers" "^7.10.4"
|
|
||||||
"@babel/parser" "^7.11.1"
|
|
||||||
"@babel/template" "^7.10.4"
|
|
||||||
"@babel/traverse" "^7.11.0"
|
|
||||||
"@babel/types" "^7.11.0"
|
|
||||||
convert-source-map "^1.7.0"
|
|
||||||
debug "^4.1.0"
|
|
||||||
gensync "^1.0.0-beta.1"
|
|
||||||
json5 "^2.1.2"
|
|
||||||
lodash "^4.17.19"
|
|
||||||
resolve "^1.3.2"
|
|
||||||
semver "^5.4.1"
|
|
||||||
source-map "^0.5.0"
|
|
||||||
|
|
||||||
"@babel/generator@^7.11.0", "@babel/generator@^7.11.4":
|
|
||||||
version "7.11.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.4.tgz#1ec7eec00defba5d6f83e50e3ee72ae2fee482be"
|
|
||||||
integrity sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.11.0"
|
|
||||||
jsesc "^2.5.1"
|
jsesc "^2.5.1"
|
||||||
source-map "^0.5.0"
|
source-map "^0.5.0"
|
||||||
|
|
||||||
|
@ -311,15 +289,10 @@
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.4":
|
"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.7.0":
|
||||||
version "7.11.4"
|
version "7.11.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.4.tgz#6fa1a118b8b0d80d0267b719213dc947e88cc0ca"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
|
||||||
integrity sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==
|
integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
|
||||||
|
|
||||||
"@babel/parser@^7.11.1", "@babel/parser@^7.7.0":
|
|
||||||
version "7.11.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9"
|
|
||||||
integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==
|
|
||||||
|
|
||||||
"@babel/plugin-proposal-async-generator-functions@^7.10.4":
|
"@babel/plugin-proposal-async-generator-functions@^7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
|
@ -806,10 +779,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.10.4"
|
"@babel/helper-plugin-utils" "^7.10.4"
|
||||||
|
|
||||||
"@babel/plugin-transform-runtime@^7.11.0":
|
"@babel/plugin-transform-runtime@^7.11.5":
|
||||||
version "7.11.0"
|
version "7.11.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.5.tgz#f108bc8e0cf33c37da031c097d1df470b3a293fc"
|
||||||
integrity sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw==
|
integrity sha512-9aIoee+EhjySZ6vY5hnLjigHzunBlscx9ANKutkeWTJTx6m5Rbq6Ic01tLvO54lSusR+BxV7u4UDdCmXv5aagg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-imports" "^7.10.4"
|
"@babel/helper-module-imports" "^7.10.4"
|
||||||
"@babel/helper-plugin-utils" "^7.10.4"
|
"@babel/helper-plugin-utils" "^7.10.4"
|
||||||
|
@ -998,25 +971,25 @@
|
||||||
"@babel/parser" "^7.10.4"
|
"@babel/parser" "^7.10.4"
|
||||||
"@babel/types" "^7.10.4"
|
"@babel/types" "^7.10.4"
|
||||||
|
|
||||||
"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.7.0":
|
"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5", "@babel/traverse@^7.7.0":
|
||||||
version "7.11.0"
|
version "7.11.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
|
||||||
integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==
|
integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.10.4"
|
"@babel/code-frame" "^7.10.4"
|
||||||
"@babel/generator" "^7.11.0"
|
"@babel/generator" "^7.11.5"
|
||||||
"@babel/helper-function-name" "^7.10.4"
|
"@babel/helper-function-name" "^7.10.4"
|
||||||
"@babel/helper-split-export-declaration" "^7.11.0"
|
"@babel/helper-split-export-declaration" "^7.11.0"
|
||||||
"@babel/parser" "^7.11.0"
|
"@babel/parser" "^7.11.5"
|
||||||
"@babel/types" "^7.11.0"
|
"@babel/types" "^7.11.5"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
|
|
||||||
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
|
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
|
||||||
version "7.11.0"
|
version "7.11.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d"
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
|
||||||
integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==
|
integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-validator-identifier" "^7.10.4"
|
"@babel/helper-validator-identifier" "^7.10.4"
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
|
@ -1493,10 +1466,10 @@
|
||||||
jest-diff "^25.2.1"
|
jest-diff "^25.2.1"
|
||||||
pretty-format "^25.2.1"
|
pretty-format "^25.2.1"
|
||||||
|
|
||||||
"@types/json-schema@^7.0.4":
|
"@types/json-schema@^7.0.5":
|
||||||
version "7.0.4"
|
version "7.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
||||||
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
|
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
|
||||||
|
|
||||||
"@types/json5@^0.0.29":
|
"@types/json5@^0.0.29":
|
||||||
version "0.0.29"
|
version "0.0.29"
|
||||||
|
@ -1798,6 +1771,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
|
||||||
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
|
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
|
||||||
|
|
||||||
|
ajv-keywords@^3.5.2:
|
||||||
|
version "3.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
||||||
|
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
||||||
|
|
||||||
ajv@^4.7.0:
|
ajv@^4.7.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
||||||
|
@ -1806,7 +1784,7 @@ ajv@^4.7.0:
|
||||||
co "^4.6.0"
|
co "^4.6.0"
|
||||||
json-stable-stringify "^1.0.1"
|
json-stable-stringify "^1.0.1"
|
||||||
|
|
||||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.9.1:
|
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.9.1:
|
||||||
version "6.12.4"
|
version "6.12.4"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
|
||||||
integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==
|
integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==
|
||||||
|
@ -2528,12 +2506,12 @@ browserify-zlib@^0.2.0:
|
||||||
pako "~1.0.5"
|
pako "~1.0.5"
|
||||||
|
|
||||||
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5:
|
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5:
|
||||||
version "4.14.0"
|
version "4.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.0.tgz#2908951abfe4ec98737b72f34c3bcedc8d43b000"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.1.tgz#cb2b490ba881d45dc3039078c7ed04411eaf3fa3"
|
||||||
integrity sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==
|
integrity sha512-zyBTIHydW37pnb63c7fHFXUG6EcqWOqoMdDx6cdyaDFriZ20EoVxcE95S54N+heRqY8m8IUgB5zYta/gCwSaaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite "^1.0.30001111"
|
caniuse-lite "^1.0.30001124"
|
||||||
electron-to-chromium "^1.3.523"
|
electron-to-chromium "^1.3.562"
|
||||||
escalade "^3.0.2"
|
escalade "^3.0.2"
|
||||||
node-releases "^1.1.60"
|
node-releases "^1.1.60"
|
||||||
|
|
||||||
|
@ -2703,10 +2681,10 @@ caniuse-api@^3.0.0:
|
||||||
lodash.memoize "^4.1.2"
|
lodash.memoize "^4.1.2"
|
||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001124:
|
||||||
version "1.0.30001120"
|
version "1.0.30001124"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001120.tgz#cd21d35e537214e19f7b9f4f161f7b0f2710d46c"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz#5d9998190258e11630d674fc50ea8e579ae0ced2"
|
||||||
integrity sha512-JBP68okZs1X8D7MQTY602jxMYBmXEKOFkzTBaNSkubooMPFOAv2TXWaKle7qgHpjLDhUzA/TMT0qsNleVyXGUQ==
|
integrity sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA==
|
||||||
|
|
||||||
capture-exit@^2.0.0:
|
capture-exit@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
@ -3826,10 +3804,10 @@ ejs@^2.3.4, ejs@^2.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
|
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
|
||||||
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
|
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
|
||||||
|
|
||||||
electron-to-chromium@^1.3.523:
|
electron-to-chromium@^1.3.562:
|
||||||
version "1.3.545"
|
version "1.3.562"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.545.tgz#d9add694c78554b8c00bc6e6fc929d5ccd7d1b99"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.562.tgz#79c20277ee1c8d0173a22af00e38433b752bc70f"
|
||||||
integrity sha512-+0R/i17u5E1cwF3g0W8Niq3UUKTUMyyT4kLkutZUHG8mDNvFsAckK3HIanzGVtixe3b6rknD8k7gHiR6nKFkgg==
|
integrity sha512-WhRe6liQ2q/w1MZc8mD8INkenHivuHdrr4r5EQHNomy3NJux+incP6M6lDMd0paShP3MD0WGe5R1TWmEClf+Bg==
|
||||||
|
|
||||||
elliptic@^6.5.3:
|
elliptic@^6.5.3:
|
||||||
version "6.5.3"
|
version "6.5.3"
|
||||||
|
@ -4298,21 +4276,21 @@ esquery@^1.2.0:
|
||||||
estraverse "^5.1.0"
|
estraverse "^5.1.0"
|
||||||
|
|
||||||
esrecurse@^4.1.0:
|
esrecurse@^4.1.0:
|
||||||
version "4.2.1"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
|
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
|
||||||
integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
|
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
|
||||||
dependencies:
|
dependencies:
|
||||||
estraverse "^4.1.0"
|
estraverse "^5.2.0"
|
||||||
|
|
||||||
estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
estraverse@^4.1.1, estraverse@^4.2.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||||
|
|
||||||
estraverse@^5.1.0:
|
estraverse@^5.1.0, estraverse@^5.2.0:
|
||||||
version "5.1.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
|
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
|
||||||
integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
|
integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
|
||||||
|
|
||||||
esutils@^2.0.2:
|
esutils@^2.0.2:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
|
@ -4333,9 +4311,9 @@ event-emitter@~0.3.5:
|
||||||
es5-ext "~0.10.14"
|
es5-ext "~0.10.14"
|
||||||
|
|
||||||
eventemitter3@^4.0.0:
|
eventemitter3@^4.0.0:
|
||||||
version "4.0.5"
|
version "4.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.5.tgz#51d81e4f1ccc8311a04f0c20121ea824377ea6d9"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||||
integrity sha512-QR0rh0YiPuxuDQ6+T9GAO/xWTExXpxIes1Nl9RykNGTnE1HJmkuEfxJH9cubjIOQZ/GH4qNBR4u8VSHaKiWs4g==
|
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||||
|
|
||||||
events@^3.0.0:
|
events@^3.0.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
|
@ -6618,10 +6596,10 @@ kleur@^3.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
||||||
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||||
|
|
||||||
klona@^1.1.2:
|
klona@^2.0.3:
|
||||||
version "1.1.2"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/klona/-/klona-1.1.2.tgz#a79e292518a5a5412ec8d097964bff1571a64db0"
|
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.3.tgz#98274552c513583ad7a01456a789a2a0b4a2a538"
|
||||||
integrity sha512-xf88rTeHiXk+XE2Vhi6yj8Wm3gMZrygGdKjJqN8HkV+PwF/t50/LdAKHoHpPcxFAlmQszTZ1CugrK25S7qDRLA==
|
integrity sha512-CgPOT3ZadDpXxKcfV56lEQ9OQSZ42Mk26gnozI+uN/k39vzD8toUhRQoqsX0m9Q3eMPEfsLWmtyUpK/yqST4yg==
|
||||||
|
|
||||||
knot.js@^1.1.5:
|
knot.js@^1.1.5:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
|
@ -7071,7 +7049,7 @@ minipass@^3.0.0, minipass@^3.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
minizlib@^2.1.0:
|
minizlib@^2.1.1:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||||
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
|
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
|
||||||
|
@ -7977,9 +7955,9 @@ posix-character-classes@^0.1.0:
|
||||||
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
|
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
|
||||||
|
|
||||||
postcss-calc@^7.0.1:
|
postcss-calc@^7.0.1:
|
||||||
version "7.0.3"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.3.tgz#d65cca92a3c52bf27ad37a5f732e0587b74f1623"
|
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.4.tgz#5e177ddb417341e6d4a193c5d9fd8ada79094f8b"
|
||||||
integrity sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA==
|
integrity sha512-0I79VRAd1UTkaHzY9w83P39YGO/M3bG7/tNLrHGEunBolfoGM0hSjrGvjoeaj0JE/zIw5GsI2KZ0UwDJqv5hjw==
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.27"
|
postcss "^7.0.27"
|
||||||
postcss-selector-parser "^6.0.2"
|
postcss-selector-parser "^6.0.2"
|
||||||
|
@ -8341,9 +8319,9 @@ postgres-bytea@~1.0.0:
|
||||||
integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
|
integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
|
||||||
|
|
||||||
postgres-date@~1.0.0:
|
postgres-date@~1.0.0:
|
||||||
version "1.0.6"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.6.tgz#4925e8085b30c2ba1a06ac91b9a3473954a2ce2d"
|
resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
|
||||||
integrity sha512-o2a4gxeFcox+CgB3Ig/kNHBP23PiEXHCXx7pcIIsvzoNz4qv+lKTyiSkjOXIMNUl12MO/mOYl2K6wR9X5K6Plg==
|
integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
|
||||||
|
|
||||||
postgres-interval@^1.1.0:
|
postgres-interval@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
|
@ -9401,15 +9379,15 @@ sass-lint@^1.13.1:
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
util "^0.10.3"
|
util "^0.10.3"
|
||||||
|
|
||||||
sass-loader@^9.0.3:
|
sass-loader@^10.0.2:
|
||||||
version "9.0.3"
|
version "10.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-9.0.3.tgz#086adcf0bfdcc9d920413e2cdc3ba3321373d547"
|
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.2.tgz#c7b73010848b264792dd45372eea0b87cba4401e"
|
||||||
integrity sha512-fOwsP98ac1VMme+V3+o0HaaMHp8Q/C9P+MUazLFVi3Jl7ORGHQXL1XeRZt3zLSGZQQPC8xE42Y2WptItvGjDQg==
|
integrity sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg==
|
||||||
dependencies:
|
dependencies:
|
||||||
klona "^1.1.2"
|
klona "^2.0.3"
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
neo-async "^2.6.2"
|
neo-async "^2.6.2"
|
||||||
schema-utils "^2.7.0"
|
schema-utils "^2.7.1"
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
|
|
||||||
sass@^1.26.10:
|
sass@^1.26.10:
|
||||||
|
@ -9448,14 +9426,14 @@ schema-utils@^1.0.0:
|
||||||
ajv-errors "^1.0.0"
|
ajv-errors "^1.0.0"
|
||||||
ajv-keywords "^3.1.0"
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
schema-utils@^2.2.0, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0:
|
schema-utils@^2.2.0, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0, schema-utils@^2.7.1:
|
||||||
version "2.7.0"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
||||||
integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
|
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.4"
|
"@types/json-schema" "^7.0.5"
|
||||||
ajv "^6.12.2"
|
ajv "^6.12.4"
|
||||||
ajv-keywords "^3.4.1"
|
ajv-keywords "^3.5.2"
|
||||||
|
|
||||||
scroll-behavior@^0.9.1:
|
scroll-behavior@^0.9.1:
|
||||||
version "0.9.12"
|
version "0.9.12"
|
||||||
|
@ -10264,14 +10242,14 @@ tapable@^1.0.0, tapable@^1.1.3:
|
||||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
||||||
|
|
||||||
tar@^6.0.2:
|
tar@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
|
||||||
integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==
|
integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr "^2.0.0"
|
chownr "^2.0.0"
|
||||||
fs-minipass "^2.0.0"
|
fs-minipass "^2.0.0"
|
||||||
minipass "^3.0.0"
|
minipass "^3.0.0"
|
||||||
minizlib "^2.1.0"
|
minizlib "^2.1.1"
|
||||||
mkdirp "^1.0.3"
|
mkdirp "^1.0.3"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue