Merge pull request #1419 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						6e83020950
					
				
							
								
								
									
										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