Merge pull request #1513 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						c7f04961b6
					
				
							
								
								
									
										5
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										5
									
								
								Gemfile
								
								
								
								
							|  | @ -111,7 +111,7 @@ group :development, :test do | |||
|   gem 'i18n-tasks', '~> 0.9', require: false | ||||
|   gem 'pry-byebug', '~> 3.9' | ||||
|   gem 'pry-rails', '~> 0.3' | ||||
|   gem 'rspec-rails', '~> 4.1' | ||||
|   gem 'rspec-rails', '~> 5.0' | ||||
| end | ||||
| 
 | ||||
| group :production, :test do | ||||
|  | @ -143,7 +143,7 @@ group :development do | |||
|   gem 'rubocop', '~> 1.11', require: false | ||||
|   gem 'rubocop-rails', '~> 2.9', require: false | ||||
|   gem 'brakeman', '~> 4.10', require: false | ||||
|   gem 'bundler-audit', '~> 0.7', require: false | ||||
|   gem 'bundler-audit', '~> 0.8', require: false | ||||
| 
 | ||||
|   gem 'capistrano', '~> 3.16' | ||||
|   gem 'capistrano-rails', '~> 1.6' | ||||
|  | @ -155,7 +155,6 @@ end | |||
| 
 | ||||
| group :production do | ||||
|   gem 'lograge', '~> 0.11' | ||||
|   gem 'redis-rails', '~> 5.0' | ||||
| end | ||||
| 
 | ||||
| gem 'concurrent-ruby', require: false | ||||
|  |  | |||
							
								
								
									
										33
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										33
									
								
								Gemfile.lock
								
								
								
								
							|  | @ -115,9 +115,9 @@ GEM | |||
|     bullet (6.1.4) | ||||
|       activesupport (>= 3.0.0) | ||||
|       uniform_notifier (~> 1.11) | ||||
|     bundler-audit (0.7.0.1) | ||||
|     bundler-audit (0.8.0) | ||||
|       bundler (>= 1.2.0, < 3) | ||||
|       thor (>= 0.18, < 2) | ||||
|       thor (~> 1.0) | ||||
|     byebug (11.1.3) | ||||
|     capistrano (3.16.0) | ||||
|       airbrussh (>= 1.0.0) | ||||
|  | @ -492,24 +492,8 @@ GEM | |||
|       rdf (~> 3.1) | ||||
|     redcarpet (3.5.1) | ||||
|     redis (4.2.5) | ||||
|     redis-actionpack (5.2.0) | ||||
|       actionpack (>= 5, < 7) | ||||
|       redis-rack (>= 2.1.0, < 3) | ||||
|       redis-store (>= 1.1.0, < 2) | ||||
|     redis-activesupport (5.2.0) | ||||
|       activesupport (>= 3, < 7) | ||||
|       redis-store (>= 1.3, < 2) | ||||
|     redis-namespace (1.8.1) | ||||
|       redis (>= 3.0.4) | ||||
|     redis-rack (2.1.3) | ||||
|       rack (>= 2.0.8, < 3) | ||||
|       redis-store (>= 1.2, < 2) | ||||
|     redis-rails (5.0.2) | ||||
|       redis-actionpack (>= 5.0, < 6) | ||||
|       redis-activesupport (>= 5.0, < 6) | ||||
|       redis-store (>= 1.2, < 2) | ||||
|     redis-store (1.9.0) | ||||
|       redis (>= 4, < 5) | ||||
|     regexp_parser (2.1.1) | ||||
|     request_store (1.5.0) | ||||
|       rack (>= 1.4) | ||||
|  | @ -531,10 +515,10 @@ GEM | |||
|     rspec-mocks (3.10.2) | ||||
|       diff-lcs (>= 1.2.0, < 2.0) | ||||
|       rspec-support (~> 3.10.0) | ||||
|     rspec-rails (4.1.0) | ||||
|       actionpack (>= 4.2) | ||||
|       activesupport (>= 4.2) | ||||
|       railties (>= 4.2) | ||||
|     rspec-rails (5.0.0) | ||||
|       actionpack (>= 5.2) | ||||
|       activesupport (>= 5.2) | ||||
|       railties (>= 5.2) | ||||
|       rspec-core (~> 3.10) | ||||
|       rspec-expectations (~> 3.10) | ||||
|       rspec-mocks (~> 3.10) | ||||
|  | @ -706,7 +690,7 @@ DEPENDENCIES | |||
|   brakeman (~> 4.10) | ||||
|   browser | ||||
|   bullet (~> 6.1) | ||||
|   bundler-audit (~> 0.7) | ||||
|   bundler-audit (~> 0.8) | ||||
|   capistrano (~> 3.16) | ||||
|   capistrano-rails (~> 1.6) | ||||
|   capistrano-rbenv (~> 2.2) | ||||
|  | @ -792,9 +776,8 @@ DEPENDENCIES | |||
|   redcarpet (~> 3.5) | ||||
|   redis (~> 4.2) | ||||
|   redis-namespace (~> 1.8) | ||||
|   redis-rails (~> 5.0) | ||||
|   rqrcode (~> 1.2) | ||||
|   rspec-rails (~> 4.1) | ||||
|   rspec-rails (~> 5.0) | ||||
|   rspec-sidekiq (~> 3.1) | ||||
|   rspec_junit_formatter (~> 0.4) | ||||
|   rubocop (~> 1.11) | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ module Admin | |||
|       if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) | ||||
|         @domain_block.save | ||||
|         flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety | ||||
|         @domain_block.errors[:domain].clear | ||||
|         @domain_block.errors.delete(:domain) | ||||
|         render :new | ||||
|       else | ||||
|         if existing_domain_block.present? | ||||
|  |  | |||
|  | @ -31,7 +31,9 @@ module CacheConcern | |||
|   def cache_collection(raw, klass) | ||||
|     return raw unless klass.respond_to?(:with_includes) | ||||
| 
 | ||||
|     raw                    = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) | ||||
|     raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) | ||||
|     return [] if raw.empty? | ||||
| 
 | ||||
|     cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) | ||||
|     uncached_ids           = raw.map(&:id) - cached_keys_with_value.keys | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { Iterable, fromJS } from 'immutable'; | ||||
| import { hydrateCompose } from './compose'; | ||||
| import { importFetchedAccounts } from './importer'; | ||||
| import { saveSettings } from './settings'; | ||||
| 
 | ||||
| export const STORE_HYDRATE = 'STORE_HYDRATE'; | ||||
| export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; | ||||
|  | @ -9,9 +10,22 @@ const convertState = rawState => | |||
|   fromJS(rawState, (k, v) => | ||||
|     Iterable.isIndexed(v) ? v.toList() : v.toMap()); | ||||
| 
 | ||||
