Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `Gemfile.lock`: Not a real conflict, upstream-updated dependency (redis) textually too close to glitch-soc-only dependecy. Updated redis gem like upstream did.
This commit is contained in:
		
						commit
						8ec4be4233
					
				
							
								
								
									
										12
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Gemfile
									
									
									
									
									
								
							| @ -5,7 +5,7 @@ ruby '>= 2.5.0', '< 3.1.0' | ||||
| 
 | ||||
| gem 'pkg-config', '~> 1.4' | ||||
| 
 | ||||
| gem 'puma', '~> 5.3' | ||||
| gem 'puma', '~> 5.4' | ||||
| gem 'rails', '~> 6.1.4' | ||||
| gem 'sprockets', '~> 3.7.2' | ||||
| gem 'thor', '~> 1.1' | ||||
| @ -17,7 +17,7 @@ gem 'makara', '~> 0.5' | ||||
| gem 'pghero', '~> 2.8' | ||||
| gem 'dotenv-rails', '~> 2.7' | ||||
| 
 | ||||
| gem 'aws-sdk-s3', '~> 1.96', require: false | ||||
| gem 'aws-sdk-s3', '~> 1.98', require: false | ||||
| gem 'fog-core', '<= 2.1.0' | ||||
| gem 'fog-openstack', '~> 0.3', require: false | ||||
| gem 'paperclip', '~> 6.0' | ||||
| @ -60,7 +60,7 @@ gem 'idn-ruby', require: 'idn' | ||||
| gem 'kaminari', '~> 1.2' | ||||
| gem 'link_header', '~> 0.0' | ||||
| gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar' | ||||
| gem 'nokogiri', '~> 1.11' | ||||
| gem 'nokogiri', '~> 1.12' | ||||
| gem 'nsa', '~> 0.2' | ||||
| gem 'oj', '~> 3.12' | ||||
| gem 'ox', '~> 2.14' | ||||
| @ -73,11 +73,11 @@ gem 'rack-attack', '~> 6.5' | ||||
| gem 'rack-cors', '~> 1.1', require: 'rack/cors' | ||||
| gem 'rails-i18n', '~> 6.0' | ||||
| gem 'rails-settings-cached', '~> 0.6' | ||||
| gem 'redis', '~> 4.3', require: ['redis', 'redis/connection/hiredis'] | ||||
| gem 'redis', '~> 4.4', require: ['redis', 'redis/connection/hiredis'] | ||||
| gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' | ||||
| gem 'rqrcode', '~> 2.0' | ||||
| gem 'ruby-progressbar', '~> 1.11' | ||||
| gem 'sanitize', '~> 5.2' | ||||
| gem 'sanitize', '~> 6.0' | ||||
| gem 'scenic', '~> 1.5' | ||||
| gem 'sidekiq', '~> 6.2' | ||||
| gem 'sidekiq-scheduler', '~> 3.1' | ||||
| @ -138,7 +138,7 @@ group :development do | ||||
|   gem 'memory_profiler' | ||||
|   gem 'rubocop', '~> 1.18', require: false | ||||
|   gem 'rubocop-rails', '~> 2.11', require: false | ||||
|   gem 'brakeman', '~> 5.0', require: false | ||||
|   gem 'brakeman', '~> 5.1', require: false | ||||
|   gem 'bundler-audit', '~> 0.8', require: false | ||||
| 
 | ||||
|   gem 'capistrano', '~> 3.16' | ||||
|  | ||||
							
								
								
									
										55
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @ -79,20 +79,20 @@ GEM | ||||
|       encryptor (~> 3.0.0) | ||||
|     awrence (1.1.1) | ||||
|     aws-eventstream (1.1.1) | ||||
|     aws-partitions (1.467.0) | ||||
|     aws-sdk-core (3.114.2) | ||||
|     aws-partitions (1.482.0) | ||||
|     aws-sdk-core (3.119.0) | ||||
|       aws-eventstream (~> 1, >= 1.0.2) | ||||
|       aws-partitions (~> 1, >= 1.239.0) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|       jmespath (~> 1.0) | ||||
|     aws-sdk-kms (1.43.0) | ||||
|       aws-sdk-core (~> 3, >= 3.112.0) | ||||
|     aws-sdk-kms (1.46.0) | ||||
|       aws-sdk-core (~> 3, >= 3.119.0) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|     aws-sdk-s3 (1.96.1) | ||||
|       aws-sdk-core (~> 3, >= 3.112.0) | ||||
|     aws-sdk-s3 (1.98.0) | ||||
|       aws-sdk-core (~> 3, >= 3.119.0) | ||||
|       aws-sdk-kms (~> 1) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|     aws-sigv4 (1.2.3) | ||||
|     aws-sigv4 (1.2.4) | ||||
|       aws-eventstream (~> 1, >= 1.0.2) | ||||
|     bcrypt (3.1.16) | ||||
|     better_errors (2.9.1) | ||||
| @ -106,7 +106,7 @@ GEM | ||||
|       ffi (~> 1.14) | ||||
|     bootsnap (1.6.0) | ||||
|       msgpack (~> 1.0) | ||||
|     brakeman (5.0.4) | ||||
|     brakeman (5.1.1) | ||||
|     browser (4.2.0) | ||||
|     brpoplpush-redis_script (0.1.2) | ||||
|       concurrent-ruby (~> 1.0, >= 1.0.5) | ||||
| @ -354,7 +354,7 @@ GEM | ||||
|       nokogiri (~> 1) | ||||
|       rake | ||||
|     mini_mime (1.1.0) | ||||
|     mini_portile2 (2.5.3) | ||||
|     mini_portile2 (2.6.1) | ||||
|     minitest (5.14.4) | ||||
|     msgpack (1.4.2) | ||||
|     multi_json (1.15.0) | ||||
| @ -364,17 +364,15 @@ GEM | ||||
|       net-ssh (>= 2.6.5, < 7.0.0) | ||||
|     net-ssh (6.1.0) | ||||
|     nio4r (2.5.7) | ||||
|     nokogiri (1.11.7) | ||||
|       mini_portile2 (~> 2.5.0) | ||||
|     nokogiri (1.12.2) | ||||
|       mini_portile2 (~> 2.6.1) | ||||
|       racc (~> 1.4) | ||||
|     nokogumbo (2.0.4) | ||||
|       nokogiri (~> 1.8, >= 1.8.4) | ||||
|     nsa (0.2.8) | ||||
|       activesupport (>= 4.2, < 7) | ||||
|       concurrent-ruby (~> 1.0, >= 1.0.2) | ||||
|       sidekiq (>= 3.5) | ||||
|       statsd-ruby (~> 1.4, >= 1.4.0) | ||||
|     oj (3.12.1) | ||||
|     oj (3.12.2) | ||||
|     omniauth (1.9.1) | ||||
|       hashie (>= 3.4.6) | ||||
|       rack (>= 1.6.2, < 3) | ||||
| @ -428,7 +426,7 @@ GEM | ||||
|     pry-rails (0.3.9) | ||||
|       pry (>= 0.10.4) | ||||
|     public_suffix (4.0.6) | ||||
|     puma (5.3.2) | ||||
|     puma (5.4.0) | ||||
|       nio4r (~> 2.0) | ||||
|     pundit (2.1.0) | ||||
|       activesupport (>= 3.0.0) | ||||
| @ -486,7 +484,7 @@ GEM | ||||
|     rdf-normalize (0.4.0) | ||||
|       rdf (~> 3.1) | ||||
|     redcarpet (3.5.1) | ||||
|     redis (4.3.1) | ||||
|     redis (4.4.0) | ||||
|     redis-namespace (1.8.1) | ||||
|       redis (>= 3.0.4) | ||||
|     regexp_parser (2.1.1) | ||||
| @ -524,16 +522,16 @@ GEM | ||||
|     rspec-support (3.10.2) | ||||
|     rspec_junit_formatter (0.4.1) | ||||
|       rspec-core (>= 2, < 4, != 2.12.0) | ||||
|     rubocop (1.18.3) | ||||
|     rubocop (1.18.4) | ||||
|       parallel (~> 1.10) | ||||
|       parser (>= 3.0.0.0) | ||||
|       rainbow (>= 2.2.2, < 4.0) | ||||
|       regexp_parser (>= 1.8, < 3.0) | ||||
|       rexml | ||||
|       rubocop-ast (>= 1.7.0, < 2.0) | ||||
|       rubocop-ast (>= 1.8.0, < 2.0) | ||||
|       ruby-progressbar (~> 1.7) | ||||
|       unicode-display_width (>= 1.4.0, < 3.0) | ||||
|     rubocop-ast (1.7.0) | ||||
|     rubocop-ast (1.8.0) | ||||
|       parser (>= 3.0.1.1) | ||||
|     rubocop-rails (2.11.3) | ||||
|       activesupport (>= 4.2.0) | ||||
| @ -547,10 +545,9 @@ GEM | ||||
|       fugit (~> 1.1, >= 1.1.6) | ||||
|     safety_net_attestation (0.4.0) | ||||
|       jwt (~> 2.0) | ||||
|     sanitize (5.2.3) | ||||
|     sanitize (6.0.0) | ||||
|       crass (~> 1.0.2) | ||||
|       nokogiri (>= 1.8.0) | ||||
|       nokogumbo (~> 2.0) | ||||
|       nokogiri (>= 1.12.0) | ||||
|     scenic (1.5.4) | ||||
|       activerecord (>= 4.0.0) | ||||
|       railties (>= 4.0.0) | ||||
| @ -569,7 +566,7 @@ GEM | ||||
|       sidekiq (>= 3) | ||||
|       thwait | ||||
|       tilt (>= 1.4.0) | ||||
|     sidekiq-unique-jobs (7.1.2) | ||||
|     sidekiq-unique-jobs (7.1.5) | ||||
|       brpoplpush-redis_script (> 0.1.1, <= 2.0.0) | ||||
|       concurrent-ruby (~> 1.0, >= 1.0.5) | ||||
|       sidekiq (>= 5.0, < 7.0) | ||||
| @ -675,12 +672,12 @@ DEPENDENCIES | ||||
|   active_record_query_trace (~> 1.8) | ||||
|   addressable (~> 2.8) | ||||
|   annotate (~> 3.1) | ||||
|   aws-sdk-s3 (~> 1.96) | ||||
|   aws-sdk-s3 (~> 1.98) | ||||
|   better_errors (~> 2.9) | ||||
|   binding_of_caller (~> 1.0) | ||||
|   blurhash (~> 0.1) | ||||
|   bootsnap (~> 1.6.0) | ||||
|   brakeman (~> 5.0) | ||||
|   brakeman (~> 5.1) | ||||
|   browser | ||||
|   bullet (~> 6.1) | ||||
|   bundler-audit (~> 0.8) | ||||
| @ -732,7 +729,7 @@ DEPENDENCIES | ||||
|   microformats (~> 4.2) | ||||
|   mime-types (~> 3.3.1) | ||||
|   net-ldap (~> 0.17) | ||||
|   nokogiri (~> 1.11) | ||||
|   nokogiri (~> 1.12) | ||||
|   nsa (~> 0.2) | ||||
|   oj (~> 3.12) | ||||
|   omniauth (~> 1.9) | ||||
| @ -752,7 +749,7 @@ DEPENDENCIES | ||||
|   private_address_check (~> 0.5) | ||||
|   pry-byebug (~> 3.9) | ||||
|   pry-rails (~> 0.3) | ||||
|   puma (~> 5.3) | ||||
|   puma (~> 5.4) | ||||
|   pundit (~> 2.1) | ||||
|   rack (~> 2.2.3) | ||||
|   rack-attack (~> 6.5) | ||||
| @ -763,7 +760,7 @@ DEPENDENCIES | ||||
|   rails-settings-cached (~> 0.6) | ||||
|   rdf-normalize (~> 0.4) | ||||
|   redcarpet (~> 3.5) | ||||
|   redis (~> 4.3) | ||||
|   redis (~> 4.4) | ||||
|   redis-namespace (~> 1.8) | ||||
|   rqrcode (~> 2.0) | ||||
|   rspec-rails (~> 5.0) | ||||
| @ -772,7 +769,7 @@ DEPENDENCIES | ||||
|   rubocop (~> 1.18) | ||||
|   rubocop-rails (~> 2.11) | ||||
|   ruby-progressbar (~> 1.11) | ||||
|   sanitize (~> 5.2) | ||||
|   sanitize (~> 6.0) | ||||
|   scenic (~> 1.5) | ||||
|   sidekiq (~> 6.2) | ||||
|   sidekiq-bulk (~> 0.2.0) | ||||
|  | ||||
							
								
								
									
										35
									
								
								app/controllers/statuses_cleanup_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/controllers/statuses_cleanup_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class StatusesCleanupController < ApplicationController | ||||
|   layout 'admin' | ||||
| 
 | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_policy | ||||
|   before_action :set_body_classes | ||||
| 
 | ||||
|   def show; end | ||||
| 
 | ||||
|   def update | ||||
|     if @policy.update(resource_params) | ||||
|       redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg') | ||||
|     else | ||||
|       render action: :show | ||||
|     end | ||||
|   rescue ActionController::ParameterMissing | ||||
|     # Do nothing | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def set_policy | ||||
|     @policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false) | ||||
|   end | ||||
| 
 | ||||
|   def resource_params | ||||
|     params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs) | ||||
|   end | ||||
| 
 | ||||
|   def set_body_classes | ||||
|     @body_classes = 'admin' | ||||
|   end | ||||
| end | ||||
| @ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
|     dispatch(openModal('CONFIRM', { | ||||
|       message: intl.formatMessage(messages.logoutMessage), | ||||
|       confirm: intl.formatMessage(messages.logoutConfirm), | ||||
|       closeWhenConfirm: false, | ||||
|       onConfirm: () => logOut(), | ||||
|     })); | ||||
|   }, | ||||
|  | ||||
| @ -74,6 +74,7 @@ class Compose extends React.PureComponent { | ||||
|     dispatch(openModal('CONFIRM', { | ||||
|       message: intl.formatMessage(messages.logoutMessage), | ||||
|       confirm: intl.formatMessage(messages.logoutConfirm), | ||||
|       closeWhenConfirm: false, | ||||
|       onConfirm: () => logOut(), | ||||
|     })); | ||||
| 
 | ||||
|  | ||||
| @ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent { | ||||
|     onConfirm: PropTypes.func.isRequired, | ||||
|     secondary: PropTypes.string, | ||||
|     onSecondary: PropTypes.func, | ||||
|     closeWhenConfirm: PropTypes.bool, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     closeWhenConfirm: true, | ||||
|   }; | ||||
| 
 | ||||
|   componentDidMount() { | ||||
|     this.button.focus(); | ||||
|   } | ||||
| 
 | ||||
|   handleClick = () => { | ||||
|     if (this.props.closeWhenConfirm) { | ||||
|       this.props.onClose(); | ||||
|     } | ||||
|     this.props.onConfirm(); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
|     dispatch(openModal('CONFIRM', { | ||||
|       message: intl.formatMessage(messages.logoutMessage), | ||||
|       confirm: intl.formatMessage(messages.logoutConfirm), | ||||
|       closeWhenConfirm: false, | ||||
|       onConfirm: () => logOut(), | ||||
|     })); | ||||
|   }, | ||||
|  | ||||
| @ -3022,13 +3022,13 @@ a.account__display-name { | ||||
|     } | ||||
| 
 | ||||
|     @media screen and (max-height: 810px) { | ||||
|       .trends__item:nth-child(3) { | ||||
|       .trends__item:nth-of-type(3) { | ||||
|         display: none; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     @media screen and (max-height: 720px) { | ||||
|       .trends__item:nth-child(2) { | ||||
|       .trends__item:nth-of-type(2) { | ||||
|         display: none; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -452,10 +452,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity | ||||
|   end | ||||
| 
 | ||||
|   def supported_blurhash?(blurhash) | ||||
|     components = blurhash.blank? ? nil : Blurhash.components(blurhash) | ||||
|     components = blurhash.blank? || !blurhash_valid_chars?(blurhash) ? nil : Blurhash.components(blurhash) | ||||
|     components.present? && components.none? { |comp| comp > 5 } | ||||
|   end | ||||
| 
 | ||||
|   def blurhash_valid_chars?(blurhash) | ||||
|     /^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash) | ||||
|   end | ||||
| 
 | ||||
|   def skip_download? | ||||
|     return @skip_download if defined?(@skip_download) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										171
									
								
								app/models/account_statuses_cleanup_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								app/models/account_statuses_cleanup_policy.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,171 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: account_statuses_cleanup_policies | ||||
| # | ||||
| #  id                 :bigint           not null, primary key | ||||
| #  account_id         :bigint           not null | ||||
| #  enabled            :boolean          default(TRUE), not null | ||||
| #  min_status_age     :integer          default(1209600), not null | ||||
| #  keep_direct        :boolean          default(TRUE), not null | ||||
| #  keep_pinned        :boolean          default(TRUE), not null | ||||
| #  keep_polls         :boolean          default(FALSE), not null | ||||
| #  keep_media         :boolean          default(FALSE), not null | ||||
| #  keep_self_fav      :boolean          default(TRUE), not null | ||||
| #  keep_self_bookmark :boolean          default(TRUE), not null | ||||
| #  min_favs           :integer | ||||
| #  min_reblogs        :integer | ||||
| #  created_at         :datetime         not null | ||||
| #  updated_at         :datetime         not null | ||||
| # | ||||
| class AccountStatusesCleanupPolicy < ApplicationRecord | ||||
|   include Redisable | ||||
| 
 | ||||
|   ALLOWED_MIN_STATUS_AGE = [ | ||||
|     2.weeks.seconds, | ||||
|     1.month.seconds, | ||||
|     2.months.seconds, | ||||
|     3.months.seconds, | ||||
|     6.months.seconds, | ||||
|     1.year.seconds, | ||||
|     2.years.seconds, | ||||
|   ].freeze | ||||
| 
 | ||||
|   EXCEPTION_BOOLS      = %w(keep_direct keep_pinned keep_polls keep_media keep_self_fav keep_self_bookmark).freeze | ||||
|   EXCEPTION_THRESHOLDS = %w(min_favs min_reblogs).freeze | ||||
| 
 | ||||
|   # Depending on the cleanup policy, the query to discover the next | ||||
|   # statuses to delete my get expensive if the account has a lot of old | ||||
|   # statuses otherwise excluded from deletion by the other exceptions. | ||||
|   # | ||||
|   # Therefore, `EARLY_SEARCH_CUTOFF` is meant to be the maximum number of | ||||
|   # old statuses to be considered for deletion prior to checking exceptions. | ||||
|   # | ||||
|   # This is used in `compute_cutoff_id` to provide a `max_id` to | ||||
|   # `statuses_to_delete`. | ||||
|   EARLY_SEARCH_CUTOFF = 5_000 | ||||
| 
 | ||||
|   belongs_to :account | ||||
| 
 | ||||
|   validates :min_status_age, inclusion: { in: ALLOWED_MIN_STATUS_AGE } | ||||
|   validates :min_favs, numericality: { greater_than_or_equal_to: 1, allow_nil: true } | ||||
|   validates :min_reblogs, numericality: { greater_than_or_equal_to: 1, allow_nil: true } | ||||
|   validate :validate_local_account | ||||
| 
 | ||||
|   before_save :update_last_inspected | ||||
| 
 | ||||
|   def statuses_to_delete(limit = 50, max_id = nil, min_id = nil) | ||||
|     scope = account.statuses | ||||
|     scope.merge!(old_enough_scope(max_id)) | ||||
|     scope = scope.where(Status.arel_table[:id].gteq(min_id)) if min_id.present? | ||||
|     scope.merge!(without_popular_scope) unless min_favs.nil? && min_reblogs.nil? | ||||
|     scope.merge!(without_direct_scope) if keep_direct? | ||||
|     scope.merge!(without_pinned_scope) if keep_pinned? | ||||
|     scope.merge!(without_poll_scope) if keep_polls? | ||||
|     scope.merge!(without_media_scope) if keep_media? | ||||
|     scope.merge!(without_self_fav_scope) if keep_self_fav? | ||||
|     scope.merge!(without_self_bookmark_scope) if keep_self_bookmark? | ||||
| 
 | ||||
|     scope.reorder(id: :asc).limit(limit) | ||||
|   end | ||||
| 
 | ||||
|   # This computes a toot id such that: | ||||
|   # - the toot would be old enough to be candidate for deletion | ||||
|   # - there are at most EARLY_SEARCH_CUTOFF toots between the last inspected toot and this one | ||||
|   # | ||||
|   # The idea is to limit expensive SQL queries when an account has lots of toots excluded from | ||||
|   # deletion, while not starting anew on each run. | ||||
|   def compute_cutoff_id | ||||
|     min_id = last_inspected || 0 | ||||
|     max_id = Mastodon::Snowflake.id_at(min_status_age.seconds.ago, with_random: false) | ||||
|     subquery = account.statuses.where(Status.arel_table[:id].gteq(min_id)).where(Status.arel_table[:id].lteq(max_id)) | ||||
|     subquery = subquery.select(:id).reorder(id: :asc).limit(EARLY_SEARCH_CUTOFF) | ||||
| 
 | ||||
|     # We're textually interpolating a subquery here as ActiveRecord seem to not provide | ||||
|     # a way to apply the limit to the subquery | ||||
|     Status.connection.execute("SELECT MAX(id) FROM (#{subquery.to_sql}) t").values.first.first | ||||
|   end | ||||
| 
 | ||||
|   # The most important thing about `last_inspected` is that any toot older than it is guaranteed | ||||
|   # not to be kept by the policy regardless of its age. | ||||
|   def record_last_inspected(last_id) | ||||
|     redis.set("account_cleanup:#{account.id}", last_id, ex: 1.week.seconds) | ||||
|   end | ||||
| 
 | ||||
|   def last_inspected | ||||
|     redis.get("account_cleanup:#{account.id}")&.to_i | ||||
|   end | ||||
| 
 | ||||
|   def invalidate_last_inspected(status, action) | ||||
|     last_value = last_inspected | ||||
|     return if last_value.nil? || status.id > last_value || status.account_id != account_id | ||||
| 
 | ||||
|     case action | ||||
|     when :unbookmark | ||||
|       return unless keep_self_bookmark? | ||||
|     when :unfav | ||||
|       return unless keep_self_fav? | ||||
|     when :unpin | ||||
|       return unless keep_pinned? | ||||
|     end | ||||
| 
 | ||||
|     record_last_inspected(status.id) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def update_last_inspected | ||||
|     if EXCEPTION_BOOLS.map { |name| attribute_change_to_be_saved(name) }.compact.include?([true, false]) | ||||
|       # Policy has been widened in such a way that any previously-inspected status | ||||
|       # may need to be deleted, so we'll have to start again. | ||||
|       redis.del("account_cleanup:#{account.id}") | ||||
|     end | ||||
|     if EXCEPTION_THRESHOLDS.map { |name| attribute_change_to_be_saved(name) }.compact.any? { |old, new| old.present? && (new.nil? || new > old) } | ||||
|       redis.del("account_cleanup:#{account.id}") | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def validate_local_account | ||||
|     errors.add(:account, :invalid) unless account&.local? | ||||
|   end | ||||
| 
 | ||||
|   def without_direct_scope | ||||
|     Status.where.not(visibility: :direct) | ||||
|   end | ||||
| 
 | ||||
|   def old_enough_scope(max_id = nil) | ||||
|     # Filtering on `id` rather than `min_status_age` ago will treat | ||||
|     # non-snowflake statuses as older than they really are, but Mastodon | ||||
|     # has switched to snowflake IDs significantly over 2 years ago anyway. | ||||
|     max_id = [max_id, Mastodon::Snowflake.id_at(min_status_age.seconds.ago, with_random: false)].compact.min | ||||
|     Status.where(Status.arel_table[:id].lteq(max_id)) | ||||
|   end | ||||
| 
 | ||||
|   def without_self_fav_scope | ||||
|     Status.where('NOT EXISTS (SELECT * FROM favourites fav WHERE fav.account_id = statuses.account_id AND fav.status_id = statuses.id)') | ||||
|   end | ||||
| 
 | ||||
|   def without_self_bookmark_scope | ||||
|     Status.where('NOT EXISTS (SELECT * FROM bookmarks bookmark WHERE bookmark.account_id = statuses.account_id AND bookmark.status_id = statuses.id)') | ||||
|   end | ||||
| 
 | ||||
|   def without_pinned_scope | ||||
|     Status.where('NOT EXISTS (SELECT * FROM status_pins pin WHERE pin.account_id = statuses.account_id AND pin.status_id = statuses.id)') | ||||
|   end | ||||
| 
 | ||||
|   def without_media_scope | ||||
|     Status.where('NOT EXISTS (SELECT * FROM media_attachments media WHERE media.status_id = statuses.id)') | ||||
|   end | ||||
| 
 | ||||
|   def without_poll_scope | ||||
|     Status.where(poll_id: nil) | ||||
|   end | ||||
| 
 | ||||
|   def without_popular_scope | ||||
|     scope = Status.left_joins(:status_stat) | ||||
|     scope = scope.where('COALESCE(status_stats.reblogs_count, 0) <= ?', min_reblogs) unless min_reblogs.nil? | ||||
|     scope = scope.where('COALESCE(status_stats.favourites_count, 0) <= ?', min_favs) unless min_favs.nil? | ||||
|     scope | ||||
|   end | ||||
| end | ||||
| @ -23,4 +23,12 @@ class Bookmark < ApplicationRecord | ||||
|   before_validation do | ||||
|     self.status = status.reblog if status&.reblog? | ||||
|   end | ||||
| 
 | ||||
|   after_destroy :invalidate_cleanup_info | ||||
| 
 | ||||
|   def invalidate_cleanup_info | ||||
|     return unless status&.account_id == account_id && account.local? | ||||
| 
 | ||||
|     account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unbookmark) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -66,5 +66,8 @@ module AccountAssociations | ||||
| 
 | ||||
|     # Follow recommendations | ||||
|     has_one :follow_recommendation_suppression, inverse_of: :account, dependent: :destroy | ||||
| 
 | ||||
|     # Account statuses cleanup policy | ||||
|     has_one :statuses_cleanup_policy, class_name: 'AccountStatusesCleanupPolicy', inverse_of: :account, dependent: :destroy | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -81,6 +81,9 @@ module AccountInteractions | ||||
|     has_many :following, -> { order('follows.id desc') }, through: :active_relationships,  source: :target_account | ||||
|     has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account | ||||
| 
 | ||||
|     # Account notes | ||||
|     has_many :account_notes, dependent: :destroy | ||||
| 
 | ||||
|     # Block relationships | ||||
|     has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy | ||||
|     has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account | ||||
|  | ||||
| @ -28,6 +28,7 @@ class Favourite < ApplicationRecord | ||||
| 
 | ||||
|   after_create :increment_cache_counters | ||||
|   after_destroy :decrement_cache_counters | ||||
|   after_destroy :invalidate_cleanup_info | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
| @ -39,4 +40,10 @@ class Favourite < ApplicationRecord | ||||
|     return if association(:status).loaded? && status.marked_for_destruction? | ||||
|     status&.decrement_count!(:favourites_count) | ||||
|   end | ||||
| 
 | ||||
|   def invalidate_cleanup_info | ||||
|     return unless status&.account_id == account_id && account.local? | ||||
| 
 | ||||
|     account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -15,4 +15,12 @@ class StatusPin < ApplicationRecord | ||||
|   belongs_to :status | ||||
| 
 | ||||
|   validates_with StatusPinValidator | ||||
| 
 | ||||
|   after_destroy :invalidate_cleanup_info | ||||
| 
 | ||||
|   def invalidate_cleanup_info | ||||
|     return unless status&.account_id == account_id && account.local? | ||||
| 
 | ||||
|     account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unpin) | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										27
									
								
								app/services/account_statuses_cleanup_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/services/account_statuses_cleanup_service.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AccountStatusesCleanupService < BaseService | ||||
|   # @param [AccountStatusesCleanupPolicy] account_policy | ||||
|   # @param [Integer] budget | ||||
|   # @return [Integer] | ||||
|   def call(account_policy, budget = 50) | ||||
|     return 0 unless account_policy.enabled? | ||||
| 
 | ||||
|     cutoff_id = account_policy.compute_cutoff_id | ||||
|     return 0 if cutoff_id.blank? | ||||
| 
 | ||||
|     num_deleted = 0 | ||||
|     last_deleted = nil | ||||
| 
 | ||||
|     account_policy.statuses_to_delete(budget, cutoff_id, account_policy.last_inspected).reorder(nil).find_each(order: :asc) do |status| | ||||
|       status.discard | ||||
|       RemovalWorker.perform_async(status.id, redraft: false) | ||||
|       num_deleted += 1 | ||||
|       last_deleted = status.id | ||||
|     end | ||||
| 
 | ||||
|     account_policy.record_last_inspected(last_deleted.presence || cutoff_id) | ||||
| 
 | ||||
|     num_deleted | ||||
|   end | ||||
| end | ||||
| @ -4,6 +4,7 @@ class DeleteAccountService < BaseService | ||||
|   include Payloadable | ||||
| 
 | ||||
|   ASSOCIATIONS_ON_SUSPEND = %w( | ||||
|     account_notes | ||||
|     account_pins | ||||
|     active_relationships | ||||
|     aliases | ||||
| @ -34,6 +35,7 @@ class DeleteAccountService < BaseService | ||||
|   # by foreign keys, making them safe to delete without loading | ||||
|   # into memory | ||||
|   ASSOCIATIONS_WITHOUT_SIDE_EFFECTS = %w( | ||||
|     account_notes | ||||
|     account_pins | ||||
|     aliases | ||||
|     conversation_mutes | ||||
|  | ||||
							
								
								
									
										45
									
								
								app/views/statuses_cleanup/show.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/views/statuses_cleanup/show.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| - content_for :page_title do | ||||
|   = t('settings.statuses_cleanup') | ||||
| 
 | ||||
| - content_for :heading_actions do | ||||
|   = button_tag t('generic.save_changes'), class: 'button', form: 'edit_policy' | ||||
| 
 | ||||
| = simple_form_for @policy, url: statuses_cleanup_path, method: :put, html: { id: 'edit_policy' } do |f| | ||||
| 
 | ||||
|   .fields-row | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :enabled, as: :boolean, wrapper: :with_label, label: t('statuses_cleanup.enabled'), hint: t('statuses_cleanup.enabled_hint') | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :min_status_age, wrapper: :with_label, label: t('statuses_cleanup.min_age_label'), collection: AccountStatusesCleanupPolicy::ALLOWED_MIN_STATUS_AGE.map(&:to_i), label_method: lambda { |i| t("statuses_cleanup.min_age.#{i}") }, include_blank: false, hint: false | ||||
| 
 | ||||
|   .flash-message= t('statuses_cleanup.explanation') | ||||
| 
 | ||||
|   %h4= t('statuses_cleanup.exceptions') | ||||
| 
 | ||||
|   .fields-row | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :keep_pinned, wrapper: :with_label, label: t('statuses_cleanup.keep_pinned'), hint: t('statuses_cleanup.keep_pinned_hint') | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :keep_direct, wrapper: :with_label, label: t('statuses_cleanup.keep_direct'), hint: t('statuses_cleanup.keep_direct_hint') | ||||
| 
 | ||||
|   .fields-row | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :keep_self_fav, wrapper: :with_label, label: t('statuses_cleanup.keep_self_fav'), hint: t('statuses_cleanup.keep_self_fav_hint') | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :keep_self_bookmark, wrapper: :with_label, label: t('statuses_cleanup.keep_self_bookmark'), hint: t('statuses_cleanup.keep_self_bookmark_hint') | ||||
| 
 | ||||
|   .fields-row | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :keep_polls, wrapper: :with_label, label: t('statuses_cleanup.keep_polls'), hint: t('statuses_cleanup.keep_polls_hint') | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :keep_media, wrapper: :with_label, label: t('statuses_cleanup.keep_media'), hint: t('statuses_cleanup.keep_media_hint') | ||||
| 
 | ||||
|   %h4= t('statuses_cleanup.interaction_exceptions') | ||||
| 
 | ||||
|   .fields-row | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :min_favs, wrapper: :with_label, label: t('statuses_cleanup.min_favs'), hint: t('statuses_cleanup.min_favs_hint'), input_html: { min: 1, placeholder: t('statuses_cleanup.ignore_favs') } | ||||
|     .fields-row__column.fields-row__column-6.fields-group | ||||
|       = f.input :min_reblogs, wrapper: :with_label, label: t('statuses_cleanup.min_reblogs'), hint: t('statuses_cleanup.min_reblogs_hint'), input_html: { min: 1, placeholder: t('statuses_cleanup.ignore_reblogs') } | ||||
| 
 | ||||
|   .flash-message= t('statuses_cleanup.interaction_exceptions_explanation') | ||||
| @ -47,7 +47,7 @@ class MoveWorker | ||||
| 
 | ||||
|   def copy_account_notes! | ||||
|     AccountNote.where(target_account: @source_account).find_each do |note| | ||||
|       text = I18n.with_locale(note.account.user.locale || I18n.default_locale) do | ||||
|       text = I18n.with_locale(note.account.user&.locale || I18n.default_locale) do | ||||
|         I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct) | ||||
|       end | ||||
| 
 | ||||
| @ -84,7 +84,7 @@ class MoveWorker | ||||
| 
 | ||||
|   def add_account_note_if_needed!(account, id) | ||||
|     unless AccountNote.where(account: account, target_account: @target_account).exists? | ||||
|       text = I18n.with_locale(account.user.locale || I18n.default_locale) do | ||||
|       text = I18n.with_locale(account.user&.locale || I18n.default_locale) do | ||||
|         I18n.t(id, acct: @source_account.acct) | ||||
|       end | ||||
|       AccountNote.create!(account: account, target_account: @target_account, comment: text) | ||||
|  | ||||
							
								
								
									
										96
									
								
								app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Scheduler::AccountsStatusesCleanupScheduler | ||||
|   include Sidekiq::Worker | ||||
| 
 | ||||
|   # This limit is mostly to be nice to the fediverse at large and not | ||||
|   # generate too much traffic. | ||||
|   # This also helps limiting the running time of the scheduler itself. | ||||
|   MAX_BUDGET         = 50 | ||||
| 
 | ||||
|   # This is an attempt to spread the load across instances, as various | ||||
|   # accounts are likely to have various followers. | ||||
|   PER_ACCOUNT_BUDGET = 5 | ||||
| 
 | ||||
|   # This is an attempt to limit the workload generated by status removal | ||||
|   # jobs to something the particular instance can handle. | ||||
|   PER_THREAD_BUDGET  = 5 | ||||
| 
 | ||||
|   # Those avoid loading an instance that is already under load | ||||
|   MAX_DEFAULT_SIZE    = 2 | ||||
|   MAX_DEFAULT_LATENCY = 5 | ||||
|   MAX_PUSH_SIZE       = 5 | ||||
|   MAX_PUSH_LATENCY    = 10 | ||||
|   # 'pull' queue has lower priority jobs, and it's unlikely that pushing | ||||
|   # deletes would cause much issues with this queue if it didn't cause issues | ||||
|   # with default and push. Yet, do not enqueue deletes if the instance is | ||||
|   # lagging behind too much. | ||||
|   MAX_PULL_SIZE       = 500 | ||||
|   MAX_PULL_LATENCY    = 300 | ||||
| 
 | ||||
|   # This is less of an issue in general, but deleting old statuses is likely | ||||
|   # to cause delivery errors, and thus increase the number of jobs to be retried. | ||||
|   # This doesn't directly translate to load, but connection errors and a high | ||||
|   # number of dead instances may lead to this spiraling out of control if | ||||
|   # unchecked. | ||||
|   MAX_RETRY_SIZE = 50_000 | ||||
| 
 | ||||
|   sidekiq_options retry: 0, lock: :until_executed | ||||
| 
 | ||||
|   def perform | ||||
|     return if under_load? | ||||
| 
 | ||||
|     budget = compute_budget | ||||
|     first_policy_id = last_processed_id | ||||
| 
 | ||||
|     loop do | ||||
|       num_processed_accounts = 0 | ||||
| 
 | ||||
|       scope = AccountStatusesCleanupPolicy.where(enabled: true) | ||||
|       scope.where(Account.arel_table[:id].gt(first_policy_id)) if first_policy_id.present? | ||||
|       scope.find_each(order: :asc) do |policy| | ||||
|         num_deleted = AccountStatusesCleanupService.new.call(policy, [budget, PER_ACCOUNT_BUDGET].min) | ||||
|         num_processed_accounts += 1 unless num_deleted.zero? | ||||
|         budget -= num_deleted | ||||
|         if budget.zero? | ||||
|           save_last_processed_id(policy.id) | ||||
|           break | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       # The idea here is to loop through all policies at least once until the budget is exhausted | ||||
|       # and start back after the last processed account otherwise | ||||
|       break if budget.zero? || (num_processed_accounts.zero? && first_policy_id.nil?) | ||||
|       first_policy_id = nil | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def compute_budget | ||||
|     threads = Sidekiq::ProcessSet.new.filter { |x| x['queues'].include?('push') }.map { |x| x['concurrency'] }.sum | ||||
|     [PER_THREAD_BUDGET * threads, MAX_BUDGET].min | ||||
|   end | ||||
| 
 | ||||
|   def under_load? | ||||
|     return true if Sidekiq::Stats.new.retry_size > MAX_RETRY_SIZE | ||||
|     queue_under_load?('default', MAX_DEFAULT_SIZE, MAX_DEFAULT_LATENCY) || queue_under_load?('push', MAX_PUSH_SIZE, MAX_PUSH_LATENCY) || queue_under_load?('pull', MAX_PULL_SIZE, MAX_PULL_LATENCY) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def queue_under_load?(name, max_size, max_latency) | ||||
|     queue = Sidekiq::Queue.new(name) | ||||
|     queue.size > max_size || queue.latency > max_latency | ||||
|   end | ||||
| 
 | ||||
|   def last_processed_id | ||||
|     Redis.current.get('account_statuses_cleanup_scheduler:last_account_id') | ||||
|   end | ||||
| 
 | ||||
|   def save_last_processed_id(id) | ||||
|     if id.nil? | ||||
|       Redis.current.del('account_statuses_cleanup_scheduler:last_account_id') | ||||
|     else | ||||
|       Redis.current.set('account_statuses_cleanup_scheduler:last_account_id', id, ex: 1.hour.seconds) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1254,6 +1254,7 @@ en: | ||||
|     preferences: Preferences | ||||
|     profile: Profile | ||||
|     relationships: Follows and followers | ||||
|     statuses_cleanup: Automated post deletion | ||||
|     two_factor_authentication: Two-factor Auth | ||||
|     webauthn_authentication: Security keys | ||||
|   statuses: | ||||
| @ -1305,6 +1306,40 @@ en: | ||||
|       public_long: Everyone can see | ||||
|       unlisted: Unlisted | ||||
|       unlisted_long: Everyone can see, but not listed on public timelines | ||||
|   statuses_cleanup: | ||||
|     enabled: Automatically delete old posts | ||||
|     enabled_hint: Automatically deletes your posts once they reach a specified age threshold, unless they match one of the exceptions below | ||||
|     exceptions: Exceptions | ||||
|     explanation: Because deleting posts is an expensive operation, this is done slowly over time when the server is not otherwise busy. For this reason, your posts may be deleted a while after they reach the age threshold. | ||||
|     ignore_favs: Ignore favourites | ||||
|     ignore_reblogs: Ignore boosts | ||||
|     interaction_exceptions: Exceptions based on interactions | ||||
|     interaction_exceptions_explanation: Note that there is no guarantee for posts to be deleted if they go below the favourite or boost threshold after having once gone over them. | ||||
|     keep_direct: Keep direct messages | ||||
|     keep_direct_hint: Doesn't delete any of your direct messages | ||||
|     keep_media: Keep posts with media attachments | ||||
|     keep_media_hint: Doesn't delete any of your posts that have media attachments | ||||
|     keep_pinned: Keep pinned posts | ||||
|     keep_pinned_hint: Doesn't delete any of your pinned posts | ||||
|     keep_polls: Keep polls | ||||
|     keep_polls_hint: Doesn't delete any of your polls | ||||
|     keep_self_bookmark: Keep posts you bookmarked | ||||
|     keep_self_bookmark_hint: Doesn't delete your own posts if you have bookmarked them | ||||
|     keep_self_fav: Keep posts you favourited | ||||
|     keep_self_fav_hint: Doesn't delete your own posts if you have favourited them | ||||
|     min_age: | ||||
|       '1209600': 2 weeks | ||||
|       '15778476': 6 months | ||||
|       '2629746': 1 month | ||||
|       '31556952': 1 year | ||||
|       '5259492': 2 months | ||||
|       '63113904': 2 years | ||||
|       '7889238': 3 months | ||||
|     min_age_label: Age threshold | ||||
|     min_favs: Keep posts favourited more than | ||||
|     min_favs_hint: Doesn't delete any of your posts that has received more than this amount of favourites. Leave blank to delete posts regardless of their number of favourites | ||||
|     min_reblogs: Keep posts boosted more than | ||||
|     min_reblogs_hint: Doesn't delete any of your posts that has been boosted more than this number of times. Leave blank to delete posts regardless of their number of boosts | ||||
|   stream_entries: | ||||
|     pinned: Pinned post | ||||
|     reblogged: boosted | ||||
|  | ||||
| @ -24,6 +24,7 @@ SimpleNavigation::Configuration.run do |navigation| | ||||
| 
 | ||||
|     n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url, if: -> { current_user.functional? } | ||||
|     n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } | ||||
|     n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? } | ||||
| 
 | ||||
|     n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| | ||||
|       s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities} | ||||
|  | ||||
| @ -178,6 +178,7 @@ Rails.application.routes.draw do | ||||
|   resources :invites, only: [:index, :create, :destroy] | ||||
|   resources :filters, except: [:show] | ||||
|   resource :relationships, only: [:show, :update] | ||||
|   resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update] | ||||
| 
 | ||||
|   get '/public', to: 'public_timelines#show', as: :public_timeline | ||||
|   get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy | ||||
|  | ||||
| @ -57,3 +57,7 @@ | ||||
|     cron: '0 * * * *' | ||||
|     class: Scheduler::InstanceRefreshScheduler | ||||
|     queue: scheduler | ||||
|   accounts_statuses_cleanup_scheduler: | ||||
|     interval: 1 minute | ||||
|     class: Scheduler::AccountsStatusesCleanupScheduler | ||||
|     queue: scheduler | ||||
|  | ||||
| @ -0,0 +1,20 @@ | ||||
| class CreateAccountStatusesCleanupPolicies < ActiveRecord::Migration[6.1] | ||||
|   def change | ||||
|     create_table :account_statuses_cleanup_policies do |t| | ||||
|       t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade } | ||||
|       t.boolean :enabled, null: false, default: true | ||||
|       t.integer :min_status_age, null: false, default: 2.weeks.seconds | ||||
|       t.boolean :keep_direct, null: false, default: true | ||||
|       t.boolean :keep_pinned, null: false, default: true | ||||
|       t.boolean :keep_polls, null: false, default: false | ||||
|       t.boolean :keep_media, null: false, default: false | ||||
|       t.boolean :keep_self_fav, null: false, default: true | ||||
|       t.boolean :keep_self_bookmark, null: false, default: true | ||||
|       t.integer :min_favs, null: true | ||||
|       t.integer :min_reblogs, null: true | ||||
| 
 | ||||
|       t.timestamps | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| @ -0,0 +1,21 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ClearOrphanedAccountNotes < ActiveRecord::Migration[5.2] | ||||
|   class Account < ApplicationRecord | ||||
|     # Dummy class, to make migration possible across version changes | ||||
|   end | ||||
| 
 | ||||
|   class AccountNote < ApplicationRecord | ||||
|     # Dummy class, to make migration possible across version changes | ||||
|     belongs_to :account | ||||
|     belongs_to :target_account, class_name: 'Account' | ||||
|   end | ||||
| 
 | ||||
|   def up | ||||
|     AccountNote.where('NOT EXISTS (SELECT * FROM users u WHERE u.account_id = account_notes.account_id)').in_batches.delete_all | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     # nothing to do | ||||
|   end | ||||
| end | ||||
							
								
								
									
										20
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								db/schema.rb
									
									
									
									
									
								
							| @ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
| 
 | ||||
| ActiveRecord::Schema.define(version: 2021_06_30_000137) do | ||||
| ActiveRecord::Schema.define(version: 2021_08_08_071221) do | ||||
| 
 | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @ -114,6 +114,23 @@ ActiveRecord::Schema.define(version: 2021_06_30_000137) do | ||||
|     t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true | ||||
|   end | ||||
| 
 | ||||
|   create_table "account_statuses_cleanup_policies", force: :cascade do |t| | ||||
|     t.bigint "account_id", null: false | ||||
|     t.boolean "enabled", default: true, null: false | ||||
|     t.integer "min_status_age", default: 1209600, null: false | ||||
|     t.boolean "keep_direct", default: true, null: false | ||||
|     t.boolean "keep_pinned", default: true, null: false | ||||
|     t.boolean "keep_polls", default: false, null: false | ||||
|     t.boolean "keep_media", default: false, null: false | ||||
|     t.boolean "keep_self_fav", default: true, null: false | ||||
|     t.boolean "keep_self_bookmark", default: true, null: false | ||||
|     t.integer "min_favs" | ||||
|     t.integer "min_reblogs" | ||||
|     t.datetime "created_at", precision: 6, null: false | ||||
|     t.datetime "updated_at", precision: 6, null: false | ||||
|     t.index ["account_id"], name: "index_account_statuses_cleanup_policies_on_account_id" | ||||
|   end | ||||
| 
 | ||||
|   create_table "account_warning_presets", force: :cascade do |t| | ||||
|     t.text "text", default: "", null: false | ||||
|     t.datetime "created_at", null: false | ||||
| @ -986,6 +1003,7 @@ ActiveRecord::Schema.define(version: 2021_06_30_000137) do | ||||
|   add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade | ||||
|   add_foreign_key "account_pins", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "account_stats", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "account_statuses_cleanup_policies", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade | ||||
|   add_foreign_key "account_warnings", "accounts", on_delete: :nullify | ||||
|   add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify | ||||
|  | ||||
| @ -138,10 +138,11 @@ module Mastodon::Snowflake | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def id_at(timestamp) | ||||
|       id  = timestamp.to_i * 1000 + rand(1000) | ||||
|     def id_at(timestamp, with_random: true) | ||||
|       id  = timestamp.to_i * 1000 | ||||
|       id += rand(1000) if with_random | ||||
|       id  = id << 16 | ||||
|       id += rand(2**16) | ||||
|       id += rand(2**16) if with_random | ||||
|       id | ||||
|     end | ||||
| 
 | ||||
|  | ||||
| @ -19,7 +19,7 @@ module Paperclip | ||||
|       metadata = VideoMetadataExtractor.new(@file.path) | ||||
| 
 | ||||
|       unless metadata.valid? | ||||
|         log("Unsupported file #{@file.path}") | ||||
|         Paperclip.log("Unsupported file #{@file.path}") | ||||
|         return File.open(@file.path) | ||||
|       end | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										18
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								package.json
									
									
									
									
									
								
							| @ -60,13 +60,13 @@ | ||||
|   }, | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@babel/core": "^7.14.6", | ||||
|     "@babel/core": "^7.14.8", | ||||
|     "@babel/plugin-proposal-decorators": "^7.14.5", | ||||
|     "@babel/plugin-transform-react-inline-elements": "^7.14.5", | ||||
|     "@babel/plugin-transform-runtime": "^7.14.5", | ||||
|     "@babel/preset-env": "^7.14.7", | ||||
|     "@babel/preset-env": "^7.15.0", | ||||
|     "@babel/preset-react": "^7.14.5", | ||||
|     "@babel/runtime": "^7.14.6", | ||||
|     "@babel/runtime": "^7.14.8", | ||||
|     "@gamestdio/websocket": "^0.3.2", | ||||
|     "@github/webauthn-json": "^0.5.7", | ||||
|     "@rails/ujs": "^6.1.4", | ||||
| @ -89,7 +89,7 @@ | ||||
|     "css-loader": "^5.2.7", | ||||
|     "cssnano": "^4.1.11", | ||||
|     "detect-passive-events": "^2.0.3", | ||||
|     "dotenv": "^9.0.2", | ||||
|     "dotenv": "^10.0.0", | ||||
|     "emoji-mart": "^3.0.1", | ||||
|     "es6-symbol": "^3.1.3", | ||||
|     "escape-html": "^1.0.3", | ||||
| @ -114,7 +114,7 @@ | ||||
|     "marky": "^1.2.2", | ||||
|     "mini-css-extract-plugin": "^1.6.2", | ||||
|     "mkdirp": "^1.0.4", | ||||
|     "npmlog": "^4.1.2", | ||||
|     "npmlog": "^5.0.0", | ||||
|     "object-assign": "^4.1.1", | ||||
|     "object-fit-images": "^3.2.3", | ||||
|     "object.values": "^1.1.3", | ||||
| @ -149,12 +149,12 @@ | ||||
|     "redux": "^4.1.0", | ||||
|     "redux-immutable": "^4.0.0", | ||||
|     "redux-thunk": "^2.2.0", | ||||
|     "regenerator-runtime": "^0.13.7", | ||||
|     "regenerator-runtime": "^0.13.9", | ||||
|     "rellax": "^1.12.1", | ||||
|     "requestidlecallback": "^0.3.0", | ||||
|     "reselect": "^4.0.0", | ||||
|     "rimraf": "^3.0.2", | ||||
|     "sass": "^1.35.2", | ||||
|     "sass": "^1.37.0", | ||||
|     "sass-loader": "^10.2.0", | ||||
|     "stacktrace-js": "^2.0.2", | ||||
|     "stringz": "^2.1.0", | ||||
| @ -171,14 +171,14 @@ | ||||
|     "webpack-cli": "^3.3.12", | ||||
|     "webpack-merge": "^5.8.0", | ||||
|     "wicg-inert": "^3.1.1", | ||||
|     "ws": "^7.5.3" | ||||
|     "ws": "^8.0.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@testing-library/jest-dom": "^5.14.1", | ||||
|     "@testing-library/react": "^11.2.7", | ||||
|     "babel-eslint": "^10.1.0", | ||||
|     "babel-jest": "^27.0.6", | ||||
|     "eslint": "^7.31.0", | ||||
|     "eslint": "^7.32.0", | ||||
|     "eslint-plugin-import": "~2.23.4", | ||||
|     "eslint-plugin-jsx-a11y": "~6.4.1", | ||||
|     "eslint-plugin-promise": "~5.1.0", | ||||
|  | ||||
							
								
								
									
										27
									
								
								spec/controllers/statuses_cleanup_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								spec/controllers/statuses_cleanup_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe StatusesCleanupController, type: :controller do | ||||
|   render_views | ||||
| 
 | ||||
|   before do | ||||
|     @user = Fabricate(:user) | ||||
|     sign_in @user, scope: :user | ||||
|   end | ||||
| 
 | ||||
|   describe "GET #show" do | ||||
|     it "returns http success" do | ||||
|       get :show | ||||
|       expect(response).to have_http_status(200) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'PUT #update' do | ||||
|     it 'updates the account status cleanup policy' do | ||||
|       put :update, params: { account_statuses_cleanup_policy: { enabled: true, min_status_age: 2.weeks.seconds, keep_direct: false, keep_polls: true } } | ||||
|       expect(response).to redirect_to(statuses_cleanup_path) | ||||
|       expect(@user.account.statuses_cleanup_policy.enabled).to eq true | ||||
|       expect(@user.account.statuses_cleanup_policy.keep_direct).to eq false | ||||
|       expect(@user.account.statuses_cleanup_policy.keep_polls).to eq true | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -0,0 +1,3 @@ | ||||
| Fabricator(:account_statuses_cleanup_policy) do | ||||
|   account | ||||
| end | ||||
							
								
								
									
										546
									
								
								spec/models/account_statuses_cleanup_policy_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										546
									
								
								spec/models/account_statuses_cleanup_policy_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,546 @@ | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe AccountStatusesCleanupPolicy, type: :model do | ||||
|   let(:account) { Fabricate(:account, username: 'alice', domain: nil) } | ||||
| 
 | ||||
|   describe 'validation' do | ||||
|     it 'disallow remote accounts' do | ||||
|       account.update(domain: 'example.com') | ||||
|       account_statuses_cleanup_policy = Fabricate.build(:account_statuses_cleanup_policy, account: account) | ||||
|       account_statuses_cleanup_policy.valid? | ||||
|       expect(account_statuses_cleanup_policy).to model_have_error_on_field(:account) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'save hooks' do | ||||
|     context 'when widening a policy' do | ||||
|       let!(:account_statuses_cleanup_policy) do | ||||
|         Fabricate(:account_statuses_cleanup_policy, | ||||
|           account: account, | ||||
|           keep_direct: true, | ||||
|           keep_pinned: true, | ||||
|           keep_polls: true, | ||||
|           keep_media: true, | ||||
|           keep_self_fav: true, | ||||
|           keep_self_bookmark: true, | ||||
|           min_favs: 1, | ||||
|           min_reblogs: 1 | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.record_last_inspected(42) | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of keep_direct' do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of keep_pinned' do | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of keep_polls' do | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of keep_media' do | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of keep_self_fav' do | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of keep_self_bookmark' do | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of higher min_favs' do | ||||
|         account_statuses_cleanup_policy.min_favs = 5 | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of disabled min_favs' do | ||||
|         account_statuses_cleanup_policy.min_favs = nil | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of higher min_reblogs' do | ||||
|         account_statuses_cleanup_policy.min_reblogs = 5 | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
| 
 | ||||
|       it 'invalidates last_inspected when widened because of disable min_reblogs' do | ||||
|         account_statuses_cleanup_policy.min_reblogs = nil | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to be nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when narrowing a policy' do | ||||
|       let!(:account_statuses_cleanup_policy) do | ||||
|         Fabricate(:account_statuses_cleanup_policy, | ||||
|           account: account, | ||||
|           keep_direct: false, | ||||
|           keep_pinned: false, | ||||
|           keep_polls: false, | ||||
|           keep_media: false, | ||||
|           keep_self_fav: false, | ||||
|           keep_self_bookmark: false, | ||||
|           min_favs: nil, | ||||
|           min_reblogs: nil | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not unnecessarily invalidate last_inspected' do | ||||
|         account_statuses_cleanup_policy.record_last_inspected(42) | ||||
|         account_statuses_cleanup_policy.keep_direct = true | ||||
|         account_statuses_cleanup_policy.keep_pinned = true | ||||
|         account_statuses_cleanup_policy.keep_polls = true | ||||
|         account_statuses_cleanup_policy.keep_media = true | ||||
|         account_statuses_cleanup_policy.keep_self_fav = true | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = true | ||||
|         account_statuses_cleanup_policy.min_favs = 5 | ||||
|         account_statuses_cleanup_policy.min_reblogs = 5 | ||||
|         account_statuses_cleanup_policy.save | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to eq 42 | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#record_last_inspected' do | ||||
|     let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } | ||||
| 
 | ||||
|     it 'records the given id' do | ||||
|       account_statuses_cleanup_policy.record_last_inspected(42) | ||||
|       expect(account_statuses_cleanup_policy.last_inspected).to eq 42 | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#invalidate_last_inspected' do | ||||
|     let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } | ||||
|     let(:status) { Fabricate(:status, id: 10, account: account) } | ||||
|     subject { account_statuses_cleanup_policy.invalidate_last_inspected(status, action) } | ||||
| 
 | ||||
|     before do | ||||
|       account_statuses_cleanup_policy.record_last_inspected(42) | ||||
|     end | ||||
| 
 | ||||
|     context 'when the action is :unbookmark' do | ||||
|       let(:action) { :unbookmark } | ||||
| 
 | ||||
|       context 'when the policy is not to keep self-bookmarked toots' do | ||||
|         before do | ||||
|           account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|         end | ||||
| 
 | ||||
|         it 'does not change the recorded id' do | ||||
|           subject | ||||
|           expect(account_statuses_cleanup_policy.last_inspected).to eq 42 | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when the policy is to keep self-bookmarked toots' do | ||||
|         before do | ||||
|           account_statuses_cleanup_policy.keep_self_bookmark = true | ||||
|         end | ||||
| 
 | ||||
|         it 'records the older id' do | ||||
|           subject | ||||
|           expect(account_statuses_cleanup_policy.last_inspected).to eq 10 | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the action is :unfav' do | ||||
|       let(:action) { :unfav } | ||||
| 
 | ||||
|       context 'when the policy is not to keep self-favourited toots' do | ||||
|         before do | ||||
|           account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         end | ||||
| 
 | ||||
|         it 'does not change the recorded id' do | ||||
|           subject | ||||
|           expect(account_statuses_cleanup_policy.last_inspected).to eq 42 | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when the policy is to keep self-favourited toots' do | ||||
|         before do | ||||
|           account_statuses_cleanup_policy.keep_self_fav = true | ||||
|         end | ||||
| 
 | ||||
|         it 'records the older id' do | ||||
|           subject | ||||
|           expect(account_statuses_cleanup_policy.last_inspected).to eq 10 | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the action is :unpin' do | ||||
|       let(:action) { :unpin } | ||||
| 
 | ||||
|       context 'when the policy is not to keep pinned toots' do | ||||
|         before do | ||||
|           account_statuses_cleanup_policy.keep_pinned = false | ||||
|         end | ||||
| 
 | ||||
|         it 'does not change the recorded id' do | ||||
|           subject | ||||
|           expect(account_statuses_cleanup_policy.last_inspected).to eq 42 | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when the policy is to keep pinned toots' do | ||||
|         before do | ||||
|           account_statuses_cleanup_policy.keep_pinned = true | ||||
|         end | ||||
| 
 | ||||
|         it 'records the older id' do | ||||
|           subject | ||||
|           expect(account_statuses_cleanup_policy.last_inspected).to eq 10 | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the status is more recent than the recorded inspected id' do | ||||
|       let(:action) { :unfav } | ||||
|       let(:status) { Fabricate(:status, account: account) } | ||||
| 
 | ||||
|       it 'does not change the recorded id' do | ||||
|         subject | ||||
|         expect(account_statuses_cleanup_policy.last_inspected).to eq 42 | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#compute_cutoff_id' do | ||||
|     let!(:unrelated_status)  { Fabricate(:status, created_at: 3.years.ago) } | ||||
|     let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } | ||||
| 
 | ||||
|     subject { account_statuses_cleanup_policy.compute_cutoff_id } | ||||
| 
 | ||||
|     context 'when the account has posted multiple toots' do | ||||
|       let!(:very_old_status)   { Fabricate(:status, created_at: 3.years.ago, account: account) } | ||||
|       let!(:old_status)        { Fabricate(:status, created_at: 3.weeks.ago, account: account) } | ||||
|       let!(:recent_status)     { Fabricate(:status, created_at: 2.days.ago, account: account) } | ||||
| 
 | ||||
|       it 'returns the most recent id that is still below policy age' do | ||||
|         expect(subject).to eq old_status.id | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the account has not posted anything' do | ||||
|       it 'returns nil' do | ||||
|         expect(subject).to be_nil | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#statuses_to_delete' do | ||||
|     let!(:unrelated_status)  { Fabricate(:status, created_at: 3.years.ago) } | ||||
|     let!(:very_old_status)   { Fabricate(:status, created_at: 3.years.ago, account: account) } | ||||
|     let!(:pinned_status)     { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:direct_message)    { Fabricate(:status, created_at: 1.year.ago, account: account, visibility: :direct) } | ||||
|     let!(:self_faved)        { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:self_bookmarked)   { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:status_with_poll)  { Fabricate(:status, created_at: 1.year.ago, account: account, poll_attributes: { account: account, voters_count: 0, options: ['a', 'b'], expires_in: 2.days }) } | ||||
|     let!(:status_with_media) { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:faved4)            { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:faved5)            { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:reblogged4)        { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:reblogged5)        { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|     let!(:recent_status)     { Fabricate(:status, created_at: 2.days.ago, account: account) } | ||||
| 
 | ||||
|     let!(:media_attachment)  { Fabricate(:media_attachment, account: account, status: status_with_media) } | ||||
|     let!(:status_pin)        { Fabricate(:status_pin, account: account, status: pinned_status) } | ||||
|     let!(:favourite)         { Fabricate(:favourite, account: account, status: self_faved) } | ||||
|     let!(:bookmark)          { Fabricate(:bookmark, account: account, status: self_bookmarked) } | ||||
| 
 | ||||
|     let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } | ||||
| 
 | ||||
|     subject { account_statuses_cleanup_policy.statuses_to_delete } | ||||
| 
 | ||||
|     before do | ||||
|       4.times { faved4.increment_count!(:favourites_count) } | ||||
|       5.times { faved5.increment_count!(:favourites_count) } | ||||
|       4.times { reblogged4.increment_count!(:reblogs_count) } | ||||
|       5.times { reblogged5.increment_count!(:reblogs_count) } | ||||
|     end | ||||
| 
 | ||||
|     context 'when passed a max_id' do | ||||
|       let!(:old_status)               { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|       let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } | ||||
| 
 | ||||
|       subject { account_statuses_cleanup_policy.statuses_to_delete(50, old_status.id).pluck(:id) } | ||||
| 
 | ||||
|       it 'returns statuses including max_id' do | ||||
|         expect(subject).to include(old_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns statuses including older than max_id' do | ||||
|         expect(subject).to include(very_old_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return statuses newer than max_id' do | ||||
|         expect(subject).to_not include(slightly_less_old_status.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when passed a min_id' do | ||||
|       let!(:old_status)               { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|       let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } | ||||
| 
 | ||||
|       subject { account_statuses_cleanup_policy.statuses_to_delete(50, recent_status.id, old_status.id).pluck(:id) } | ||||
| 
 | ||||
|       it 'returns statuses including min_id' do | ||||
|         expect(subject).to include(old_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns statuses including newer than max_id' do | ||||
|         expect(subject).to include(slightly_less_old_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return statuses older than min_id' do | ||||
|         expect(subject).to_not include(very_old_status.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when passed a low limit' do | ||||
|       it 'only returns the limited number of items' do | ||||
|         expect(account_statuses_cleanup_policy.statuses_to_delete(1).count).to eq 1 | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep statuses more recent than 2 years' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.min_status_age = 2.years.seconds | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return unrelated old status' do | ||||
|         expect(subject.pluck(:id)).to_not include(unrelated_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns only oldest status for deletion' do | ||||
|         expect(subject.pluck(:id)).to eq [very_old_status.id] | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep DMs and reject everything else' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = true | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the old direct message for deletion' do | ||||
|         expect(subject.pluck(:id)).to_not include(direct_message.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep self-bookmarked toots and reject everything else' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = true | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the old self-bookmarked message for deletion' do | ||||
|         expect(subject.pluck(:id)).to_not include(self_bookmarked.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep self-faved toots and reject everything else' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.keep_self_fav = true | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the old self-bookmarked message for deletion' do | ||||
|         expect(subject.pluck(:id)).to_not include(self_faved.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep toots with media and reject everything else' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.keep_media = true | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the old message with media for deletion' do | ||||
|         expect(subject.pluck(:id)).to_not include(status_with_media.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep toots with polls and reject everything else' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.keep_polls = true | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the old poll message for deletion' do | ||||
|         expect(subject.pluck(:id)).to_not include(status_with_poll.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep pinned toots and reject everything else' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.keep_pinned = true | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the old pinned message for deletion' do | ||||
|         expect(subject.pluck(:id)).to_not include(pinned_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is to not keep any special messages' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = false | ||||
|         account_statuses_cleanup_policy.keep_pinned = false | ||||
|         account_statuses_cleanup_policy.keep_polls = false | ||||
|         account_statuses_cleanup_policy.keep_media = false | ||||
|         account_statuses_cleanup_policy.keep_self_fav = false | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the recent toot' do | ||||
|         expect(subject.pluck(:id)).to_not include(recent_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the unrelated toot' do | ||||
|         expect(subject.pluck(:id)).to_not include(unrelated_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns every other old status for deletion' do | ||||
|         expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is set to keep every category of toots' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.keep_direct = true | ||||
|         account_statuses_cleanup_policy.keep_pinned = true | ||||
|         account_statuses_cleanup_policy.keep_polls = true | ||||
|         account_statuses_cleanup_policy.keep_media = true | ||||
|         account_statuses_cleanup_policy.keep_self_fav = true | ||||
|         account_statuses_cleanup_policy.keep_self_bookmark = true | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return unrelated old status' do | ||||
|         expect(subject.pluck(:id)).to_not include(unrelated_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns only normal statuses for deletion' do | ||||
|         expect(subject.pluck(:id).sort).to eq [very_old_status.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id].sort | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is to keep statuses with more than 4 boosts' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.min_reblogs = 4 | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the recent toot' do | ||||
|         expect(subject.pluck(:id)).to_not include(recent_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the toot reblogged 5 times' do | ||||
|         expect(subject.pluck(:id)).to_not include(reblogged5.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the unrelated toot' do | ||||
|         expect(subject.pluck(:id)).to_not include(unrelated_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns old statuses not reblogged as much' do | ||||
|         expect(subject.pluck(:id)).to include(very_old_status.id, faved4.id, faved5.id, reblogged4.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when policy is to keep statuses with more than 4 favs' do | ||||
|       before do | ||||
|         account_statuses_cleanup_policy.min_favs = 4 | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the recent toot' do | ||||
|         expect(subject.pluck(:id)).to_not include(recent_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the toot faved 5 times' do | ||||
|         expect(subject.pluck(:id)).to_not include(faved5.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return the unrelated toot' do | ||||
|         expect(subject.pluck(:id)).to_not include(unrelated_status.id) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns old statuses not faved as much' do | ||||
|         expect(subject.pluck(:id)).to include(very_old_status.id, faved4.id, reblogged4.id, reblogged5.id) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										101
									
								
								spec/services/account_statuses_cleanup_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								spec/services/account_statuses_cleanup_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| describe AccountStatusesCleanupService, type: :service do | ||||
|   let(:account)           { Fabricate(:account, username: 'alice', domain: nil) } | ||||
|   let(:account_policy)    { Fabricate(:account_statuses_cleanup_policy, account: account) } | ||||
|   let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) } | ||||
| 
 | ||||
|   describe '#call' do | ||||
|     context 'when the account has not posted anything' do | ||||
|       it 'returns 0 deleted toots' do | ||||
|         expect(subject.call(account_policy)).to eq 0 | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the account has posted several old statuses' do | ||||
|       let!(:very_old_status)    { Fabricate(:status, created_at: 3.years.ago, account: account) } | ||||
|       let!(:old_status)         { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|       let!(:another_old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } | ||||
|       let!(:recent_status)      { Fabricate(:status, created_at: 1.day.ago, account: account) } | ||||
| 
 | ||||
|       context 'given a budget of 1' do | ||||
|         it 'reports 1 deleted toot' do | ||||
|           expect(subject.call(account_policy, 1)).to eq 1 | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'given a normal budget of 10' do | ||||
|         it 'reports 3 deleted statuses' do | ||||
|           expect(subject.call(account_policy, 10)).to eq 3 | ||||
|         end | ||||
| 
 | ||||
|         it 'records the last deleted id' do | ||||
|           subject.call(account_policy, 10) | ||||
|           expect(account_policy.last_inspected).to eq [old_status.id, another_old_status.id].max | ||||
|         end | ||||
| 
 | ||||
|         it 'actually deletes the statuses' do | ||||
|           subject.call(account_policy, 10) | ||||
|           expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when called repeatedly with a budget of 2' do | ||||
|         it 'reports 2 then 1 deleted statuses' do | ||||
|          expect(subject.call(account_policy, 2)).to eq 2 | ||||
|          expect(subject.call(account_policy, 2)).to eq 1 | ||||
|         end | ||||
| 
 | ||||
|         it 'actually deletes the statuses in the expected order' do | ||||
|           subject.call(account_policy, 2) | ||||
|           expect(Status.find_by(id: very_old_status.id)).to be_nil | ||||
|           subject.call(account_policy, 2) | ||||
|           expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when a self-faved toot is unfaved' do | ||||
|         let!(:self_faved) { Fabricate(:status, created_at: 6.months.ago, account: account) } | ||||
|         let!(:favourite)  { Fabricate(:favourite, account: account, status: self_faved) } | ||||
| 
 | ||||
|         it 'deletes it once unfaved' do | ||||
|           expect(subject.call(account_policy, 20)).to eq 3 | ||||
|           expect(Status.find_by(id: self_faved.id)).to_not be_nil | ||||
|           expect(subject.call(account_policy, 20)).to eq 0 | ||||
|           favourite.destroy! | ||||
|           expect(subject.call(account_policy, 20)).to eq 1 | ||||
|           expect(Status.find_by(id: self_faved.id)).to be_nil | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when there are more un-deletable old toots than the early search cutoff' do | ||||
|         before do | ||||
|           stub_const 'AccountStatusesCleanupPolicy::EARLY_SEARCH_CUTOFF', 5 | ||||
|           # Old statuses that should be cut-off | ||||
|           10.times do | ||||
|             Fabricate(:status, created_at: 4.years.ago, visibility: :direct, account: account) | ||||
|           end | ||||
|           # New statuses that prevent cut-off id to reach the last status | ||||
|           10.times do | ||||
|             Fabricate(:status, created_at: 4.seconds.ago, visibility: :direct, account: account) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         it 'reports 0 deleted statuses then 0 then 3 then 0 again' do | ||||
|           expect(subject.call(account_policy, 10)).to eq 0 | ||||
|           expect(subject.call(account_policy, 10)).to eq 0 | ||||
|           expect(subject.call(account_policy, 10)).to eq 3 | ||||
|           expect(subject.call(account_policy, 10)).to eq 0 | ||||
|         end | ||||
| 
 | ||||
|         it 'never causes the recorded id to get higher than oldest deletable toot' do | ||||
|           subject.call(account_policy, 10) | ||||
|           subject.call(account_policy, 10) | ||||
|           subject.call(account_policy, 10) | ||||
|           subject.call(account_policy, 10) | ||||
|           expect(account_policy.last_inspected).to be < Mastodon::Snowflake.id_at(account_policy.min_status_age.seconds.ago, with_random: false) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -21,6 +21,8 @@ RSpec.describe DeleteAccountService, type: :service do | ||||
|     let!(:favourite_notification) { Fabricate(:notification, account: local_follower, activity: favourite, type: :favourite) } | ||||
|     let!(:follow_notification) { Fabricate(:notification, account: local_follower, activity: active_relationship, type: :follow) } | ||||
| 
 | ||||
|     let!(:account_note) { Fabricate(:account_note, account: account) } | ||||
| 
 | ||||
|     subject do | ||||
|       -> { described_class.new.call(account) } | ||||
|     end | ||||
| @ -35,8 +37,9 @@ RSpec.describe DeleteAccountService, type: :service do | ||||
|           account.active_relationships, | ||||
|           account.passive_relationships, | ||||
|           account.polls, | ||||
|           account.account_notes, | ||||
|         ].map(&:count) | ||||
|       }.from([2, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) | ||||
|       }.from([2, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) | ||||
|     end | ||||
| 
 | ||||
|     it 'deletes associated target records' do | ||||
|  | ||||
| @ -0,0 +1,127 @@ | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| describe Scheduler::AccountsStatusesCleanupScheduler do | ||||
|   subject { described_class.new } | ||||
| 
 | ||||
|   let!(:account1)  { Fabricate(:account, domain: nil) } | ||||
|   let!(:account2)  { Fabricate(:account, domain: nil) } | ||||
|   let!(:account3)  { Fabricate(:account, domain: nil) } | ||||
|   let!(:account4)  { Fabricate(:account, domain: nil) } | ||||
|   let!(:remote)    { Fabricate(:account) } | ||||
| 
 | ||||
|   let!(:policy1)   { Fabricate(:account_statuses_cleanup_policy, account: account1) } | ||||
|   let!(:policy2)   { Fabricate(:account_statuses_cleanup_policy, account: account3) } | ||||
|   let!(:policy3)   { Fabricate(:account_statuses_cleanup_policy, account: account4, enabled: false) } | ||||
| 
 | ||||
|   let(:queue_size)       { 0 } | ||||
|   let(:queue_latency)    { 0 } | ||||
|   let(:process_set_stub) do | ||||
|     [ | ||||
|       { | ||||
|         'concurrency' => 2, | ||||
|         'queues' => ['push', 'default'], | ||||
|       }, | ||||
|     ] | ||||
|   end | ||||
|   let(:retry_size) { 0 } | ||||
| 
 | ||||
|   before do | ||||
|     queue_stub = double | ||||
|     allow(queue_stub).to receive(:size).and_return(queue_size) | ||||
|     allow(queue_stub).to receive(:latency).and_return(queue_latency) | ||||
|     allow(Sidekiq::Queue).to receive(:new).and_return(queue_stub) | ||||
|     allow(Sidekiq::ProcessSet).to receive(:new).and_return(process_set_stub) | ||||
| 
 | ||||
|     sidekiq_stats_stub = double | ||||
|     allow(sidekiq_stats_stub).to receive(:retry_size).and_return(retry_size) | ||||
|     allow(Sidekiq::Stats).to receive(:new).and_return(sidekiq_stats_stub) | ||||
| 
 | ||||
|     # Create a bunch of old statuses | ||||
|     10.times do | ||||
|       Fabricate(:status, account: account1, created_at: 3.years.ago) | ||||
|       Fabricate(:status, account: account2, created_at: 3.years.ago) | ||||
|       Fabricate(:status, account: account3, created_at: 3.years.ago) | ||||
|       Fabricate(:status, account: account4, created_at: 3.years.ago) | ||||
|       Fabricate(:status, account: remote, created_at: 3.years.ago) | ||||
|     end | ||||
| 
 | ||||
|     # Create a bunch of newer statuses | ||||
|     5.times do | ||||
|       Fabricate(:status, account: account1, created_at: 3.minutes.ago) | ||||
|       Fabricate(:status, account: account2, created_at: 3.minutes.ago) | ||||
|       Fabricate(:status, account: account3, created_at: 3.minutes.ago) | ||||
|       Fabricate(:status, account: account4, created_at: 3.minutes.ago) | ||||
|       Fabricate(:status, account: remote, created_at: 3.minutes.ago) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#under_load?' do | ||||
|     context 'when nothing is queued' do | ||||
|       it 'returns false' do | ||||
|         expect(subject.under_load?).to be false | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when numerous jobs are queued' do | ||||
|       let(:queue_size)    { 5 } | ||||
|       let(:queue_latency) { 120 } | ||||
| 
 | ||||
|       it 'returns true' do | ||||
|         expect(subject.under_load?).to be true | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when there is a huge amount of jobs to retry' do | ||||
|       let(:retry_size) { 1_000_000 } | ||||
| 
 | ||||
|       it 'returns true' do | ||||
|         expect(subject.under_load?).to be true | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#get_budget' do | ||||
|     context 'on a single thread' do | ||||
|       let(:process_set_stub) { [ { 'concurrency' => 1, 'queues' => ['push', 'default'] } ] } | ||||
| 
 | ||||
|       it 'returns a low value' do | ||||
|         expect(subject.compute_budget).to be < 10 | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'on a lot of threads' do | ||||
|       let(:process_set_stub) do | ||||
|         [ | ||||
|           { 'concurrency' => 2, 'queues' => ['push', 'default'] }, | ||||
|           { 'concurrency' => 2, 'queues' => ['push'] }, | ||||
|           { 'concurrency' => 2, 'queues' => ['push'] }, | ||||
|           { 'concurrency' => 2, 'queues' => ['push'] }, | ||||
|         ] | ||||
|       end | ||||
| 
 | ||||
|       it 'returns a larger value' do | ||||
|         expect(subject.compute_budget).to be > 10 | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#perform' do | ||||
|     context 'when the budget is lower than the number of toots to delete' do | ||||
|       it 'deletes as many statuses as the given budget' do | ||||
|         expect { subject.perform }.to change { Status.count }.by(-subject.compute_budget) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not delete from accounts with no cleanup policy' do | ||||
|         expect { subject.perform }.to_not change { account2.statuses.count } | ||||
|       end | ||||
| 
 | ||||
|       it 'does not delete from accounts with disabled cleanup policies' do | ||||
|         expect { subject.perform }.to_not change { account4.statuses.count } | ||||
|       end | ||||
| 
 | ||||
|       it 'eventually deletes every deletable toot' do | ||||
|         expect { subject.perform; subject.perform; subject.perform; subject.perform }.to change { Status.count }.by(-20) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										372
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										372
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -16,25 +16,25 @@ | ||||
|   dependencies: | ||||
|     "@babel/highlight" "^7.14.5" | ||||
| 
 | ||||
| "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.5", "@babel/compat-data@^7.14.7": | ||||
|   version "7.14.7" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.7.tgz#7b047d7a3a89a67d2258dc61f604f098f1bc7e08" | ||||
|   integrity sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw== | ||||
| "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.7", "@babel/compat-data@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" | ||||
|   integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA== | ||||
| 
 | ||||
| "@babel/core@^7.1.0", "@babel/core@^7.14.6", "@babel/core@^7.7.2", "@babel/core@^7.7.5": | ||||
|   version "7.14.6" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.6.tgz#e0814ec1a950032ff16c13a2721de39a8416fcab" | ||||
|   integrity sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA== | ||||
| "@babel/core@^7.1.0", "@babel/core@^7.14.8", "@babel/core@^7.7.2", "@babel/core@^7.7.5": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010" | ||||
|   integrity sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q== | ||||
|   dependencies: | ||||
|     "@babel/code-frame" "^7.14.5" | ||||
|     "@babel/generator" "^7.14.5" | ||||
|     "@babel/generator" "^7.14.8" | ||||
|     "@babel/helper-compilation-targets" "^7.14.5" | ||||
|     "@babel/helper-module-transforms" "^7.14.5" | ||||
|     "@babel/helpers" "^7.14.6" | ||||
|     "@babel/parser" "^7.14.6" | ||||
|     "@babel/helper-module-transforms" "^7.14.8" | ||||
|     "@babel/helpers" "^7.14.8" | ||||
|     "@babel/parser" "^7.14.8" | ||||
|     "@babel/template" "^7.14.5" | ||||
|     "@babel/traverse" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/traverse" "^7.14.8" | ||||
|     "@babel/types" "^7.14.8" | ||||
|     convert-source-map "^1.7.0" | ||||
|     debug "^4.1.0" | ||||
|     gensync "^1.0.0-beta.2" | ||||
| @ -42,12 +42,21 @@ | ||||
|     semver "^6.3.0" | ||||
|     source-map "^0.5.0" | ||||
| 
 | ||||
| "@babel/generator@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.5.tgz#848d7b9f031caca9d0cd0af01b063f226f52d785" | ||||
|   integrity sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA== | ||||
| "@babel/generator@^7.14.8": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.8.tgz#bf86fd6af96cf3b74395a8ca409515f89423e070" | ||||
|   integrity sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg== | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/types" "^7.14.8" | ||||
|     jsesc "^2.5.1" | ||||
|     source-map "^0.5.0" | ||||
| 
 | ||||
| "@babel/generator@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.0.tgz#a7d0c172e0d814974bad5aa77ace543b97917f15" | ||||
|   integrity sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ== | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.15.0" | ||||
|     jsesc "^2.5.1" | ||||
|     source-map "^0.5.0" | ||||
| 
 | ||||
| @ -74,12 +83,12 @@ | ||||
|     "@babel/helper-annotate-as-pure" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
| 
 | ||||
| "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz#7a99c5d0967911e972fe2c3411f7d5b498498ecf" | ||||
|   integrity sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw== | ||||
| "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz#973df8cbd025515f3ff25db0c05efc704fa79818" | ||||
|   integrity sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A== | ||||
|   dependencies: | ||||
|     "@babel/compat-data" "^7.14.5" | ||||
|     "@babel/compat-data" "^7.15.0" | ||||
|     "@babel/helper-validator-option" "^7.14.5" | ||||
|     browserslist "^4.16.6" | ||||
|     semver "^6.3.0" | ||||
| @ -155,6 +164,13 @@ | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.14.5" | ||||
| 
 | ||||
| "@babel/helper-member-expression-to-functions@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz#0ddaf5299c8179f27f37327936553e9bba60990b" | ||||
|   integrity sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg== | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.15.0" | ||||
| 
 | ||||
| "@babel/helper-module-imports@^7.0.0-beta.49", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3" | ||||
| @ -162,19 +178,33 @@ | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.14.5" | ||||
| 
 | ||||
| "@babel/helper-module-transforms@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz#7de42f10d789b423eb902ebd24031ca77cb1e10e" | ||||
|   integrity sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA== | ||||
| "@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.14.8": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49" | ||||
|   integrity sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA== | ||||
|   dependencies: | ||||
|     "@babel/helper-module-imports" "^7.14.5" | ||||
|     "@babel/helper-replace-supers" "^7.14.5" | ||||
|     "@babel/helper-simple-access" "^7.14.5" | ||||
|     "@babel/helper-simple-access" "^7.14.8" | ||||
|     "@babel/helper-split-export-declaration" "^7.14.5" | ||||
|     "@babel/helper-validator-identifier" "^7.14.5" | ||||
|     "@babel/helper-validator-identifier" "^7.14.8" | ||||
|     "@babel/template" "^7.14.5" | ||||
|     "@babel/traverse" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/traverse" "^7.14.8" | ||||
|     "@babel/types" "^7.14.8" | ||||
| 
 | ||||
| "@babel/helper-module-transforms@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz#679275581ea056373eddbe360e1419ef23783b08" | ||||
|   integrity sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg== | ||||
|   dependencies: | ||||
|     "@babel/helper-module-imports" "^7.14.5" | ||||
|     "@babel/helper-replace-supers" "^7.15.0" | ||||
|     "@babel/helper-simple-access" "^7.14.8" | ||||
|     "@babel/helper-split-export-declaration" "^7.14.5" | ||||
|     "@babel/helper-validator-identifier" "^7.14.9" | ||||
|     "@babel/template" "^7.14.5" | ||||
|     "@babel/traverse" "^7.15.0" | ||||
|     "@babel/types" "^7.15.0" | ||||
| 
 | ||||
| "@babel/helper-optimise-call-expression@^7.14.5": | ||||
|   version "7.14.5" | ||||
| @ -207,12 +237,22 @@ | ||||
|     "@babel/traverse" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
| 
 | ||||
| "@babel/helper-simple-access@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz#66ea85cf53ba0b4e588ba77fc813f53abcaa41c4" | ||||
|   integrity sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw== | ||||
| "@babel/helper-replace-supers@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz#ace07708f5bf746bf2e6ba99572cce79b5d4e7f4" | ||||
|   integrity sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA== | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/helper-member-expression-to-functions" "^7.15.0" | ||||
|     "@babel/helper-optimise-call-expression" "^7.14.5" | ||||
|     "@babel/traverse" "^7.15.0" | ||||
|     "@babel/types" "^7.15.0" | ||||
| 
 | ||||
| "@babel/helper-simple-access@^7.14.8": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924" | ||||
|   integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg== | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.14.8" | ||||
| 
 | ||||
| "@babel/helper-skip-transparent-expression-wrappers@^7.14.5": | ||||
|   version "7.14.5" | ||||
| @ -238,6 +278,16 @@ | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" | ||||
|   integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== | ||||
| 
 | ||||
| "@babel/helper-validator-identifier@^7.14.8": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c" | ||||
|   integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow== | ||||
| 
 | ||||
| "@babel/helper-validator-identifier@^7.14.9": | ||||
|   version "7.14.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" | ||||
|   integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== | ||||
| 
 | ||||
| "@babel/helper-validator-option@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" | ||||
| @ -253,14 +303,14 @@ | ||||
|     "@babel/traverse" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
| 
 | ||||
| "@babel/helpers@^7.14.6": | ||||
|   version "7.14.6" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.6.tgz#5b58306b95f1b47e2a0199434fa8658fa6c21635" | ||||
|   integrity sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA== | ||||
| "@babel/helpers@^7.14.8": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77" | ||||
|   integrity sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw== | ||||
|   dependencies: | ||||
|     "@babel/template" "^7.14.5" | ||||
|     "@babel/traverse" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/traverse" "^7.14.8" | ||||
|     "@babel/types" "^7.14.8" | ||||
| 
 | ||||
| "@babel/highlight@^7.10.4": | ||||
|   version "7.12.13" | ||||
| @ -280,10 +330,15 @@ | ||||
|     chalk "^2.0.0" | ||||
|     js-tokens "^4.0.0" | ||||
| 
 | ||||
| "@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.7.0": | ||||
|   version "7.14.6" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.6.tgz#d85cc68ca3cac84eae384c06f032921f5227f4b2" | ||||
|   integrity sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ== | ||||
| "@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.8", "@babel/parser@^7.7.0": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4" | ||||
|   integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA== | ||||
| 
 | ||||
| "@babel/parser@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.0.tgz#b6d6e29058ca369127b0eeca2a1c4b5794f1b6b9" | ||||
|   integrity sha512-0v7oNOjr6YT9Z2RAOTv4T9aP+ubfx4Q/OhVtAet7PFDt0t9Oy6Jn+/rfC6b8HJ5zEqrQCiMxJfgtHpmIminmJQ== | ||||
| 
 | ||||
| "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5": | ||||
|   version "7.14.5" | ||||
| @ -294,10 +349,10 @@ | ||||
|     "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" | ||||
|     "@babel/plugin-proposal-optional-chaining" "^7.14.5" | ||||
| 
 | ||||
| "@babel/plugin-proposal-async-generator-functions@^7.14.7": | ||||
|   version "7.14.7" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz#784a48c3d8ed073f65adcf30b57bcbf6c8119ace" | ||||
|   integrity sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q== | ||||
| "@babel/plugin-proposal-async-generator-functions@^7.14.9": | ||||
|   version "7.14.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz#7028dc4fa21dc199bbacf98b39bab1267d0eaf9a" | ||||
|   integrity sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw== | ||||
|   dependencies: | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
|     "@babel/helper-remap-async-to-generator" "^7.14.5" | ||||
| @ -587,10 +642,10 @@ | ||||
|   dependencies: | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
| 
 | ||||
| "@babel/plugin-transform-classes@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz#0e98e82097b38550b03b483f9b51a78de0acb2cf" | ||||
|   integrity sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA== | ||||
| "@babel/plugin-transform-classes@^7.14.9": | ||||
|   version "7.14.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz#2a391ffb1e5292710b00f2e2c210e1435e7d449f" | ||||
|   integrity sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A== | ||||
|   dependencies: | ||||
|     "@babel/helper-annotate-as-pure" "^7.14.5" | ||||
|     "@babel/helper-function-name" "^7.14.5" | ||||
| @ -675,14 +730,14 @@ | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
|     babel-plugin-dynamic-import-node "^2.3.3" | ||||
| 
 | ||||
| "@babel/plugin-transform-modules-commonjs@^7.14.5": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz#7aaee0ea98283de94da98b28f8c35701429dad97" | ||||
|   integrity sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A== | ||||
| "@babel/plugin-transform-modules-commonjs@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz#3305896e5835f953b5cdb363acd9e8c2219a5281" | ||||
|   integrity sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig== | ||||
|   dependencies: | ||||
|     "@babel/helper-module-transforms" "^7.14.5" | ||||
|     "@babel/helper-module-transforms" "^7.15.0" | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
|     "@babel/helper-simple-access" "^7.14.5" | ||||
|     "@babel/helper-simple-access" "^7.14.8" | ||||
|     babel-plugin-dynamic-import-node "^2.3.3" | ||||
| 
 | ||||
| "@babel/plugin-transform-modules-systemjs@^7.14.5": | ||||
| @ -704,10 +759,10 @@ | ||||
|     "@babel/helper-module-transforms" "^7.14.5" | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
| 
 | ||||
| "@babel/plugin-transform-named-capturing-groups-regex@^7.14.7": | ||||
|   version "7.14.7" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz#60c06892acf9df231e256c24464bfecb0908fd4e" | ||||
|   integrity sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg== | ||||
| "@babel/plugin-transform-named-capturing-groups-regex@^7.14.9": | ||||
|   version "7.14.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz#c68f5c5d12d2ebaba3762e57c2c4f6347a46e7b2" | ||||
|   integrity sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA== | ||||
|   dependencies: | ||||
|     "@babel/helper-create-regexp-features-plugin" "^7.14.5" | ||||
| 
 | ||||
| @ -858,17 +913,17 @@ | ||||
|     "@babel/helper-create-regexp-features-plugin" "^7.14.5" | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
| 
 | ||||
| "@babel/preset-env@^7.14.7": | ||||
|   version "7.14.7" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.14.7.tgz#5c70b22d4c2d893b03d8c886a5c17422502b932a" | ||||
|   integrity sha512-itOGqCKLsSUl0Y+1nSfhbuuOlTs0MJk2Iv7iSH+XT/mR8U1zRLO7NjWlYXB47yhK4J/7j+HYty/EhFZDYKa/VA== | ||||
| "@babel/preset-env@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.0.tgz#e2165bf16594c9c05e52517a194bf6187d6fe464" | ||||
|   integrity sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q== | ||||
|   dependencies: | ||||
|     "@babel/compat-data" "^7.14.7" | ||||
|     "@babel/helper-compilation-targets" "^7.14.5" | ||||
|     "@babel/compat-data" "^7.15.0" | ||||
|     "@babel/helper-compilation-targets" "^7.15.0" | ||||
|     "@babel/helper-plugin-utils" "^7.14.5" | ||||
|     "@babel/helper-validator-option" "^7.14.5" | ||||
|     "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.14.5" | ||||
|     "@babel/plugin-proposal-async-generator-functions" "^7.14.7" | ||||
|     "@babel/plugin-proposal-async-generator-functions" "^7.14.9" | ||||
|     "@babel/plugin-proposal-class-properties" "^7.14.5" | ||||
|     "@babel/plugin-proposal-class-static-block" "^7.14.5" | ||||
|     "@babel/plugin-proposal-dynamic-import" "^7.14.5" | ||||
| @ -901,7 +956,7 @@ | ||||
|     "@babel/plugin-transform-async-to-generator" "^7.14.5" | ||||
|     "@babel/plugin-transform-block-scoped-functions" "^7.14.5" | ||||
|     "@babel/plugin-transform-block-scoping" "^7.14.5" | ||||
|     "@babel/plugin-transform-classes" "^7.14.5" | ||||
|     "@babel/plugin-transform-classes" "^7.14.9" | ||||
|     "@babel/plugin-transform-computed-properties" "^7.14.5" | ||||
|     "@babel/plugin-transform-destructuring" "^7.14.7" | ||||
|     "@babel/plugin-transform-dotall-regex" "^7.14.5" | ||||
| @ -912,10 +967,10 @@ | ||||
|     "@babel/plugin-transform-literals" "^7.14.5" | ||||
|     "@babel/plugin-transform-member-expression-literals" "^7.14.5" | ||||
|     "@babel/plugin-transform-modules-amd" "^7.14.5" | ||||
|     "@babel/plugin-transform-modules-commonjs" "^7.14.5" | ||||
|     "@babel/plugin-transform-modules-commonjs" "^7.15.0" | ||||
|     "@babel/plugin-transform-modules-systemjs" "^7.14.5" | ||||
|     "@babel/plugin-transform-modules-umd" "^7.14.5" | ||||
|     "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.7" | ||||
|     "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.9" | ||||
|     "@babel/plugin-transform-new-target" "^7.14.5" | ||||
|     "@babel/plugin-transform-object-super" "^7.14.5" | ||||
|     "@babel/plugin-transform-parameters" "^7.14.5" | ||||
| @ -930,11 +985,11 @@ | ||||
|     "@babel/plugin-transform-unicode-escapes" "^7.14.5" | ||||
|     "@babel/plugin-transform-unicode-regex" "^7.14.5" | ||||
|     "@babel/preset-modules" "^0.1.4" | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/types" "^7.15.0" | ||||
|     babel-plugin-polyfill-corejs2 "^0.2.2" | ||||
|     babel-plugin-polyfill-corejs3 "^0.2.2" | ||||
|     babel-plugin-polyfill-regenerator "^0.2.2" | ||||
|     core-js-compat "^3.15.0" | ||||
|     core-js-compat "^3.16.0" | ||||
|     semver "^6.3.0" | ||||
| 
 | ||||
| "@babel/preset-modules@^0.1.4": | ||||
| @ -975,10 +1030,10 @@ | ||||
|   dependencies: | ||||
|     regenerator-runtime "^0.12.0" | ||||
| 
 | ||||
| "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": | ||||
|   version "7.14.6" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" | ||||
|   integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== | ||||
| "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" | ||||
|   integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== | ||||
|   dependencies: | ||||
|     regenerator-runtime "^0.13.4" | ||||
| 
 | ||||
| @ -991,27 +1046,42 @@ | ||||
|     "@babel/parser" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
| 
 | ||||
| "@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.7.0": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.5.tgz#c111b0f58afab4fea3d3385a406f692748c59870" | ||||
|   integrity sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg== | ||||
| "@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8", "@babel/traverse@^7.7.0": | ||||
|   version "7.14.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.8.tgz#c0253f02677c5de1a8ff9df6b0aacbec7da1a8ce" | ||||
|   integrity sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg== | ||||
|   dependencies: | ||||
|     "@babel/code-frame" "^7.14.5" | ||||
|     "@babel/generator" "^7.14.5" | ||||
|     "@babel/generator" "^7.14.8" | ||||
|     "@babel/helper-function-name" "^7.14.5" | ||||
|     "@babel/helper-hoist-variables" "^7.14.5" | ||||
|     "@babel/helper-split-export-declaration" "^7.14.5" | ||||
|     "@babel/parser" "^7.14.5" | ||||
|     "@babel/types" "^7.14.5" | ||||
|     "@babel/parser" "^7.14.8" | ||||
|     "@babel/types" "^7.14.8" | ||||
|     debug "^4.1.0" | ||||
|     globals "^11.1.0" | ||||
| 
 | ||||
| "@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.14.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": | ||||
|   version "7.14.5" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff" | ||||
|   integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg== | ||||
| "@babel/traverse@^7.15.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98" | ||||
|   integrity sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw== | ||||
|   dependencies: | ||||
|     "@babel/helper-validator-identifier" "^7.14.5" | ||||
|     "@babel/code-frame" "^7.14.5" | ||||
|     "@babel/generator" "^7.15.0" | ||||
|     "@babel/helper-function-name" "^7.14.5" | ||||
|     "@babel/helper-hoist-variables" "^7.14.5" | ||||
|     "@babel/helper-split-export-declaration" "^7.14.5" | ||||
|     "@babel/parser" "^7.15.0" | ||||
|     "@babel/types" "^7.15.0" | ||||
|     debug "^4.1.0" | ||||
|     globals "^11.1.0" | ||||
| 
 | ||||
| "@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.15.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": | ||||
|   version "7.15.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd" | ||||
|   integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ== | ||||
|   dependencies: | ||||
|     "@babel/helper-validator-identifier" "^7.14.9" | ||||
|     to-fast-properties "^2.0.0" | ||||
| 
 | ||||
| "@bcoe/v8-coverage@^0.2.3": | ||||
| @ -2021,12 +2091,17 @@ anymatch@^3.0.3, anymatch@~3.1.1: | ||||
|     normalize-path "^3.0.0" | ||||
|     picomatch "^2.0.4" | ||||
| 
 | ||||
| aproba@^1.0.3, aproba@^1.1.1: | ||||
| "aproba@^1.0.3 || ^2.0.0": | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" | ||||
|   integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== | ||||
| 
 | ||||
| aproba@^1.1.1: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" | ||||
|   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== | ||||
| 
 | ||||
| are-we-there-yet@~1.1.2: | ||||
| are-we-there-yet@^1.1.5: | ||||
|   version "1.1.5" | ||||
|   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" | ||||
|   integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== | ||||
| @ -3144,6 +3219,11 @@ color-string@^1.5.2: | ||||
|     color-name "^1.0.0" | ||||
|     simple-swizzle "^0.2.2" | ||||
| 
 | ||||
| color-support@^1.1.2: | ||||
|   version "1.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" | ||||
|   integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== | ||||
| 
 | ||||
| color@^3.0.0: | ||||
|   version "3.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" | ||||
| @ -3240,7 +3320,7 @@ console-browserify@^1.1.0: | ||||
|   resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" | ||||
|   integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== | ||||
| 
 | ||||
| console-control-strings@^1.0.0, console-control-strings@~1.1.0: | ||||
| console-control-strings@^1.0.0, console-control-strings@^1.1.0: | ||||
|   version "1.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" | ||||
|   integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= | ||||
| @ -3296,10 +3376,10 @@ copy-descriptor@^0.1.0: | ||||
|   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" | ||||
|   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= | ||||
| 
 | ||||
| core-js-compat@^3.15.0, core-js-compat@^3.9.1: | ||||
|   version "3.15.1" | ||||
|   resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.15.1.tgz#1afe233716d37ee021956ef097594071b2b585a7" | ||||
|   integrity sha512-xGhzYMX6y7oEGQGAJmP2TmtBLvR4nZmRGEcFa3ubHOq5YEp51gGN9AovVa0AoujGZIq+Wm6dISiYyGNfdflYww== | ||||
| core-js-compat@^3.16.0, core-js-compat@^3.9.1: | ||||
|   version "3.16.0" | ||||
|   resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.16.0.tgz#fced4a0a534e7e02f7e084bff66c701f8281805f" | ||||
|   integrity sha512-5D9sPHCdewoUK7pSUPfTF7ZhLh8k9/CoJXWUEo+F1dZT5Z1DVgcuRqUKhjeKW+YLb8f21rTFgWwQJiNw1hoZ5Q== | ||||
|   dependencies: | ||||
|     browserslist "^4.16.6" | ||||
|     semver "7.0.0" | ||||
| @ -3976,10 +4056,10 @@ dot-prop@^5.2.0: | ||||
|   dependencies: | ||||
|     is-obj "^2.0.0" | ||||
| 
 | ||||
| dotenv@^9.0.2: | ||||
|   version "9.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" | ||||
|   integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== | ||||
| dotenv@^10.0.0: | ||||
|   version "10.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" | ||||
|   integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== | ||||
| 
 | ||||
| duplexer@^0.1.2: | ||||
|   version "0.1.2" | ||||
| @ -4455,10 +4535,10 @@ eslint@^2.7.0: | ||||
|     text-table "~0.2.0" | ||||
|     user-home "^2.0.0" | ||||
| 
 | ||||
| eslint@^7.31.0: | ||||
|   version "7.31.0" | ||||
|   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca" | ||||
|   integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA== | ||||
| eslint@^7.32.0: | ||||
|   version "7.32.0" | ||||
|   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" | ||||
|   integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== | ||||
|   dependencies: | ||||
|     "@babel/code-frame" "7.12.11" | ||||
|     "@eslint/eslintrc" "^0.4.3" | ||||
| @ -5077,19 +5157,20 @@ functional-red-black-tree@^1.0.1: | ||||
|   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" | ||||
|   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= | ||||
| 
 | ||||
| gauge@~2.7.3: | ||||
|   version "2.7.4" | ||||
|   resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" | ||||
|   integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= | ||||
| gauge@^3.0.0: | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.1.tgz#4bea07bcde3782f06dced8950e51307aa0f4a346" | ||||
|   integrity sha512-6STz6KdQgxO4S/ko+AbjlFGGdGcknluoqU+79GOFCDqqyYj5OanQf9AjxwN0jCidtT+ziPMmPSt9E4hfQ0CwIQ== | ||||
|   dependencies: | ||||
|     aproba "^1.0.3" | ||||
|     aproba "^1.0.3 || ^2.0.0" | ||||
|     color-support "^1.1.2" | ||||
|     console-control-strings "^1.0.0" | ||||
|     has-unicode "^2.0.0" | ||||
|     object-assign "^4.1.0" | ||||
|     has-unicode "^2.0.1" | ||||
|     object-assign "^4.1.1" | ||||
|     signal-exit "^3.0.0" | ||||
|     string-width "^1.0.1" | ||||
|     strip-ansi "^3.0.1" | ||||
|     wide-align "^1.1.0" | ||||
|     string-width "^1.0.1 || ^2.0.0" | ||||
|     strip-ansi "^3.0.1 || ^4.0.0" | ||||
|     wide-align "^1.1.2" | ||||
| 
 | ||||
| generate-function@^2.0.0: | ||||
|   version "2.3.1" | ||||
| @ -5334,7 +5415,7 @@ has-symbols@^1.0.2: | ||||
|   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" | ||||
|   integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== | ||||
| 
 | ||||
| has-unicode@^2.0.0: | ||||
| has-unicode@^2.0.1: | ||||
|   version "2.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" | ||||
|   integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= | ||||
| @ -7697,15 +7778,15 @@ npm-run-path@^4.0.0: | ||||
|   dependencies: | ||||
|     path-key "^3.0.0" | ||||
| 
 | ||||
| npmlog@^4.1.2: | ||||
|   version "4.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" | ||||
|   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== | ||||
| npmlog@^5.0.0: | ||||
|   version "5.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.0.tgz#e6a41b556e9b34cb29ea132294676c07acb30efb" | ||||
|   integrity sha512-ftpIiLjerL2tUg3dCqN8pOSoB90gqZlzv/gaZoxHaKjeLClrfJIEQ1Pdxi6qSzflz916Bljdy8dTWQ4J7hAFSQ== | ||||
|   dependencies: | ||||
|     are-we-there-yet "~1.1.2" | ||||
|     console-control-strings "~1.1.0" | ||||
|     gauge "~2.7.3" | ||||
|     set-blocking "~2.0.0" | ||||
|     are-we-there-yet "^1.1.5" | ||||
|     console-control-strings "^1.1.0" | ||||
|     gauge "^3.0.0" | ||||
|     set-blocking "^2.0.0" | ||||
| 
 | ||||
| nth-check@^1.0.2: | ||||
|   version "1.0.2" | ||||
| @ -9429,10 +9510,10 @@ regenerator-runtime@^0.12.0: | ||||
|   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" | ||||
|   integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== | ||||
| 
 | ||||
| regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: | ||||
|   version "0.13.7" | ||||
|   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" | ||||
|   integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== | ||||
| regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: | ||||
|   version "0.13.9" | ||||
|   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" | ||||
|   integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== | ||||
| 
 | ||||
| regenerator-transform@^0.14.2: | ||||
|   version "0.14.5" | ||||
| @ -9821,10 +9902,10 @@ sass-loader@^10.2.0: | ||||
|     schema-utils "^3.0.0" | ||||
|     semver "^7.3.2" | ||||
| 
 | ||||
| sass@^1.35.2: | ||||
|   version "1.35.2" | ||||
|   resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.2.tgz#b732314fcdaf7ef8d0f1698698adc378043cb821" | ||||
|   integrity sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw== | ||||
| sass@^1.37.0: | ||||
|   version "1.37.0" | ||||
|   resolved "https://registry.yarnpkg.com/sass/-/sass-1.37.0.tgz#f1b03a9d072ee9053a29d125c8130c78e92827c2" | ||||
|   integrity sha512-B+Tu6cSAG8ffs/cqsZl/bgSH2pCmavDaPTYAoW8QA1qNHh/RqndNfVKuABKYkLjUQ5aq/BnCENVpE80cqdSM1w== | ||||
|   dependencies: | ||||
|     chokidar ">=3.0.0 <4.0.0" | ||||
| 
 | ||||
| @ -9971,7 +10052,7 @@ serve-static@1.14.1: | ||||
|     parseurl "~1.3.3" | ||||
|     send "0.17.1" | ||||
| 
 | ||||
| set-blocking@^2.0.0, set-blocking@~2.0.0: | ||||
| set-blocking@^2.0.0: | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" | ||||
|   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= | ||||
| @ -10432,7 +10513,7 @@ string-width@^1.0.1: | ||||
|     is-fullwidth-code-point "^1.0.0" | ||||
|     strip-ansi "^3.0.0" | ||||
| 
 | ||||
| "string-width@^1.0.2 || 2", string-width@^2.0.0: | ||||
| "string-width@^1.0.1 || ^2.0.0", "string-width@^1.0.2 || 2", string-width@^2.0.0: | ||||
|   version "2.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" | ||||
|   integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== | ||||
| @ -10532,7 +10613,7 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: | ||||
|   dependencies: | ||||
|     ansi-regex "^2.0.0" | ||||
| 
 | ||||
| strip-ansi@^4.0.0: | ||||
| "strip-ansi@^3.0.1 || ^4.0.0", strip-ansi@^4.0.0: | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" | ||||
|   integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= | ||||
| @ -10716,9 +10797,9 @@ tapable@^1.0, tapable@^1.0.0, tapable@^1.1.3: | ||||
|   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== | ||||
| 
 | ||||
| tar@^6.0.2: | ||||
|   version "6.0.5" | ||||
|   resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" | ||||
|   integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== | ||||
|   version "6.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.3.tgz#e44b97ee7d6cc7a4c574e8b01174614538291825" | ||||
|   integrity sha512-3rUqwucgVZXTeyJyL2jqtUau8/8r54SioM1xj3AmTX3HnWQdj2AydfJ2qYYayPyIIznSplcvU9mhBb7dR2XF3w== | ||||
|   dependencies: | ||||
|     chownr "^2.0.0" | ||||
|     fs-minipass "^2.0.0" | ||||
| @ -11650,7 +11731,7 @@ wicg-inert@^3.1.1: | ||||
|   resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.1.tgz#b033fd4fbfb9e3fd709e5d84becbdf2e06e5c229" | ||||
|   integrity sha512-PhBaNh8ur9Xm4Ggy4umelwNIP6pPP1bv3EaWaKqfb/QNme2rdLjm7wIInvV4WhxVHhzA4Spgw9qNSqWtB/ca2A== | ||||
| 
 | ||||
| wide-align@^1.1.0: | ||||
| wide-align@^1.1.2: | ||||
|   version "1.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" | ||||
|   integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== | ||||
| @ -11730,11 +11811,16 @@ ws@^6.2.1: | ||||
|   dependencies: | ||||
|     async-limiter "~1.0.0" | ||||
| 
 | ||||
| ws@^7.2.3, ws@^7.3.1, ws@^7.5.3: | ||||
| ws@^7.2.3, ws@^7.3.1: | ||||
|   version "7.5.3" | ||||
|   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" | ||||
|   integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== | ||||
| 
 | ||||
| ws@^8.0.0: | ||||
|   version "8.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/ws/-/ws-8.0.0.tgz#550605d13dfc1437c9ec1396975709c6d7ffc57d" | ||||
|   integrity sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA== | ||||
| 
 | ||||
| xml-name-validator@^3.0.0: | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user