| const applyMigrations = (state) => { | ||||
|   return state.withMutations(state => { | ||||
|     // Migrate glitch-soc local-only “Show unread marker” setting to Mastodon's setting
 | ||||
|     if (state.getIn(['local_settings', 'notifications', 'show_unread']) !== undefined) { | ||||
|       // Only change if the Mastodon setting does not deviate from default
 | ||||
|       if (state.getIn(['settings', 'notifications', 'showUnread']) !== false) { | ||||
|         state.setIn(['settings', 'notifications', 'showUnread'], state.getIn(['local_settings', 'notifications', 'show_unread'])); | ||||
|       } | ||||
|       state.removeIn(['local_settings', 'notifications', 'show_unread']) | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export function hydrateStore(rawState) { | ||||
|   return dispatch => { | ||||
|     const state = convertState(rawState); | ||||
|     const state = applyMigrations(convertState(rawState)); | ||||
| 
 | ||||
|     dispatch({ | ||||
|       type: STORE_HYDRATE, | ||||
|  | @ -20,5 +34,6 @@ export function hydrateStore(rawState) { | |||
| 
 | ||||
|     dispatch(hydrateCompose()); | ||||
|     dispatch(importFetchedAccounts(Object.values(rawState.accounts))); | ||||
|     dispatch(saveSettings()); | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -113,14 +113,6 @@ class LocalSettingsPage extends React.PureComponent { | |||
|             <FormattedMessage id='settings.notifications.favicon_badge' defaultMessage='Unread notifications favicon badge' /> | ||||
|             <span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage="Add a badge for unread notifications to the favicon" /></span> | ||||
|           </LocalSettingsPageItem> | ||||
|           <LocalSettingsPageItem | ||||
|             settings={settings} | ||||
|             item={['notifications', 'show_unread']} | ||||
|             id='mastodon-settings--notifications-show_unread' | ||||
|             onChange={onChange} | ||||
|           > | ||||
|             <FormattedMessage id='settings.notifications.show_unread' defaultMessage='Show unread marker' /> | ||||
|           </LocalSettingsPageItem> | ||||
|         </section> | ||||
|         <section> | ||||
|           <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2> | ||||
|  |  | |||
|  | @ -56,6 +56,16 @@ export default class ColumnSettings extends React.PureComponent { | |||
|           <ClearColumnButton onClick={onClear} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div role='group' aria-labelledby='notifications-unread-markers'> | ||||
|           <span id='notifications-unread-markers' className='column-settings__section'> | ||||
|             <FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' /> | ||||
|           </span> | ||||
| 
 | ||||
|           <div className='column-settings__row'> | ||||
|             <SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div role='group' aria-labelledby='notifications-filter-bar'> | ||||
|           <span id='notifications-filter-bar' className='column-settings__section'> | ||||
|             <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /> | ||||
|  |  | |||
|  | @ -67,8 +67,8 @@ const mapStateToProps = state => ({ | |||
|   hasMore: state.getIn(['notifications', 'hasMore']), | ||||
|   numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, | ||||
|   notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), | ||||
|   lastReadId: state.getIn(['local_settings', 'notifications', 'show_unread']) ? state.getIn(['notifications', 'readMarkerId']) : '0', | ||||
|   canMarkAsRead: state.getIn(['local_settings', 'notifications', 'show_unread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), | ||||
|   lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0', | ||||
|   canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), | ||||
|   needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']), | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -55,7 +55,6 @@ const initialState = ImmutableMap({ | |||
|   notifications : ImmutableMap({ | ||||
|     favicon_badge : false, | ||||
|     tab_badge     : true, | ||||
|     show_unread   : true, | ||||
|   }), | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ const initialState = ImmutableMap({ | |||
|     }), | ||||
| 
 | ||||
|     dismissPermissionBanner: false, | ||||
|     showUnread: true, | ||||
| 
 | ||||
|     shows: ImmutableMap({ | ||||
|       follow: true, | ||||
|  |  | |||
|  | @ -55,6 +55,16 @@ export default class ColumnSettings extends React.PureComponent { | |||
|           <ClearColumnButton onClick={onClear} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div role='group' aria-labelledby='notifications-unread-markers'> | ||||
|           <span id='notifications-unread-markers' className='column-settings__section'> | ||||
|             <FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' /> | ||||
|           </span> | ||||
| 
 | ||||
|           <div className='column-settings__row'> | ||||
|             <SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div role='group' aria-labelledby='notifications-filter-bar'> | ||||
|           <span id='notifications-filter-bar' className='column-settings__section'> | ||||
|             <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /> | ||||
|  |  | |||
|  | @ -60,8 +60,8 @@ const mapStateToProps = state => ({ | |||
|   isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, | ||||
|   hasMore: state.getIn(['notifications', 'hasMore']), | ||||
|   numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, | ||||
|   lastReadId: state.getIn(['notifications', 'readMarkerId']), | ||||
|   canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), | ||||
|   lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0', | ||||
|   canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), | ||||
|   needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']), | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -45,6 +45,7 @@ const initialState = ImmutableMap({ | |||
|     }), | ||||
| 
 | ||||
|     dismissPermissionBanner: false, | ||||
|     showUnread: true, | ||||
| 
 | ||||
|     shows: ImmutableMap({ | ||||
|       follow: true, | ||||
|  |  | |||
|  | @ -16,7 +16,9 @@ class EntityCache | |||
|   end | ||||
| 
 | ||||
|   def emoji(shortcodes, domain) | ||||
|     shortcodes   = Array(shortcodes) | ||||
|     shortcodes = Array(shortcodes) | ||||
|     return [] if shortcodes.empty? | ||||
| 
 | ||||
|     cached       = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) }) | ||||
|     uncached_ids = [] | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'singleton' | ||||
| require_relative './sanitize_config' | ||||
| 
 | ||||
| class HTMLRenderer < Redcarpet::Render::HTML | ||||
|   def block_code(code, language) | ||||
|  | @ -223,9 +222,9 @@ class Formatter | |||
|           original_url, static_url = emoji | ||||
|           replacement = begin | ||||
|             if animate | ||||
|               "<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(original_url)}\" />" | ||||
|               image_tag(original_url, draggable: false, class: 'emojione', alt: ":#{shortcode}:", title: ":#{shortcode}:") | ||||
|             else | ||||
|               "<img draggable=\"false\" class=\"emojione custom-emoji\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(static_url)}\" data-original=\"#{original_url}\" data-static=\"#{static_url}\" />" | ||||
|               image_tag(original_url, draggable: false, class: 'emojione custom-emoji', alt: ":#{shortcode}:", title: ":#{shortcode}:", data: { original: original_url, static: static_url }) | ||||
|             end | ||||
|           end | ||||
|           before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : '' | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ class NotificationMailer < ApplicationMailer | |||
|   helper :accounts | ||||
|   helper :statuses | ||||
| 
 | ||||
|   add_template_helper RoutingHelper | ||||
|   helper RoutingHelper | ||||
| 
 | ||||
|   def mention(recipient, notification) | ||||
|     @me     = recipient | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ class UserMailer < Devise::Mailer | |||
|   helper :instance | ||||
|   helper :statuses | ||||
| 
 | ||||
|   add_template_helper RoutingHelper | ||||
|   helper RoutingHelper | ||||
| 
 | ||||
|   def confirmation_instructions(user, token, **) | ||||
|     @resource = user | ||||
|  |  | |||
|  | @ -18,46 +18,4 @@ class AccountStat < ApplicationRecord | |||
|   belongs_to :account, inverse_of: :account_stat | ||||
| 
 | ||||
|   update_index('accounts#account', :account) | ||||
| 
 | ||||
|   def increment_count!(key) | ||||
|     update(attributes_for_increment(key)) | ||||
|   rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique | ||||
|     begin | ||||
|       reload_with_id | ||||
|     rescue ActiveRecord::RecordNotFound | ||||
|       return | ||||
|     end | ||||
| 
 | ||||
|     retry | ||||
|   end | ||||
| 
 | ||||
|   def decrement_count!(key) | ||||
|     update(attributes_for_decrement(key)) | ||||
|   rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique | ||||
|     begin | ||||
|       reload_with_id | ||||
|     rescue ActiveRecord::RecordNotFound | ||||
|       return | ||||
|     end | ||||
| 
 | ||||
|     retry | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def attributes_for_increment(key) | ||||
|     attrs = { key => public_send(key) + 1 } | ||||
|     attrs[:last_status_at] = Time.now.utc if key == :statuses_count | ||||
|     attrs | ||||
|   end | ||||
| 
 | ||||
|   def attributes_for_decrement(key) | ||||
|     attrs = { key => [public_send(key) - 1, 0].max } | ||||
|     attrs | ||||
|   end | ||||
| 
 | ||||
|   def reload_with_id | ||||
|     self.id = self.class.find_by!(account: account).id if new_record? | ||||
|     reload | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -3,6 +3,8 @@ | |||
| module AccountCounters | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze | ||||
| 
 | ||||
|   included do | ||||
|     has_one :account_stat, inverse_of: :account | ||||
|     after_save :save_account_stat | ||||
|  | @ -14,11 +16,65 @@ module AccountCounters | |||
|            :following_count=, | ||||
|            :followers_count, | ||||
|            :followers_count=, | ||||
|            :increment_count!, | ||||
|            :decrement_count!, | ||||
|            :last_status_at, | ||||
|            to: :account_stat | ||||
| 
 | ||||
|   # @param [Symbol] key | ||||
|   def increment_count!(key) | ||||
|     update_count!(key, 1) | ||||
|   end | ||||
| 
 | ||||
|   # @param [Symbol] key | ||||
|   def decrement_count!(key) | ||||
|     update_count!(key, -1) | ||||
|   end | ||||
| 
 | ||||
|   # @param [Symbol] key | ||||
|   # @param [Integer] value | ||||
|   def update_count!(key, value) | ||||
|     raise ArgumentError, "Invalid key #{key}" unless ALLOWED_COUNTER_KEYS.include?(key) | ||||
|     raise ArgumentError, 'Do not call update_count! on dirty objects' if association(:account_stat).loaded? && account_stat&.changed? && account_stat.changed_attribute_names_to_save == %w(id) | ||||
| 
 | ||||
|     value = value.to_i | ||||
|     default_value = value.positive? ? value : 0 | ||||
| 
 | ||||
|     # We do an upsert using manually written SQL, as Rails' upsert method does | ||||
|     # not seem to support writing expressions in the UPDATE clause, but only | ||||
|     # re-insert the provided values instead. | ||||
|     # Even ARel seem to be missing proper handling of upserts. | ||||
|     sql = if value.positive? && key == :statuses_count | ||||
|             <<-SQL.squish | ||||
|               INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at) | ||||
|                 VALUES (:account_id, :default_value, now(), now(), now()) | ||||
|               ON CONFLICT (account_id) DO UPDATE | ||||
|               SET #{key} = account_stats.#{key} + :value, | ||||
|                   last_status_at = now(), | ||||
|                   lock_version = account_stats.lock_version + 1, | ||||
|                   updated_at = now() | ||||
|               RETURNING id; | ||||
|             SQL | ||||
|           else | ||||
|             <<-SQL.squish | ||||
|               INSERT INTO account_stats(account_id, #{key}, created_at, updated_at) | ||||
|                 VALUES (:account_id, :default_value, now(), now()) | ||||
|               ON CONFLICT (account_id) DO UPDATE | ||||
|               SET #{key} = account_stats.#{key} + :value, | ||||
|                   lock_version = account_stats.lock_version + 1, | ||||
|                   updated_at = now() | ||||
|               RETURNING id; | ||||
|             SQL | ||||
|           end | ||||
| 
 | ||||
|     sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value]) | ||||
|     account_stat_id = AccountStat.connection.exec_query(sql)[0]['id'] | ||||
| 
 | ||||
|     # Reload account_stat if it was loaded, taking into account newly-created unsaved records | ||||
|     if association(:account_stat).loaded? | ||||
|       account_stat.id = account_stat_id if account_stat.new_record? | ||||
|       account_stat.reload | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def account_stat | ||||
|     super || build_account_stat | ||||
|   end | ||||
|  |  | |||
|  | @ -49,12 +49,12 @@ class Notification < ApplicationRecord | |||
|   belongs_to :from_account, class_name: 'Account', optional: true | ||||
|   belongs_to :activity, polymorphic: true, optional: true | ||||
| 
 | ||||
|   belongs_to :mention,        foreign_type: 'Mention',       foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :status,         foreign_type: 'Status',        foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :follow,         foreign_type: 'Follow',        foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :favourite,      foreign_type: 'Favourite',     foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :poll,           foreign_type: 'Poll',          foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :mention,        foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :status,         foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :follow,         foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :follow_request, foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :favourite,      foreign_key: 'activity_id', optional: true | ||||
|   belongs_to :poll,           foreign_key: 'activity_id', optional: true | ||||
| 
 | ||||
|   validates :type, inclusion: { in: TYPES } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class UrlValidator < ActiveModel::EachValidator | ||||
| class URLValidator < ActiveModel::EachValidator | ||||
|   def validate_each(record, attribute, value) | ||||
|     record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value) | ||||
|   end | ||||
|  |  | |||
|  | @ -6,8 +6,9 @@ require 'rails/all' | |||
| # you've limited to :test, :development, or :production. | ||||
| Bundler.require(*Rails.groups) | ||||
| 
 | ||||
| require_relative '../app/lib/exceptions' | ||||
| require_relative '../lib/exceptions' | ||||
| require_relative '../lib/enumerable' | ||||
| require_relative '../lib/sanitize_ext/sanitize_config' | ||||
| require_relative '../lib/redis/namespace_extensions' | ||||
| require_relative '../lib/paperclip/url_generator_extensions' | ||||
| require_relative '../lib/paperclip/attachment_extensions' | ||||
|  | @ -27,6 +28,7 @@ require_relative '../lib/webpacker/manifest_extensions' | |||
| require_relative '../lib/webpacker/helper_extensions' | ||||
| require_relative '../lib/action_dispatch/cookie_jar_extensions' | ||||
| require_relative '../lib/rails/engine_extensions' | ||||
| require_relative '../lib/active_record/database_tasks_extensions' | ||||
| 
 | ||||
| Dotenv::Railtie.load | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ Rails.application.configure do | |||
|   if Rails.root.join('tmp/caching-dev.txt').exist? | ||||
|     config.action_controller.perform_caching = true | ||||
| 
 | ||||
|     config.cache_store = :redis_store, ENV['REDIS_URL'], REDIS_CACHE_PARAMS | ||||
|     config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS | ||||
| 
 | ||||
|     config.public_file_server.headers = { | ||||
|       'Cache-Control' => "public, max-age=#{2.days.to_i}", | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ Rails.application.configure do | |||
|   config.log_tags = [:request_id] | ||||
| 
 | ||||
|   # Use a different cache store in production. | ||||
|   config.cache_store = :redis_store, ENV['CACHE_REDIS_URL'], REDIS_CACHE_PARAMS | ||||
|   config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS | ||||
| 
 | ||||
|   # Ignore bad email addresses and do not raise email delivery errors. | ||||
|   # Set this to true and configure the email server for immediate delivery to raise delivery errors. | ||||
|  |  | |||
|  | @ -20,6 +20,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| | |||
|   inflect.acronym 'JsonLd' | ||||
|   inflect.acronym 'NodeInfo' | ||||
|   inflect.acronym 'Ed25519' | ||||
|   inflect.acronym 'TOC' | ||||
|   inflect.acronym 'RSS' | ||||
|   inflect.acronym 'REST' | ||||
|   inflect.acronym 'URL' | ||||
| 
 | ||||
|   inflect.singular 'data', 'data' | ||||
| end | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| namespace    = ENV.fetch('REDIS_NAMESPACE') { nil } | ||||
| redis_params = { url: ENV['REDIS_URL'] } | ||||
| redis_params = { url: ENV['REDIS_URL'], driver: :hiredis } | ||||
| 
 | ||||
| if namespace | ||||
|   redis_params[:namespace] = namespace | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| class AddUrlToStatuses < ActiveRecord::Migration[4.2] | ||||
| class AddURLToStatuses < ActiveRecord::Migration[4.2] | ||||
|   def change | ||||
|     add_column :statuses, :url, :string, null: true, default: nil | ||||
|   end | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| class AddUrlToAccounts < ActiveRecord::Migration[4.2] | ||||
| class AddURLToAccounts < ActiveRecord::Migration[4.2] | ||||
|   def change | ||||
|     add_column :accounts, :url, :string, null: true, default: nil | ||||
|   end | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| class AddAvatarRemoteUrlToAccounts < ActiveRecord::Migration[4.2] | ||||
| class AddAvatarRemoteURLToAccounts < ActiveRecord::Migration[4.2] | ||||
|   def change | ||||
|     add_column :accounts, :avatar_remote_url, :string, null: true, default: nil | ||||
|   end | ||||
|  |  | |||
|  | @ -7,12 +7,12 @@ end | |||
| class RailsSettingsMigration < MIGRATION_BASE_CLASS | ||||
|   def self.up | ||||
|     create_table :settings do |t| | ||||
|       t.string     :var, :null => false | ||||
|       t.string     :var, null: false | ||||
|       t.text       :value | ||||
|       t.references :target, :null => false, :polymorphic => true | ||||
|       t.timestamps :null => true | ||||
|       t.references :target, null: false, polymorphic: true, index: { name: 'index_settings_on_target_type_and_target_id' } | ||||
|       t.timestamps null: true | ||||
|     end | ||||
|     add_index :settings, [ :target_type, :target_id, :var ], :unique => true | ||||
|     add_index :settings, [ :target_type, :target_id, :var ], unique: true | ||||
|   end | ||||
| 
 | ||||
|   def self.down | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| class AddHeaderRemoteUrlToAccounts < ActiveRecord::Migration[5.0] | ||||
| class AddHeaderRemoteURLToAccounts < ActiveRecord::Migration[5.0] | ||||
|   def change | ||||
|     add_column :accounts, :header_remote_url, :string, null: false, default: '' | ||||
|   end | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1] | ||||
|   def up | ||||
|     # Prepare the function we will use to generate IDs. | ||||
|     Rake::Task['db:define_timestamp_id'].execute | ||||
|     Mastodon::Snowflake.define_timestamp_id | ||||
| 
 | ||||
|     # Set up the statuses.id column to use our timestamp-based IDs. | ||||
|     ActiveRecord::Base.connection.execute(<<~SQL) | ||||
|  | @ -11,7 +11,7 @@ class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1] | |||
|     SQL | ||||
| 
 | ||||
|     # Make sure we have a sequence to use. | ||||
|     Rake::Task['db:ensure_id_sequences_exist'].execute | ||||
|     Mastodon::Snowflake.ensure_id_sequences_exist | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ class CreateAdminActionLogs < ActiveRecord::Migration[5.1] | |||
|     create_table :admin_action_logs do |t| | ||||
|       t.belongs_to :account, foreign_key: { on_delete: :cascade } | ||||
|       t.string :action, null: false, default: '' | ||||
|       t.references :target, polymorphic: true | ||||
|       t.references :target, polymorphic: true, index: { name: 'index_admin_action_logs_on_target_type_and_target_id' } | ||||
|       t.text :recorded_changes, null: false, default: '' | ||||
| 
 | ||||
|       t.timestamps | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| require Rails.root.join('lib', 'mastodon', 'migration_helpers') | ||||
| 
 | ||||
| class AddEmbedUrlToPreviewCards < ActiveRecord::Migration[5.1] | ||||
| class AddEmbedURLToPreviewCards < ActiveRecord::Migration[5.1] | ||||
|   include Mastodon::MigrationHelpers | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| class AddFeaturedCollectionUrlToAccounts < ActiveRecord::Migration[5.1] | ||||
| class AddFeaturedCollectionURLToAccounts < ActiveRecord::Migration[5.1] | ||||
|   def change | ||||
|     add_column :accounts, :featured_collection_url, :string | ||||
|   end | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2] | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_hash | ||||
|     duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_ary | ||||
| 
 | ||||
|     duplicates.each do |row| | ||||
|       deduplicate_account!(row['ids'].split(',')) | ||||
|  |  | |||
|  | @ -17,8 +17,8 @@ class MigrateAccountConversations < ActiveRecord::Migration[5.2] | |||
|     belongs_to :account, optional: true | ||||
|     belongs_to :activity, polymorphic: true, optional: true | ||||
| 
 | ||||
|     belongs_to :status,         foreign_type: 'Status',        foreign_key: 'activity_id', optional: true | ||||
|     belongs_to :mention,        foreign_type: 'Mention',       foreign_key: 'activity_id', optional: true | ||||
|     belongs_to :status,  foreign_key: 'activity_id', optional: true | ||||
|     belongs_to :mention, foreign_key: 'activity_id', optional: true | ||||
| 
 | ||||
|     def target_status | ||||
|       mention&.status | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ class DowncaseCustomEmojiDomains < ActiveRecord::Migration[5.2] | |||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     duplicates = CustomEmoji.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM custom_emojis GROUP BY shortcode, lower(domain) HAVING count(*) > 1').to_hash | ||||
|     duplicates = CustomEmoji.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM custom_emojis GROUP BY shortcode, lower(domain) HAVING count(*) > 1').to_ary | ||||
| 
 | ||||
|     duplicates.each do |row| | ||||
|       CustomEmoji.where(id: row['ids'].split(',')[0...-1]).destroy_all | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2] | |||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     Tag.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM tags GROUP BY lower(name) HAVING count(*) > 1').to_hash.each do |row| | ||||
|     Tag.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM tags GROUP BY lower(name) HAVING count(*) > 1').to_ary.each do |row| | ||||
|       canonical_tag_id  = row['ids'].split(',').first | ||||
|       redundant_tag_ids = row['ids'].split(',')[1..-1] | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| class AddDevicesUrlToAccounts < ActiveRecord::Migration[5.2] | ||||
| class AddDevicesURLToAccounts < ActiveRecord::Migration[5.2] | ||||
|   def change | ||||
|     add_column :accounts, :devices_url, :string | ||||
|   end | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require_relative '../mastodon/snowflake' | ||||
| 
 | ||||
| module ActiveRecord | ||||
|   module Tasks | ||||
|     module DatabaseTasks | ||||
|       original_load_schema = instance_method(:load_schema) | ||||
| 
 | ||||
|       define_method(:load_schema) do |db_config, *args| | ||||
|         ActiveRecord::Base.establish_connection(db_config) | ||||
|         Mastodon::Snowflake.define_timestamp_id | ||||
| 
 | ||||
|         original_load_schema.bind(self).call(db_config, *args) | ||||
| 
 | ||||
|         Mastodon::Snowflake.ensure_id_sequences_exist | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -14,7 +14,7 @@ module Mastodon | |||
|     end | ||||
| 
 | ||||
|     MIN_SUPPORTED_VERSION = 2019_10_01_213028 | ||||
|     MAX_SUPPORTED_VERSION = 2020_12_18_054746 | ||||
|     MAX_SUPPORTED_VERSION = 2021_03_08_133107 | ||||
| 
 | ||||
|     # Stubs to enjoy ActiveRecord queries while not depending on a particular | ||||
|     # version of the code/database | ||||
|  | @ -142,7 +142,6 @@ module Mastodon | |||
|       @prompt.warn 'Please make sure to stop Mastodon and have a backup.' | ||||
|       exit(1) unless @prompt.yes?('Continue?') | ||||
| 
 | ||||
|       deduplicate_accounts! | ||||
|       deduplicate_users! | ||||
|       deduplicate_account_domain_blocks! | ||||
|       deduplicate_account_identity_proofs! | ||||
|  | @ -157,6 +156,7 @@ module Mastodon | |||
|       deduplicate_media_attachments! | ||||
|       deduplicate_preview_cards! | ||||
|       deduplicate_statuses! | ||||
|       deduplicate_accounts! | ||||
|       deduplicate_tags! | ||||
|       deduplicate_webauthn_credentials! | ||||
| 
 | ||||
|  |  | |||
|  | @ -41,42 +41,18 @@ | |||
| 
 | ||||
| module Mastodon | ||||
|   module MigrationHelpers | ||||
|     # Stub for Database.postgresql? from GitLab | ||||
|     def self.postgresql? | ||||
|       ActiveRecord::Base.configurations[Rails.env]['adapter'].casecmp('postgresql').zero? | ||||
|     end | ||||
| 
 | ||||
|     # Stub for Database.mysql? from GitLab | ||||
|     def self.mysql? | ||||
|       ActiveRecord::Base.configurations[Rails.env]['adapter'].casecmp('mysql2').zero? | ||||
|     end | ||||
| 
 | ||||
|     # Model that can be used for querying permissions of a SQL user. | ||||
|     class Grant < ActiveRecord::Base | ||||
|       self.table_name = | ||||
|         if Mastodon::MigrationHelpers.postgresql? | ||||
|           'information_schema.role_table_grants' | ||||
|         else | ||||
|           'mysql.user' | ||||
|         end | ||||
|       self.table_name = 'information_schema.role_table_grants' | ||||
| 
 | ||||
|       def self.scope_to_current_user | ||||
|         if Mastodon::MigrationHelpers.postgresql? | ||||
|           where('grantee = user') | ||||
|         else | ||||
|           where("CONCAT(User, '@', Host) = current_user()") | ||||
|         end | ||||
|         where('grantee = user') | ||||
|       end | ||||
| 
 | ||||
|       # Returns true if the current user can create and execute triggers on the | ||||
|       # given table. | ||||
|       def self.create_and_execute_trigger?(table) | ||||
|         priv = | ||||
|           if Mastodon::MigrationHelpers.postgresql? | ||||
|             where(privilege_type: 'TRIGGER', table_name: table) | ||||
|           else | ||||
|             where(Trigger_priv: 'Y') | ||||
|           end | ||||
|         priv = where(privilege_type: 'TRIGGER', table_name: table) | ||||
| 
 | ||||
|         priv.scope_to_current_user.any? | ||||
|       end | ||||
|  | @ -141,10 +117,8 @@ module Mastodon | |||
|           'in the body of your migration class' | ||||
|       end | ||||
| 
 | ||||
|       if MigrationHelpers.postgresql? | ||||
|         options = options.merge({ algorithm: :concurrently }) | ||||
|         disable_statement_timeout | ||||
|       end | ||||
|       options = options.merge({ algorithm: :concurrently }) | ||||
|       disable_statement_timeout | ||||
| 
 | ||||
|       add_index(table_name, column_name, options) | ||||
|     end | ||||
|  | @ -199,8 +173,6 @@ module Mastodon | |||
| 
 | ||||
|     # Only available on Postgresql >= 9.2 | ||||
|     def supports_drop_index_concurrently? | ||||
|       return false unless MigrationHelpers.postgresql? | ||||
| 
 | ||||
|       version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i | ||||
| 
 | ||||
|       version >= 90200 | ||||
|  | @ -226,13 +198,7 @@ module Mastodon | |||
|       # While MySQL does allow disabling of foreign keys it has no equivalent | ||||
|       # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall | ||||
|       # back to the normal foreign key procedure. | ||||
|       if MigrationHelpers.mysql? | ||||
|         return add_foreign_key(source, target, | ||||
|                                column: column, | ||||
|                                on_delete: on_delete) | ||||
|       else | ||||
|         on_delete = 'SET NULL' if on_delete == :nullify | ||||
|       end | ||||
|       on_delete = 'SET NULL' if on_delete == :nullify | ||||
| 
 | ||||
|       disable_statement_timeout | ||||
| 
 | ||||
|  | @ -270,7 +236,7 @@ module Mastodon | |||
|     # the database. Disable the session's statement timeout to ensure | ||||
|     # migrations don't get killed prematurely. (PostgreSQL only) | ||||
|     def disable_statement_timeout | ||||
|       execute('SET statement_timeout TO 0') if MigrationHelpers.postgresql? | ||||
|       execute('SET statement_timeout TO 0') | ||||
|     end | ||||
| 
 | ||||
|     # Updates the value of a column in batches. | ||||
|  | @ -319,7 +285,7 @@ module Mastodon | |||
|         count_arel = table.project(Arel.star.count.as('count')) | ||||
|         count_arel = yield table, count_arel if block_given? | ||||
| 
 | ||||
|         total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i | ||||
|         total = exec_query(count_arel.to_sql).to_ary.first['count'].to_i | ||||
| 
 | ||||
|         return if total == 0 | ||||
|       end | ||||
|  | @ -335,7 +301,7 @@ module Mastodon | |||
| 
 | ||||
|       start_arel = table.project(table[:id]).order(table[:id].asc).take(1) | ||||
|       start_arel = yield table, start_arel if block_given? | ||||
|       first_row = exec_query(start_arel.to_sql).to_hash.first | ||||
|       first_row = exec_query(start_arel.to_sql).to_ary.first | ||||
|       # In case there are no rows but we didn't catch it in the estimated size: | ||||
|       return unless first_row | ||||
|       start_id = first_row['id'].to_i | ||||
|  | @ -356,7 +322,7 @@ module Mastodon | |||
|             .skip(batch_size) | ||||
| 
 | ||||
|           stop_arel = yield table, stop_arel if block_given? | ||||
|           stop_row = exec_query(stop_arel.to_sql).to_hash.first | ||||
|           stop_row = exec_query(stop_arel.to_sql).to_ary.first | ||||
| 
 | ||||
|           update_arel = Arel::UpdateManager.new | ||||
|             .table(table) | ||||
|  | @ -487,11 +453,7 @@ module Mastodon | |||
|       # If we were in the middle of update_column_in_batches, we should remove | ||||
|       # the old column and start over, as we have no idea where we were. | ||||
|       if column_for(table, new) | ||||
|         if MigrationHelpers.postgresql? | ||||
|           remove_rename_triggers_for_postgresql(table, trigger_name) | ||||
|         else | ||||
|           remove_rename_triggers_for_mysql(trigger_name) | ||||
|         end | ||||
|         remove_rename_triggers_for_postgresql(table, trigger_name) | ||||
| 
 | ||||
|         remove_column(table, new) | ||||
|       end | ||||
|  | @ -521,13 +483,8 @@ module Mastodon | |||
|       quoted_old = quote_column_name(old) | ||||
|       quoted_new = quote_column_name(new) | ||||
| 
 | ||||
|       if MigrationHelpers.postgresql? | ||||
|         install_rename_triggers_for_postgresql(trigger_name, quoted_table, | ||||
|                                                quoted_old, quoted_new) | ||||
|       else | ||||
|         install_rename_triggers_for_mysql(trigger_name, quoted_table, | ||||
|                                           quoted_old, quoted_new) | ||||
|       end | ||||
|       install_rename_triggers_for_postgresql(trigger_name, quoted_table, | ||||
|                                              quoted_old, quoted_new) | ||||
| 
 | ||||
|       update_column_in_batches(table, new, Arel::Table.new(table)[old]) | ||||
| 
 | ||||
|  | @ -685,11 +642,7 @@ module Mastodon | |||
| 
 | ||||
|       check_trigger_permissions!(table) | ||||
| 
 | ||||
|       if MigrationHelpers.postgresql? | ||||
|         remove_rename_triggers_for_postgresql(table, trigger_name) | ||||
|       else | ||||
|         remove_rename_triggers_for_mysql(trigger_name) | ||||
|       end | ||||
|       remove_rename_triggers_for_postgresql(table, trigger_name) | ||||
| 
 | ||||
|       remove_column(table, old) | ||||
|     end | ||||
|  | @ -844,18 +797,9 @@ module Mastodon | |||
|       quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s) | ||||
|       quoted_replacement = Arel::Nodes::Quoted.new(replacement.to_s) | ||||
| 
 | ||||
|       if MigrationHelpers.mysql? | ||||
|         locate = Arel::Nodes::NamedFunction | ||||
|           .new('locate', [quoted_pattern, column]) | ||||
|         insert_in_place = Arel::Nodes::NamedFunction | ||||
|           .new('insert', [column, locate, pattern.size, quoted_replacement]) | ||||
| 
 | ||||
|         Arel::Nodes::SqlLiteral.new(insert_in_place.to_sql) | ||||
|       else | ||||
|         replace = Arel::Nodes::NamedFunction | ||||
|           .new("regexp_replace", [column, quoted_pattern, quoted_replacement]) | ||||
|         Arel::Nodes::SqlLiteral.new(replace.to_sql) | ||||
|       end | ||||
|       replace = Arel::Nodes::NamedFunction | ||||
|         .new("regexp_replace", [column, quoted_pattern, quoted_replacement]) | ||||
|       Arel::Nodes::SqlLiteral.new(replace.to_sql) | ||||
|     end | ||||
| 
 | ||||
|     def remove_foreign_key_without_error(*args) | ||||
|  |  | |||
|  | @ -27,6 +27,8 @@ namespace       = ENV.fetch('REDIS_NAMESPACE', nil) | |||
| cache_namespace = namespace ? namespace + '_cache' : 'cache' | ||||
| 
 | ||||
| REDIS_CACHE_PARAMS = { | ||||
|   driver: :hiredis, | ||||
|   url: ENV['REDIS_URL'], | ||||
|   expires_in: 10.minutes, | ||||
|   namespace: cache_namespace, | ||||
| }.freeze | ||||
|  |  | |||
|  | @ -1,36 +1,5 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require_relative '../mastodon/snowflake' | ||||
| 
 | ||||
| def each_schema_load_environment | ||||
|   # If we're in development, also run this for the test environment. | ||||
|   # This is a somewhat hacky way to do this, so here's why: | ||||
|   # 1. We have to define this before we load the schema, or we won't | ||||
|   #    have a timestamp_id function when we get to it in the schema. | ||||
|   # 2. db:setup calls db:schema:load_if_ruby, which calls | ||||
|   #    db:schema:load, which we define above as having a prerequisite | ||||
|   #    of this task. | ||||
|   # 3. db:schema:load ends up running | ||||
|   #    ActiveRecord::Tasks::DatabaseTasks.load_schema_current, which | ||||
|   #    calls a private method `each_current_configuration`, which | ||||
|   #    explicitly also does the loading for the `test` environment | ||||
|   #    if the current environment is `development`, so we end up | ||||
|   #    needing to do the same, and we can't even use the same method | ||||
|   #    to do it. | ||||
| 
 | ||||
|   if Rails.env.development? | ||||
|     test_conf = ActiveRecord::Base.configurations['test'] | ||||
| 
 | ||||
|     if test_conf['database']&.present? | ||||
|       ActiveRecord::Base.establish_connection(:test) | ||||
|       yield | ||||
|       ActiveRecord::Base.establish_connection(Rails.env.to_sym) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   yield | ||||
| end | ||||
| 
 | ||||
| namespace :db do | ||||
|   namespace :migrate do | ||||
|     desc 'Setup the db or migrate depending on state of db' | ||||
|  | @ -50,7 +19,7 @@ namespace :db do | |||
| 
 | ||||
|   task :post_migration_hook do | ||||
|     at_exit do | ||||
|       unless %w(C POSIX).include?(ActiveRecord::Base.connection.execute('SELECT datcollate FROM pg_database WHERE datname = current_database();').first['datcollate']) | ||||
|       unless %w(C POSIX).include?(ActiveRecord::Base.connection.select_one('SELECT datcollate FROM pg_database WHERE datname = current_database();')['datcollate']) | ||||
|         warn <<~WARNING | ||||
|           Your database collation is susceptible to index corruption. | ||||
|             (This warning does not indicate that index corruption has occured and can be ignored) | ||||
|  | @ -60,30 +29,11 @@ namespace :db do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   task :pre_migration_check do | ||||
|     version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i | ||||
|     abort 'ERROR: This version of Mastodon requires PostgreSQL 9.5 or newer. Please update PostgreSQL before updating Mastodon.' if version < 90_500 | ||||
|   end | ||||
| 
 | ||||
|   Rake::Task['db:migrate'].enhance(['db:pre_migration_check']) | ||||
|   Rake::Task['db:migrate'].enhance(['db:post_migration_hook']) | ||||
| 
 | ||||
|   # Before we load the schema, define the timestamp_id function. | ||||
|   # Idiomatically, we might do this in a migration, but then it | ||||
|   # wouldn't end up in schema.rb, so we'd need to figure out a way to | ||||
|   # get it in before doing db:setup as well. This is simpler, and | ||||
|   # ensures it's always in place. | ||||
|   Rake::Task['db:schema:load'].enhance ['db:define_timestamp_id'] | ||||
| 
 | ||||
|   # After we load the schema, make sure we have sequences for each | ||||
|   # table using timestamp IDs. | ||||
|   Rake::Task['db:schema:load'].enhance do | ||||
|     Rake::Task['db:ensure_id_sequences_exist'].invoke | ||||
|   end | ||||
| 
 | ||||
|   task :define_timestamp_id do | ||||
|     each_schema_load_environment do | ||||
|       Mastodon::Snowflake.define_timestamp_id | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   task :ensure_id_sequences_exist do | ||||
|     each_schema_load_environment do | ||||
|       Mastodon::Snowflake.ensure_id_sequences_exist | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
							
								
								
									
										14
									
								
								package.json
								
								
								
								
							
							
						
						
									
										14
									
								
								package.json
								
								
								
								
							|  | @ -60,14 +60,14 @@ | |||
|   }, | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@babel/core": "^7.13.8", | ||||
|     "@babel/core": "^7.13.10", | ||||
|     "@babel/plugin-proposal-class-properties": "^7.8.3", | ||||
|     "@babel/plugin-proposal-decorators": "^7.13.5", | ||||
|     "@babel/plugin-transform-react-inline-elements": "^7.12.13", | ||||
|     "@babel/plugin-transform-runtime": "^7.13.9", | ||||
|     "@babel/preset-env": "^7.13.9", | ||||
|     "@babel/plugin-transform-runtime": "^7.13.10", | ||||
|     "@babel/preset-env": "^7.13.10", | ||||
|     "@babel/preset-react": "^7.12.13", | ||||
|     "@babel/runtime": "^7.13.9", | ||||
|     "@babel/runtime": "^7.13.10", | ||||
|     "@clusterws/cws": "^3.0.0", | ||||
|     "@gamestdio/websocket": "^0.3.2", | ||||
|     "@github/webauthn-json": "^0.5.7", | ||||
|  | @ -88,7 +88,7 @@ | |||
|     "color-blend": "^3.0.1", | ||||
|     "compression-webpack-plugin": "^6.1.1", | ||||
|     "cross-env": "^7.0.3", | ||||
|     "css-loader": "^5.1.1", | ||||
|     "css-loader": "^5.1.2", | ||||
|     "cssnano": "^4.1.10", | ||||
|     "detect-passive-events": "^2.0.3", | ||||
|     "dotenv": "^8.2.0", | ||||
|  | @ -146,7 +146,7 @@ | |||
|     "react-sparklines": "^1.7.0", | ||||
|     "react-swipeable-views": "^0.13.9", | ||||
|     "react-textarea-autosize": "^8.3.2", | ||||
|     "react-toggle": "^4.1.1", | ||||
|     "react-toggle": "^4.1.2", | ||||
|     "redis": "^3.0.2", | ||||
|     "redux": "^4.0.5", | ||||
|     "redux-immutable": "^4.0.0", | ||||
|  | @ -179,7 +179,7 @@ | |||
|     "@testing-library/react": "^11.2.5", | ||||
|     "babel-eslint": "^10.1.0", | ||||
|     "babel-jest": "^26.6.3", | ||||
|     "eslint": "^7.21.0", | ||||
|     "eslint": "^7.22.0", | ||||
|     "eslint-plugin-import": "~2.22.1", | ||||
|     "eslint-plugin-jsx-a11y": "~6.4.1", | ||||
|     "eslint-plugin-promise": "~4.3.1", | ||||
|  |  | |||
|  | @ -370,7 +370,7 @@ RSpec.describe AccountsController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'cachable response' | ||||
|  | @ -402,7 +402,7 @@ RSpec.describe AccountsController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns public Cache-Control header' do | ||||
|  | @ -428,7 +428,7 @@ RSpec.describe AccountsController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'cachable response' | ||||
|  | @ -446,7 +446,7 @@ RSpec.describe AccountsController, type: :controller do | |||
|           end | ||||
| 
 | ||||
|           it 'returns application/activity+json' do | ||||
|             expect(response.content_type).to eq 'application/activity+json' | ||||
|             expect(response.media_type).to eq 'application/activity+json' | ||||
|           end | ||||
| 
 | ||||
|           it 'returns private Cache-Control header' do | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'cachable response' | ||||
|  | @ -88,7 +88,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do | |||
|           end | ||||
| 
 | ||||
|           it 'returns application/activity+json' do | ||||
|             expect(response.content_type).to eq 'application/activity+json' | ||||
|             expect(response.media_type).to eq 'application/activity+json' | ||||
|           end | ||||
| 
 | ||||
|           it_behaves_like 'cachable response' | ||||
|  | @ -116,7 +116,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do | |||
|             end | ||||
| 
 | ||||
|             it 'returns application/activity+json' do | ||||
|               expect(response.content_type).to eq 'application/activity+json' | ||||
|               expect(response.media_type).to eq 'application/activity+json' | ||||
|             end | ||||
| 
 | ||||
|             it 'returns private Cache-Control header' do | ||||
|  | @ -141,7 +141,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do | |||
|             end | ||||
| 
 | ||||
|             it 'returns application/activity+json' do | ||||
|               expect(response.content_type).to eq 'application/activity+json' | ||||
|               expect(response.media_type).to eq 'application/activity+json' | ||||
|             end | ||||
| 
 | ||||
|             it 'returns private Cache-Control header' do | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll | |||
|       end | ||||
| 
 | ||||
|       it 'returns application/activity+json' do | ||||
|         expect(response.content_type).to eq 'application/activity+json' | ||||
|         expect(response.media_type).to eq 'application/activity+json' | ||||
|       end | ||||
| 
 | ||||
|       it 'returns orderedItems with followers from example.com' do | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns totalItems' do | ||||
|  | @ -85,7 +85,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns orderedItems with public or unlisted statuses' do | ||||
|  | @ -133,7 +133,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns orderedItems with public or unlisted statuses' do | ||||
|  | @ -159,7 +159,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns orderedItems with private statuses' do | ||||
|  | @ -185,7 +185,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns empty orderedItems' do | ||||
|  | @ -210,7 +210,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it 'returns empty orderedItems' do | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do | |||
|         end | ||||
| 
 | ||||
|         it 'returns application/activity+json' do | ||||
|           expect(response.content_type).to eq 'application/activity+json' | ||||
|           expect(response.media_type).to eq 'application/activity+json' | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'cachable response' | ||||
|  | @ -120,7 +120,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do | |||
|           end | ||||
| 
 | ||||
|           it 'returns application/activity+json' do | ||||
|             expect(response.content_type).to eq 'application/activity+json' | ||||
|             expect(response.media_type).to eq 'application/activity+json' | ||||
|           end | ||||
| 
 | ||||
|           it_behaves_like 'cachable response' | ||||
|  |  | |||
|  | @ -0,0 +1,40 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe CacheConcern, type: :controller do | ||||
|   controller(ApplicationController) do | ||||
|     include CacheConcern | ||||
| 
 | ||||
|     def empty_array | ||||
|       render plain: cache_collection([], Status).size | ||||
|     end | ||||
| 
 | ||||
|     def empty_relation | ||||
|       render plain: cache_collection(Status.none, Status).size | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     routes.draw do | ||||
|       get  'empty_array' => 'anonymous#empty_array' | ||||
|       post 'empty_relation' => 'anonymous#empty_relation' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#cache_collection' do | ||||
|     context 'given an empty array' do | ||||
|       it 'returns an empty array' do | ||||
|         get :empty_array | ||||
|         expect(response.body).to eq '0' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'given an empty relation' do | ||||
|       it 'returns an empty array' do | ||||
|         get :empty_relation | ||||
|         expect(response.body).to eq '0' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -22,8 +22,8 @@ describe ApplicationController, type: :controller do | |||
|       get :index, format: :csv | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'text/csv' | ||||
|       expect(response.headers['Content-Disposition']).to eq 'attachment; filename="anonymous.csv"' | ||||
|       expect(response.media_type).to eq 'text/csv' | ||||
|       expect(response.headers['Content-Disposition']).to start_with 'attachment; filename="anonymous.csv"' | ||||
|       expect(response.body).to eq user.account.username | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ describe WellKnown::HostMetaController, type: :controller do | |||
|       get :show, format: :xml | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/xrd+xml' | ||||
|       expect(response.media_type).to eq 'application/xrd+xml' | ||||
|       expect(response.body).to eq <<XML | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ describe WellKnown::KeybaseProofConfigController, type: :controller do | |||
|       get :show | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/json' | ||||
|       expect(response.media_type).to eq 'application/json' | ||||
|       expect { JSON.parse(response.body) }.not_to raise_exception | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ describe WellKnown::NodeInfoController, type: :controller do | |||
|       get :index | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/json' | ||||
|       expect(response.media_type).to eq 'application/json' | ||||
| 
 | ||||
|       json = body_as_json | ||||
| 
 | ||||
|  | @ -23,7 +23,7 @@ describe WellKnown::NodeInfoController, type: :controller do | |||
|       get :show | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/json' | ||||
|       expect(response.media_type).to eq 'application/json' | ||||
| 
 | ||||
|       json = body_as_json | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ describe WellKnown::WebfingerController, type: :controller do | |||
|       end | ||||
| 
 | ||||
|       it 'returns application/jrd+json' do | ||||
|         expect(response.content_type).to eq 'application/jrd+json' | ||||
|         expect(response.media_type).to eq 'application/jrd+json' | ||||
|       end | ||||
| 
 | ||||
|       it 'returns links for the account' do | ||||
|  |  | |||
|  | @ -0,0 +1,19 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe EntityCache do | ||||
|   let(:local_account)  { Fabricate(:account, domain: nil, username: 'alice') } | ||||
|   let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') } | ||||
| 
 | ||||
|   describe '#emoji' do | ||||
|     subject { EntityCache.instance.emoji(shortcodes, domain) } | ||||
| 
 | ||||
|     context 'called with an empty list of shortcodes' do | ||||
|       let(:shortcodes) { [] } | ||||
|       let(:domain)     { 'example.org' } | ||||
| 
 | ||||
|       it 'returns an empty array' do | ||||
|         is_expected.to eq [] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,7 +1,6 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'rails_helper' | ||||
| require Rails.root.join('app', 'lib', 'sanitize_config.rb') | ||||
| 
 | ||||
| describe Sanitize::Config do | ||||
|   shared_examples 'common HTML sanitization' do | ||||
|  |  | |||
|  | @ -1,57 +0,0 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe AccountStat, type: :model do | ||||
|   describe '#increment_count!' do | ||||
|     it 'increments the count' do | ||||
|       account_stat = AccountStat.create(account: Fabricate(:account)) | ||||
|       expect(account_stat.followers_count).to eq 0 | ||||
|       account_stat.increment_count!(:followers_count) | ||||
|       expect(account_stat.followers_count).to eq 1 | ||||
|     end | ||||
| 
 | ||||
|     it 'increments the count in multi-threaded an environment' do | ||||
|       account_stat   = AccountStat.create(account: Fabricate(:account), statuses_count: 0) | ||||
|       increment_by   = 15 | ||||
|       wait_for_start = true | ||||
| 
 | ||||
|       threads = Array.new(increment_by) do | ||||
|         Thread.new do | ||||
|           true while wait_for_start | ||||
|           AccountStat.find(account_stat.id).increment_count!(:statuses_count) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       wait_for_start = false | ||||
|       threads.each(&:join) | ||||
| 
 | ||||
|       expect(account_stat.reload.statuses_count).to eq increment_by | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#decrement_count!' do | ||||
|     it 'decrements the count' do | ||||
|       account_stat = AccountStat.create(account: Fabricate(:account), followers_count: 15) | ||||
|       expect(account_stat.followers_count).to eq 15 | ||||
|       account_stat.decrement_count!(:followers_count) | ||||
|       expect(account_stat.followers_count).to eq 14 | ||||
|     end | ||||
| 
 | ||||
|     it 'decrements the count in multi-threaded an environment' do | ||||
|       account_stat   = AccountStat.create(account: Fabricate(:account), statuses_count: 15) | ||||
|       decrement_by   = 10 | ||||
|       wait_for_start = true | ||||
| 
 | ||||
|       threads = Array.new(decrement_by) do | ||||
|         Thread.new do | ||||
|           true while wait_for_start | ||||
|           AccountStat.find(account_stat.id).decrement_count!(:statuses_count) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       wait_for_start = false | ||||
|       threads.each(&:join) | ||||
| 
 | ||||
|       expect(account_stat.reload.statuses_count).to eq 5 | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,60 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| describe AccountCounters do | ||||
|   let!(:account) { Fabricate(:account) } | ||||
| 
 | ||||
|   describe '#increment_count!' do | ||||
|     it 'increments the count' do | ||||
|       expect(account.followers_count).to eq 0 | ||||
|       account.increment_count!(:followers_count) | ||||
|       expect(account.followers_count).to eq 1 | ||||
|     end | ||||
| 
 | ||||
|     it 'increments the count in multi-threaded an environment' do | ||||
|       increment_by   = 15 | ||||
|       wait_for_start = true | ||||
| 
 | ||||
|       threads = Array.new(increment_by) do | ||||
|         Thread.new do | ||||
|           true while wait_for_start | ||||
|           account.increment_count!(:statuses_count) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       wait_for_start = false | ||||
|       threads.each(&:join) | ||||
| 
 | ||||
|       expect(account.statuses_count).to eq increment_by | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#decrement_count!' do | ||||
|     it 'decrements the count' do | ||||
|       account.followers_count = 15 | ||||
|       account.save! | ||||
|       expect(account.followers_count).to eq 15 | ||||
|       account.decrement_count!(:followers_count) | ||||
|       expect(account.followers_count).to eq 14 | ||||
|     end | ||||
| 
 | ||||
|     it 'decrements the count in multi-threaded an environment' do | ||||
|       decrement_by   = 10 | ||||
|       wait_for_start = true | ||||
| 
 | ||||
|       account.statuses_count = 15 | ||||
|       account.save! | ||||
| 
 | ||||
|       threads = Array.new(decrement_by) do | ||||
|         Thread.new do | ||||
|           true while wait_for_start | ||||
|           account.decrement_count!(:statuses_count) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       wait_for_start = false | ||||
|       threads.each(&:join) | ||||
| 
 | ||||
|       expect(account.statuses_count).to eq 5 | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -6,7 +6,7 @@ describe "The catch all route" do | |||
|       get "/test" | ||||
| 
 | ||||
|       expect(response.status).to eq 404 | ||||
|       expect(response.content_type).to eq "text/html" | ||||
|       expect(response.media_type).to eq "text/html" | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -15,7 +15,7 @@ describe "The catch all route" do | |||
|       get "/test.test" | ||||
| 
 | ||||
|       expect(response.status).to eq 404 | ||||
|       expect(response.content_type).to eq "text/html" | ||||
|       expect(response.media_type).to eq "text/html" | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ describe "The host_meta route" do | |||
|       get host_meta_url | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq "application/xrd+xml" | ||||
|       expect(response.media_type).to eq "application/xrd+xml" | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ describe 'Link headers' do | |||
|     end | ||||
| 
 | ||||
|     def link_header_with_type(type) | ||||
|       response.headers['Link'].links.find do |link| | ||||
|       LinkHeader.parse(response.headers['Link'].to_s).links.find do |link| | ||||
|         link.attr_pairs.any? { |pair| pair == ['type', type] } | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ describe 'The webfinger route' do | |||
|       get webfinger_url(resource: alice.to_webfinger_s) | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/jrd+json' | ||||
|       expect(response.media_type).to eq 'application/jrd+json' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -17,7 +17,7 @@ describe 'The webfinger route' do | |||
|       get webfinger_url(resource: alice.to_webfinger_s, format: :json) | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/jrd+json' | ||||
|       expect(response.media_type).to eq 'application/jrd+json' | ||||
|     end | ||||
| 
 | ||||
|     it 'returns a json response for json accept header' do | ||||
|  | @ -25,7 +25,7 @@ describe 'The webfinger route' do | |||
|       get webfinger_url(resource: alice.to_webfinger_s), headers: headers | ||||
| 
 | ||||
|       expect(response).to have_http_status(200) | ||||
|       expect(response.content_type).to eq 'application/jrd+json' | ||||
|       expect(response.media_type).to eq 'application/jrd+json' | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe UrlValidator, type: :validator do | ||||
| RSpec.describe URLValidator, type: :validator do | ||||
|   describe '#validate_each' do | ||||
|     before do | ||||
|       allow(validator).to receive(:compliant?).with(value) { compliant } | ||||
|  |  | |||
							
								
								
									
										131
									
								
								yarn.lock
								
								
								
								
							
							
						
						
									
										131
									
								
								yarn.lock
								
								
								
								
							|  | @ -21,17 +21,17 @@ | |||
|   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" | ||||
|   integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== | ||||
| 
 | ||||
| "@babel/core@^7.1.0", "@babel/core@^7.13.8", "@babel/core@^7.7.2", "@babel/core@^7.7.5": | ||||
|   version "7.13.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.8.tgz#c191d9c5871788a591d69ea1dc03e5843a3680fb" | ||||
|   integrity sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg== | ||||
| "@babel/core@^7.1.0", "@babel/core@^7.13.10", "@babel/core@^7.7.2", "@babel/core@^7.7.5": | ||||
|   version "7.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559" | ||||
|   integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw== | ||||
|   dependencies: | ||||
|     "@babel/code-frame" "^7.12.13" | ||||
|     "@babel/generator" "^7.13.0" | ||||
|     "@babel/helper-compilation-targets" "^7.13.8" | ||||
|     "@babel/generator" "^7.13.9" | ||||
|     "@babel/helper-compilation-targets" "^7.13.10" | ||||
|     "@babel/helper-module-transforms" "^7.13.0" | ||||
|     "@babel/helpers" "^7.13.0" | ||||
|     "@babel/parser" "^7.13.4" | ||||
|     "@babel/helpers" "^7.13.10" | ||||
|     "@babel/parser" "^7.13.10" | ||||
|     "@babel/template" "^7.12.13" | ||||
|     "@babel/traverse" "^7.13.0" | ||||
|     "@babel/types" "^7.13.0" | ||||
|  | @ -43,10 +43,10 @@ | |||
|     semver "^6.3.0" | ||||
|     source-map "^0.5.0" | ||||
| 
 | ||||
| "@babel/generator@^7.13.0": | ||||
|   version "7.13.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.0.tgz#bd00d4394ca22f220390c56a0b5b85568ec1ec0c" | ||||
|   integrity sha512-zBZfgvBB/ywjx0Rgc2+BwoH/3H+lDtlgD4hBOpEv5LxRnYsm/753iRuLepqnYlynpjC3AdQxtxsoeHJoEEwOAw== | ||||
| "@babel/generator@^7.13.0", "@babel/generator@^7.13.9": | ||||
|   version "7.13.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" | ||||
|   integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.13.0" | ||||
|     jsesc "^2.5.1" | ||||
|  | @ -82,10 +82,10 @@ | |||
|     "@babel/helper-annotate-as-pure" "^7.12.13" | ||||
|     "@babel/types" "^7.12.13" | ||||
| 
 | ||||
| "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.8": | ||||
|   version "7.13.8" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz#02bdb22783439afb11b2f009814bdd88384bd468" | ||||
|   integrity sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A== | ||||
| "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8": | ||||
|   version "7.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c" | ||||
|   integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA== | ||||
|   dependencies: | ||||
|     "@babel/compat-data" "^7.13.8" | ||||
|     "@babel/helper-validator-option" "^7.12.17" | ||||
|  | @ -274,10 +274,10 @@ | |||
|     "@babel/traverse" "^7.13.0" | ||||
|     "@babel/types" "^7.13.0" | ||||
| 
 | ||||
| "@babel/helpers@^7.13.0": | ||||
|   version "7.13.0" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0" | ||||
|   integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ== | ||||
| "@babel/helpers@^7.13.10": | ||||
|   version "7.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8" | ||||
|   integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ== | ||||
|   dependencies: | ||||
|     "@babel/template" "^7.12.13" | ||||
|     "@babel/traverse" "^7.13.0" | ||||
|  | @ -292,10 +292,10 @@ | |||
|     chalk "^2.0.0" | ||||
|     js-tokens "^4.0.0" | ||||
| 
 | ||||
| "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.4", "@babel/parser@^7.7.0": | ||||
|   version "7.13.4" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab" | ||||
|   integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA== | ||||
| "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10", "@babel/parser@^7.7.0": | ||||
|   version "7.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" | ||||
|   integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== | ||||
| 
 | ||||
| "@babel/plugin-proposal-async-generator-functions@^7.13.8": | ||||
|   version "7.13.8" | ||||
|  | @ -765,10 +765,10 @@ | |||
|   dependencies: | ||||
|     "@babel/helper-plugin-utils" "^7.12.13" | ||||
| 
 | ||||
| "@babel/plugin-transform-runtime@^7.13.9": | ||||
|   version "7.13.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.9.tgz#744d3103338a0d6c90dee0497558150b490cee07" | ||||
|   integrity sha512-XCxkY/wBI6M6Jj2mlWxkmqbKPweRanszWbF3Tyut+hKh+PHcuIH/rSr/7lmmE7C3WW+HSIm2GT+d5jwmheuB0g== | ||||
| "@babel/plugin-transform-runtime@^7.13.10": | ||||
|   version "7.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1" | ||||
|   integrity sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA== | ||||
|   dependencies: | ||||
|     "@babel/helper-module-imports" "^7.12.13" | ||||
|     "@babel/helper-plugin-utils" "^7.13.0" | ||||
|  | @ -828,13 +828,13 @@ | |||
|     "@babel/helper-create-regexp-features-plugin" "^7.12.13" | ||||
|     "@babel/helper-plugin-utils" "^7.12.13" | ||||
| 
 | ||||
| "@babel/preset-env@^7.13.9": | ||||
|   version "7.13.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.9.tgz#3ee5f233316b10d066d7f379c6d1e13a96853654" | ||||
|   integrity sha512-mcsHUlh2rIhViqMG823JpscLMesRt3QbMsv1+jhopXEb3W2wXvQ9QoiOlZI9ZbR3XqPtaFpZwEZKYqGJnGMZTQ== | ||||
| "@babel/preset-env@^7.13.10": | ||||
|   version "7.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252" | ||||
|   integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ== | ||||
|   dependencies: | ||||
|     "@babel/compat-data" "^7.13.8" | ||||
|     "@babel/helper-compilation-targets" "^7.13.8" | ||||
|     "@babel/helper-compilation-targets" "^7.13.10" | ||||
|     "@babel/helper-plugin-utils" "^7.13.0" | ||||
|     "@babel/helper-validator-option" "^7.12.17" | ||||
|     "@babel/plugin-proposal-async-generator-functions" "^7.13.8" | ||||
|  | @ -939,10 +939,10 @@ | |||
|   dependencies: | ||||
|     regenerator-runtime "^0.12.0" | ||||
| 
 | ||||
| "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.9", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@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.13.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.9.tgz#97dbe2116e2630c489f22e0656decd60aaa1fcee" | ||||
|   integrity sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA== | ||||
| "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@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.13.10" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" | ||||
|   integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== | ||||
|   dependencies: | ||||
|     regenerator-runtime "^0.13.4" | ||||
| 
 | ||||
|  | @ -3060,6 +3060,11 @@ colorette@^1.2.1: | |||
|   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" | ||||
|   integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== | ||||
| 
 | ||||
| colorette@^1.2.2: | ||||
|   version "1.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" | ||||
|   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== | ||||
| 
 | ||||
| combined-stream@^1.0.6, combined-stream@~1.0.6: | ||||
|   version "1.0.8" | ||||
|   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" | ||||
|  | @ -3373,16 +3378,16 @@ css-list-helpers@^1.0.1: | |||
|   dependencies: | ||||
|     tcomb "^2.5.0" | ||||
| 
 | ||||
| css-loader@^5.1.1: | ||||
|   version "5.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.1.tgz#9362d444a0f7c08c148a109596715c904e252879" | ||||
|   integrity sha512-5FfhpjwtuRgxqmusDidowqmLlcb+1HgnEDMsi2JhiUrZUcoc+cqw+mUtMIF/+OfeMYaaFCLYp1TaIt9H6I/fKA== | ||||
| css-loader@^5.1.2: | ||||
|   version "5.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.2.tgz#b93dba498ec948b543b49d4fab5017205d4f5c3e" | ||||
|   integrity sha512-T7vTXHSx0KrVEg/xjcl7G01RcVXpcw4OELwDPvkr7izQNny85A84dK3dqrczuEfBcu7Yg7mdTjJLSTibRUoRZg== | ||||
|   dependencies: | ||||
|     camelcase "^6.2.0" | ||||
|     cssesc "^3.0.0" | ||||
|     icss-utils "^5.1.0" | ||||
|     loader-utils "^2.0.0" | ||||
|     postcss "^8.2.6" | ||||
|     postcss "^8.2.8" | ||||
|     postcss-modules-extract-imports "^3.0.0" | ||||
|     postcss-modules-local-by-default "^4.0.0" | ||||
|     postcss-modules-scope "^3.0.0" | ||||
|  | @ -4336,10 +4341,10 @@ eslint@^2.7.0: | |||
|     text-table "~0.2.0" | ||||
|     user-home "^2.0.0" | ||||
| 
 | ||||
| eslint@^7.21.0: | ||||
|   version "7.21.0" | ||||
|   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" | ||||
|   integrity sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg== | ||||
| eslint@^7.22.0: | ||||
|   version "7.22.0" | ||||
|   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.22.0.tgz#07ecc61052fec63661a2cab6bd507127c07adc6f" | ||||
|   integrity sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg== | ||||
|   dependencies: | ||||
|     "@babel/code-frame" "7.12.11" | ||||
|     "@eslint/eslintrc" "^0.4.0" | ||||
|  | @ -4358,7 +4363,7 @@ eslint@^7.21.0: | |||
|     file-entry-cache "^6.0.1" | ||||
|     functional-red-black-tree "^1.0.1" | ||||
|     glob-parent "^5.0.0" | ||||
|     globals "^12.1.0" | ||||
|     globals "^13.6.0" | ||||
|     ignore "^4.0.6" | ||||
|     import-fresh "^3.0.0" | ||||
|     imurmurhash "^0.1.4" | ||||
|  | @ -4366,7 +4371,7 @@ eslint@^7.21.0: | |||
|     js-yaml "^3.13.1" | ||||
|     json-stable-stringify-without-jsonify "^1.0.1" | ||||
|     levn "^0.4.1" | ||||
|     lodash "^4.17.20" | ||||
|     lodash "^4.17.21" | ||||
|     minimatch "^3.0.4" | ||||
|     natural-compare "^1.4.0" | ||||
|     optionator "^0.9.1" | ||||
|  | @ -5117,6 +5122,13 @@ globals@^12.1.0: | |||
|   dependencies: | ||||
|     type-fest "^0.8.1" | ||||
| 
 | ||||
| globals@^13.6.0: | ||||
|   version "13.6.0" | ||||
|   resolved "https://registry.yarnpkg.com/globals/-/globals-13.6.0.tgz#d77138e53738567bb96a3916ff6f6b487af20ef7" | ||||
|   integrity sha512-YFKCX0SiPg7l5oKYCJ2zZGxcXprVXHcSnVuvzrT3oSENQonVLqM5pf9fN5dLGZGyCjhw8TN8Btwe/jKnZ0pjvQ== | ||||
|   dependencies: | ||||
|     type-fest "^0.20.2" | ||||
| 
 | ||||
| globals@^9.2.0: | ||||
|   version "9.18.0" | ||||
|   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" | ||||
|  | @ -8484,12 +8496,12 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27, postcss@^7.0.32: | |||
|     source-map "^0.6.1" | ||||
|     supports-color "^6.1.0" | ||||
| 
 | ||||
| postcss@^8.2.6: | ||||
|   version "8.2.6" | ||||
|   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" | ||||
|   integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== | ||||
| postcss@^8.2.8: | ||||
|   version "8.2.8" | ||||
|   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece" | ||||
|   integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw== | ||||
|   dependencies: | ||||
|     colorette "^1.2.1" | ||||
|     colorette "^1.2.2" | ||||
|     nanoid "^3.1.20" | ||||
|     source-map "^0.6.1" | ||||
| 
 | ||||
|  | @ -8998,10 +9010,10 @@ react-textarea-autosize@^8.3.2: | |||
|     use-composed-ref "^1.0.0" | ||||
|     use-latest "^1.0.0" | ||||
| 
 | ||||
| react-toggle@^4.1.1: | ||||
|   version "4.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.1.1.tgz#2317f67bf918ea3508a96b09dd383efd9da572af" | ||||
|   integrity sha512-+wXlMcSpg8SmnIXauMaZiKpR+r2wp2gMUteroejp2UTSqGTVvZLN+m9EhMzFARBKEw7KpQOwzCyfzeHeAndQGw== | ||||
| react-toggle@^4.1.2: | ||||
|   version "4.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.1.2.tgz#b00500832f925ad524356d909821821ae39f6c52" | ||||
|   integrity sha512-4Ohw31TuYQdhWfA6qlKafeXx3IOH7t4ZHhmRdwsm1fQREwOBGxJT+I22sgHqR/w8JRdk+AeMCJXPImEFSrNXow== | ||||
|   dependencies: | ||||
|     classnames "^2.2.5" | ||||
| 
 | ||||
|  | @ -10776,6 +10788,11 @@ type-fest@^0.11.0: | |||
|   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" | ||||
|   integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== | ||||
| 
 | ||||
| type-fest@^0.20.2: | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" | ||||
|   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== | ||||
| 
 | ||||
| type-fest@^0.6.0: | ||||
|   version "0.6.0" | ||||
|   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue