Merge pull request #1513 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
This commit is contained in:
Claire 2021-03-19 14:34:08 +01:00 committed by GitHub
commit c7f04961b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 437 additions and 410 deletions

View file

@ -111,7 +111,7 @@ group :development, :test do
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.9' gem 'pry-byebug', '~> 3.9'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 4.1' gem 'rspec-rails', '~> 5.0'
end end
group :production, :test do group :production, :test do
@ -143,7 +143,7 @@ group :development do
gem 'rubocop', '~> 1.11', require: false gem 'rubocop', '~> 1.11', require: false
gem 'rubocop-rails', '~> 2.9', require: false gem 'rubocop-rails', '~> 2.9', require: false
gem 'brakeman', '~> 4.10', 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', '~> 3.16'
gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rails', '~> 1.6'
@ -155,7 +155,6 @@ end
group :production do group :production do
gem 'lograge', '~> 0.11' gem 'lograge', '~> 0.11'
gem 'redis-rails', '~> 5.0'
end end
gem 'concurrent-ruby', require: false gem 'concurrent-ruby', require: false

View file

@ -115,9 +115,9 @@ GEM
bullet (6.1.4) bullet (6.1.4)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundler-audit (0.7.0.1) bundler-audit (0.8.0)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (>= 0.18, < 2) thor (~> 1.0)
byebug (11.1.3) byebug (11.1.3)
capistrano (3.16.0) capistrano (3.16.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
@ -492,24 +492,8 @@ GEM
rdf (~> 3.1) rdf (~> 3.1)
redcarpet (3.5.1) redcarpet (3.5.1)
redis (4.2.5) 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-namespace (1.8.1)
redis (>= 3.0.4) 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) regexp_parser (2.1.1)
request_store (1.5.0) request_store (1.5.0)
rack (>= 1.4) rack (>= 1.4)
@ -531,10 +515,10 @@ GEM
rspec-mocks (3.10.2) rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0) rspec-support (~> 3.10.0)
rspec-rails (4.1.0) rspec-rails (5.0.0)
actionpack (>= 4.2) actionpack (>= 5.2)
activesupport (>= 4.2) activesupport (>= 5.2)
railties (>= 4.2) railties (>= 5.2)
rspec-core (~> 3.10) rspec-core (~> 3.10)
rspec-expectations (~> 3.10) rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10) rspec-mocks (~> 3.10)
@ -706,7 +690,7 @@ DEPENDENCIES
brakeman (~> 4.10) brakeman (~> 4.10)
browser browser
bullet (~> 6.1) bullet (~> 6.1)
bundler-audit (~> 0.7) bundler-audit (~> 0.8)
capistrano (~> 3.16) capistrano (~> 3.16)
capistrano-rails (~> 1.6) capistrano-rails (~> 1.6)
capistrano-rbenv (~> 2.2) capistrano-rbenv (~> 2.2)
@ -792,9 +776,8 @@ DEPENDENCIES
redcarpet (~> 3.5) redcarpet (~> 3.5)
redis (~> 4.2) redis (~> 4.2)
redis-namespace (~> 1.8) redis-namespace (~> 1.8)
redis-rails (~> 5.0)
rqrcode (~> 1.2) rqrcode (~> 1.2)
rspec-rails (~> 4.1) rspec-rails (~> 5.0)
rspec-sidekiq (~> 3.1) rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4) rspec_junit_formatter (~> 0.4)
rubocop (~> 1.11) rubocop (~> 1.11)

View file

@ -22,7 +22,7 @@ module Admin
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save @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 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 render :new
else else
if existing_domain_block.present? if existing_domain_block.present?

View file

@ -32,6 +32,8 @@ module CacheConcern
return raw unless klass.respond_to?(:with_includes) 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) cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys uncached_ids = raw.map(&:id) - cached_keys_with_value.keys

View file

@ -1,6 +1,7 @@
import { Iterable, fromJS } from 'immutable'; import { Iterable, fromJS } from 'immutable';
import { hydrateCompose } from './compose'; import { hydrateCompose } from './compose';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { saveSettings } from './settings';
export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE = 'STORE_HYDRATE';
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
@ -9,9 +10,22 @@ const convertState = rawState =>
fromJS(rawState, (k, v) => fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap()); 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) { export function hydrateStore(rawState) {
return dispatch => { return dispatch => {
const state = convertState(rawState); const state = applyMigrations(convertState(rawState));
dispatch({ dispatch({
type: STORE_HYDRATE, type: STORE_HYDRATE,
@ -20,5 +34,6 @@ export function hydrateStore(rawState) {
dispatch(hydrateCompose()); dispatch(hydrateCompose());
dispatch(importFetchedAccounts(Object.values(rawState.accounts))); dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
dispatch(saveSettings());
}; };
}; };

View file

@ -113,14 +113,6 @@ class LocalSettingsPage extends React.PureComponent {
<FormattedMessage id='settings.notifications.favicon_badge' defaultMessage='Unread notifications favicon badge' /> <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> <span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage="Add a badge for unread notifications to the favicon" /></span>
</LocalSettingsPageItem> </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>
<section> <section>
<h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2> <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>

View file

@ -56,6 +56,16 @@ export default class ColumnSettings extends React.PureComponent {
<ClearColumnButton onClick={onClear} /> <ClearColumnButton onClick={onClear} />
</div> </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'> <div role='group' aria-labelledby='notifications-filter-bar'>
<span id='notifications-filter-bar' className='column-settings__section'> <span id='notifications-filter-bar' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /> <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />

View file

@ -67,8 +67,8 @@ const mapStateToProps = state => ({
hasMore: state.getIn(['notifications', 'hasMore']), hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
lastReadId: state.getIn(['local_settings', 'notifications', 'show_unread']) ? state.getIn(['notifications', 'readMarkerId']) : '0', lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? 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), 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']), needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
}); });

View file

@ -55,7 +55,6 @@ const initialState = ImmutableMap({
notifications : ImmutableMap({ notifications : ImmutableMap({
favicon_badge : false, favicon_badge : false,
tab_badge : true, tab_badge : true,
show_unread : true,
}), }),
}); });

View file

@ -49,6 +49,7 @@ const initialState = ImmutableMap({
}), }),
dismissPermissionBanner: false, dismissPermissionBanner: false,
showUnread: true,
shows: ImmutableMap({ shows: ImmutableMap({
follow: true, follow: true,

View file

@ -55,6 +55,16 @@ export default class ColumnSettings extends React.PureComponent {
<ClearColumnButton onClick={onClear} /> <ClearColumnButton onClick={onClear} />
</div> </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'> <div role='group' aria-labelledby='notifications-filter-bar'>
<span id='notifications-filter-bar' className='column-settings__section'> <span id='notifications-filter-bar' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /> <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />

View file

@ -60,8 +60,8 @@ const mapStateToProps = state => ({
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
hasMore: state.getIn(['notifications', 'hasMore']), hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
lastReadId: state.getIn(['notifications', 'readMarkerId']), lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0',
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), 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']), needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
}); });

View file

@ -45,6 +45,7 @@ const initialState = ImmutableMap({
}), }),
dismissPermissionBanner: false, dismissPermissionBanner: false,
showUnread: true,
shows: ImmutableMap({ shows: ImmutableMap({
follow: true, follow: true,

View file

@ -17,6 +17,8 @@ class EntityCache
def emoji(shortcodes, domain) 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) }) cached = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
uncached_ids = [] uncached_ids = []

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'singleton' require 'singleton'
require_relative './sanitize_config'
class HTMLRenderer < Redcarpet::Render::HTML class HTMLRenderer < Redcarpet::Render::HTML
def block_code(code, language) def block_code(code, language)
@ -223,9 +222,9 @@ class Formatter
original_url, static_url = emoji original_url, static_url = emoji
replacement = begin replacement = begin
if animate 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 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
end end
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : '' before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''

View file

@ -4,7 +4,7 @@ class NotificationMailer < ApplicationMailer
helper :accounts helper :accounts
helper :statuses helper :statuses
add_template_helper RoutingHelper helper RoutingHelper
def mention(recipient, notification) def mention(recipient, notification)
@me = recipient @me = recipient

View file

@ -8,7 +8,7 @@ class UserMailer < Devise::Mailer
helper :instance helper :instance
helper :statuses helper :statuses
add_template_helper RoutingHelper helper RoutingHelper
def confirmation_instructions(user, token, **) def confirmation_instructions(user, token, **)
@resource = user @resource = user

View file

@ -18,46 +18,4 @@ class AccountStat < ApplicationRecord
belongs_to :account, inverse_of: :account_stat belongs_to :account, inverse_of: :account_stat
update_index('accounts#account', :account) 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 end

View file

@ -3,6 +3,8 @@
module AccountCounters module AccountCounters
extend ActiveSupport::Concern extend ActiveSupport::Concern
ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze
included do included do
has_one :account_stat, inverse_of: :account has_one :account_stat, inverse_of: :account
after_save :save_account_stat after_save :save_account_stat
@ -14,11 +16,65 @@ module AccountCounters
:following_count=, :following_count=,
:followers_count, :followers_count,
:followers_count=, :followers_count=,
:increment_count!,
:decrement_count!,
:last_status_at, :last_status_at,
to: :account_stat 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 def account_stat
super || build_account_stat super || build_account_stat
end end

View file

@ -49,12 +49,12 @@ class Notification < ApplicationRecord
belongs_to :from_account, class_name: 'Account', optional: true belongs_to :from_account, class_name: 'Account', optional: true
belongs_to :activity, polymorphic: true, optional: true belongs_to :activity, polymorphic: true, optional: true
belongs_to :mention, foreign_type: 'Mention', foreign_key: 'activity_id', optional: true belongs_to :mention, foreign_key: 'activity_id', optional: true
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', optional: true belongs_to :status, foreign_key: 'activity_id', optional: true
belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id', optional: true belongs_to :follow, foreign_key: 'activity_id', optional: true
belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id', optional: true belongs_to :follow_request, foreign_key: 'activity_id', optional: true
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true belongs_to :favourite, foreign_key: 'activity_id', optional: true
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true belongs_to :poll, foreign_key: 'activity_id', optional: true
validates :type, inclusion: { in: TYPES } validates :type, inclusion: { in: TYPES }

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class UrlValidator < ActiveModel::EachValidator class URLValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value) record.errors.add(attribute, I18n.t('applications.invalid_url')) unless compliant?(value)
end end

View file

@ -6,8 +6,9 @@ require 'rails/all'
# you've limited to :test, :development, or :production. # you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups) Bundler.require(*Rails.groups)
require_relative '../app/lib/exceptions' require_relative '../lib/exceptions'
require_relative '../lib/enumerable' require_relative '../lib/enumerable'
require_relative '../lib/sanitize_ext/sanitize_config'
require_relative '../lib/redis/namespace_extensions' require_relative '../lib/redis/namespace_extensions'
require_relative '../lib/paperclip/url_generator_extensions' require_relative '../lib/paperclip/url_generator_extensions'
require_relative '../lib/paperclip/attachment_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/webpacker/helper_extensions'
require_relative '../lib/action_dispatch/cookie_jar_extensions' require_relative '../lib/action_dispatch/cookie_jar_extensions'
require_relative '../lib/rails/engine_extensions' require_relative '../lib/rails/engine_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
Dotenv::Railtie.load Dotenv::Railtie.load

View file

@ -17,7 +17,7 @@ Rails.application.configure do
if Rails.root.join('tmp/caching-dev.txt').exist? if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true 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 = { config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}", 'Cache-Control' => "public, max-age=#{2.days.to_i}",

View file

@ -52,7 +52,7 @@ Rails.application.configure do
config.log_tags = [:request_id] config.log_tags = [:request_id]
# Use a different cache store in production. # 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. # 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. # Set this to true and configure the email server for immediate delivery to raise delivery errors.

View file

@ -20,6 +20,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'JsonLd' inflect.acronym 'JsonLd'
inflect.acronym 'NodeInfo' inflect.acronym 'NodeInfo'
inflect.acronym 'Ed25519' inflect.acronym 'Ed25519'
inflect.acronym 'TOC'
inflect.acronym 'RSS'
inflect.acronym 'REST'
inflect.acronym 'URL'
inflect.singular 'data', 'data' inflect.singular 'data', 'data'
end end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
namespace = ENV.fetch('REDIS_NAMESPACE') { nil } namespace = ENV.fetch('REDIS_NAMESPACE') { nil }
redis_params = { url: ENV['REDIS_URL'] } redis_params = { url: ENV['REDIS_URL'], driver: :hiredis }
if namespace if namespace
redis_params[:namespace] = namespace redis_params[:namespace] = namespace

View file

@ -1,4 +1,4 @@
class AddUrlToStatuses < ActiveRecord::Migration[4.2] class AddURLToStatuses < ActiveRecord::Migration[4.2]
def change def change
add_column :statuses, :url, :string, null: true, default: nil add_column :statuses, :url, :string, null: true, default: nil
end end

View file

@ -1,4 +1,4 @@
class AddUrlToAccounts < ActiveRecord::Migration[4.2] class AddURLToAccounts < ActiveRecord::Migration[4.2]
def change def change
add_column :accounts, :url, :string, null: true, default: nil add_column :accounts, :url, :string, null: true, default: nil
end end

View file

@ -1,4 +1,4 @@
class AddAvatarRemoteUrlToAccounts < ActiveRecord::Migration[4.2] class AddAvatarRemoteURLToAccounts < ActiveRecord::Migration[4.2]
def change def change
add_column :accounts, :avatar_remote_url, :string, null: true, default: nil add_column :accounts, :avatar_remote_url, :string, null: true, default: nil
end end

View file

@ -7,12 +7,12 @@ end
class RailsSettingsMigration < MIGRATION_BASE_CLASS class RailsSettingsMigration < MIGRATION_BASE_CLASS
def self.up def self.up
create_table :settings do |t| create_table :settings do |t|
t.string :var, :null => false t.string :var, null: false
t.text :value t.text :value
t.references :target, :null => false, :polymorphic => true t.references :target, null: false, polymorphic: true, index: { name: 'index_settings_on_target_type_and_target_id' }
t.timestamps :null => true t.timestamps null: true
end end
add_index :settings, [ :target_type, :target_id, :var ], :unique => true add_index :settings, [ :target_type, :target_id, :var ], unique: true
end end
def self.down def self.down

View file

@ -1,4 +1,4 @@
class AddHeaderRemoteUrlToAccounts < ActiveRecord::Migration[5.0] class AddHeaderRemoteURLToAccounts < ActiveRecord::Migration[5.0]
def change def change
add_column :accounts, :header_remote_url, :string, null: false, default: '' add_column :accounts, :header_remote_url, :string, null: false, default: ''
end end

View file

@ -1,7 +1,7 @@
class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1] class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1]
def up def up
# Prepare the function we will use to generate IDs. # 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. # Set up the statuses.id column to use our timestamp-based IDs.
ActiveRecord::Base.connection.execute(<<~SQL) ActiveRecord::Base.connection.execute(<<~SQL)
@ -11,7 +11,7 @@ class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1]
SQL SQL
# Make sure we have a sequence to use. # Make sure we have a sequence to use.
Rake::Task['db:ensure_id_sequences_exist'].execute Mastodon::Snowflake.ensure_id_sequences_exist
end end
def down def down

View file

@ -3,7 +3,7 @@ class CreateAdminActionLogs < ActiveRecord::Migration[5.1]
create_table :admin_action_logs do |t| create_table :admin_action_logs do |t|
t.belongs_to :account, foreign_key: { on_delete: :cascade } t.belongs_to :account, foreign_key: { on_delete: :cascade }
t.string :action, null: false, default: '' 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.text :recorded_changes, null: false, default: ''
t.timestamps t.timestamps

View file

@ -1,6 +1,6 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers') require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddEmbedUrlToPreviewCards < ActiveRecord::Migration[5.1] class AddEmbedURLToPreviewCards < ActiveRecord::Migration[5.1]
include Mastodon::MigrationHelpers include Mastodon::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!

View file

@ -1,4 +1,4 @@
class AddFeaturedCollectionUrlToAccounts < ActiveRecord::Migration[5.1] class AddFeaturedCollectionURLToAccounts < ActiveRecord::Migration[5.1]
def change def change
add_column :accounts, :featured_collection_url, :string add_column :accounts, :featured_collection_url, :string
end end

View file

@ -37,7 +37,7 @@ class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2]
end end
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| duplicates.each do |row|
deduplicate_account!(row['ids'].split(',')) deduplicate_account!(row['ids'].split(','))

View file

@ -17,8 +17,8 @@ class MigrateAccountConversations < ActiveRecord::Migration[5.2]
belongs_to :account, optional: true belongs_to :account, optional: true
belongs_to :activity, polymorphic: true, optional: true belongs_to :activity, polymorphic: true, optional: true
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', optional: true belongs_to :status, foreign_key: 'activity_id', optional: true
belongs_to :mention, foreign_type: 'Mention', foreign_key: 'activity_id', optional: true belongs_to :mention, foreign_key: 'activity_id', optional: true
def target_status def target_status
mention&.status mention&.status

View file

@ -2,7 +2,7 @@ class DowncaseCustomEmojiDomains < ActiveRecord::Migration[5.2]
disable_ddl_transaction! disable_ddl_transaction!
def up 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| duplicates.each do |row|
CustomEmoji.where(id: row['ids'].split(',')[0...-1]).destroy_all CustomEmoji.where(id: row['ids'].split(',')[0...-1]).destroy_all

View file

@ -2,7 +2,7 @@ class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2]
disable_ddl_transaction! disable_ddl_transaction!
def up 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 canonical_tag_id = row['ids'].split(',').first
redundant_tag_ids = row['ids'].split(',')[1..-1] redundant_tag_ids = row['ids'].split(',')[1..-1]

View file

@ -1,4 +1,4 @@
class AddDevicesUrlToAccounts < ActiveRecord::Migration[5.2] class AddDevicesURLToAccounts < ActiveRecord::Migration[5.2]
def change def change
add_column :accounts, :devices_url, :string add_column :accounts, :devices_url, :string
end end

View file

@ -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

View file

@ -14,7 +14,7 @@ module Mastodon
end end
MIN_SUPPORTED_VERSION = 2019_10_01_213028 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 # Stubs to enjoy ActiveRecord queries while not depending on a particular
# version of the code/database # version of the code/database
@ -142,7 +142,6 @@ module Mastodon
@prompt.warn 'Please make sure to stop Mastodon and have a backup.' @prompt.warn 'Please make sure to stop Mastodon and have a backup.'
exit(1) unless @prompt.yes?('Continue?') exit(1) unless @prompt.yes?('Continue?')
deduplicate_accounts!
deduplicate_users! deduplicate_users!
deduplicate_account_domain_blocks! deduplicate_account_domain_blocks!
deduplicate_account_identity_proofs! deduplicate_account_identity_proofs!
@ -157,6 +156,7 @@ module Mastodon
deduplicate_media_attachments! deduplicate_media_attachments!
deduplicate_preview_cards! deduplicate_preview_cards!
deduplicate_statuses! deduplicate_statuses!
deduplicate_accounts!
deduplicate_tags! deduplicate_tags!
deduplicate_webauthn_credentials! deduplicate_webauthn_credentials!

View file

@ -41,42 +41,18 @@
module Mastodon module Mastodon
module MigrationHelpers 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. # Model that can be used for querying permissions of a SQL user.
class Grant < ActiveRecord::Base class Grant < ActiveRecord::Base
self.table_name = self.table_name = 'information_schema.role_table_grants'
if Mastodon::MigrationHelpers.postgresql?
'information_schema.role_table_grants'
else
'mysql.user'
end
def self.scope_to_current_user def self.scope_to_current_user
if Mastodon::MigrationHelpers.postgresql?
where('grantee = user') where('grantee = user')
else
where("CONCAT(User, '@', Host) = current_user()")
end
end end
# Returns true if the current user can create and execute triggers on the # Returns true if the current user can create and execute triggers on the
# given table. # given table.
def self.create_and_execute_trigger?(table) def self.create_and_execute_trigger?(table)
priv = priv = where(privilege_type: 'TRIGGER', table_name: table)
if Mastodon::MigrationHelpers.postgresql?
where(privilege_type: 'TRIGGER', table_name: table)
else
where(Trigger_priv: 'Y')
end
priv.scope_to_current_user.any? priv.scope_to_current_user.any?
end end
@ -141,10 +117,8 @@ module Mastodon
'in the body of your migration class' 'in the body of your migration class'
end end
if MigrationHelpers.postgresql?
options = options.merge({ algorithm: :concurrently }) options = options.merge({ algorithm: :concurrently })
disable_statement_timeout disable_statement_timeout
end
add_index(table_name, column_name, options) add_index(table_name, column_name, options)
end end
@ -199,8 +173,6 @@ module Mastodon
# Only available on Postgresql >= 9.2 # Only available on Postgresql >= 9.2
def supports_drop_index_concurrently? 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 = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
version >= 90200 version >= 90200
@ -226,13 +198,7 @@ module Mastodon
# While MySQL does allow disabling of foreign keys it has no equivalent # While MySQL does allow disabling of foreign keys it has no equivalent
# of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall
# back to the normal foreign key procedure. # 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 on_delete = 'SET NULL' if on_delete == :nullify
end
disable_statement_timeout disable_statement_timeout
@ -270,7 +236,7 @@ module Mastodon
# the database. Disable the session's statement timeout to ensure # the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only) # migrations don't get killed prematurely. (PostgreSQL only)
def disable_statement_timeout def disable_statement_timeout
execute('SET statement_timeout TO 0') if MigrationHelpers.postgresql? execute('SET statement_timeout TO 0')
end end
# Updates the value of a column in batches. # 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 = table.project(Arel.star.count.as('count'))
count_arel = yield table, count_arel if block_given? 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 return if total == 0
end end
@ -335,7 +301,7 @@ module Mastodon
start_arel = table.project(table[:id]).order(table[:id].asc).take(1) start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
start_arel = yield table, start_arel if block_given? 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: # In case there are no rows but we didn't catch it in the estimated size:
return unless first_row return unless first_row
start_id = first_row['id'].to_i start_id = first_row['id'].to_i
@ -356,7 +322,7 @@ module Mastodon
.skip(batch_size) .skip(batch_size)
stop_arel = yield table, stop_arel if block_given? 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 update_arel = Arel::UpdateManager.new
.table(table) .table(table)
@ -487,11 +453,7 @@ module Mastodon
# If we were in the middle of update_column_in_batches, we should remove # 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. # the old column and start over, as we have no idea where we were.
if column_for(table, new) if column_for(table, new)
if MigrationHelpers.postgresql?
remove_rename_triggers_for_postgresql(table, trigger_name) remove_rename_triggers_for_postgresql(table, trigger_name)
else
remove_rename_triggers_for_mysql(trigger_name)
end
remove_column(table, new) remove_column(table, new)
end end
@ -521,13 +483,8 @@ module Mastodon
quoted_old = quote_column_name(old) quoted_old = quote_column_name(old)
quoted_new = quote_column_name(new) quoted_new = quote_column_name(new)
if MigrationHelpers.postgresql?
install_rename_triggers_for_postgresql(trigger_name, quoted_table, install_rename_triggers_for_postgresql(trigger_name, quoted_table,
quoted_old, quoted_new) quoted_old, quoted_new)
else
install_rename_triggers_for_mysql(trigger_name, quoted_table,
quoted_old, quoted_new)
end
update_column_in_batches(table, new, Arel::Table.new(table)[old]) update_column_in_batches(table, new, Arel::Table.new(table)[old])
@ -685,11 +642,7 @@ module Mastodon
check_trigger_permissions!(table) check_trigger_permissions!(table)
if MigrationHelpers.postgresql?
remove_rename_triggers_for_postgresql(table, trigger_name) remove_rename_triggers_for_postgresql(table, trigger_name)
else
remove_rename_triggers_for_mysql(trigger_name)
end
remove_column(table, old) remove_column(table, old)
end end
@ -844,19 +797,10 @@ module Mastodon
quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s) quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s)
quoted_replacement = Arel::Nodes::Quoted.new(replacement.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 replace = Arel::Nodes::NamedFunction
.new("regexp_replace", [column, quoted_pattern, quoted_replacement]) .new("regexp_replace", [column, quoted_pattern, quoted_replacement])
Arel::Nodes::SqlLiteral.new(replace.to_sql) Arel::Nodes::SqlLiteral.new(replace.to_sql)
end end
end
def remove_foreign_key_without_error(*args) def remove_foreign_key_without_error(*args)
remove_foreign_key(*args) remove_foreign_key(*args)

View file

@ -27,6 +27,8 @@ namespace = ENV.fetch('REDIS_NAMESPACE', nil)
cache_namespace = namespace ? namespace + '_cache' : 'cache' cache_namespace = namespace ? namespace + '_cache' : 'cache'
REDIS_CACHE_PARAMS = { REDIS_CACHE_PARAMS = {
driver: :hiredis,
url: ENV['REDIS_URL'],
expires_in: 10.minutes, expires_in: 10.minutes,
namespace: cache_namespace, namespace: cache_namespace,
}.freeze }.freeze

View file

@ -1,36 +1,5 @@
# frozen_string_literal: true # 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 :db do
namespace :migrate do namespace :migrate do
desc 'Setup the db or migrate depending on state of db' desc 'Setup the db or migrate depending on state of db'
@ -50,7 +19,7 @@ namespace :db do
task :post_migration_hook do task :post_migration_hook do
at_exit 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 warn <<~WARNING
Your database collation is susceptible to index corruption. Your database collation is susceptible to index corruption.
(This warning does not indicate that index corruption has occured and can be ignored) (This warning does not indicate that index corruption has occured and can be ignored)
@ -60,30 +29,11 @@ namespace :db do
end end
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']) 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 end

View file

@ -60,14 +60,14 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.13.8", "@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.13.5", "@babel/plugin-proposal-decorators": "^7.13.5",
"@babel/plugin-transform-react-inline-elements": "^7.12.13", "@babel/plugin-transform-react-inline-elements": "^7.12.13",
"@babel/plugin-transform-runtime": "^7.13.9", "@babel/plugin-transform-runtime": "^7.13.10",
"@babel/preset-env": "^7.13.9", "@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.12.13",
"@babel/runtime": "^7.13.9", "@babel/runtime": "^7.13.10",
"@clusterws/cws": "^3.0.0", "@clusterws/cws": "^3.0.0",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^0.5.7", "@github/webauthn-json": "^0.5.7",
@ -88,7 +88,7 @@
"color-blend": "^3.0.1", "color-blend": "^3.0.1",
"compression-webpack-plugin": "^6.1.1", "compression-webpack-plugin": "^6.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^5.1.1", "css-loader": "^5.1.2",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"detect-passive-events": "^2.0.3", "detect-passive-events": "^2.0.3",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
@ -146,7 +146,7 @@
"react-sparklines": "^1.7.0", "react-sparklines": "^1.7.0",
"react-swipeable-views": "^0.13.9", "react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^8.3.2", "react-textarea-autosize": "^8.3.2",
"react-toggle": "^4.1.1", "react-toggle": "^4.1.2",
"redis": "^3.0.2", "redis": "^3.0.2",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-immutable": "^4.0.0", "redux-immutable": "^4.0.0",
@ -179,7 +179,7 @@
"@testing-library/react": "^11.2.5", "@testing-library/react": "^11.2.5",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"eslint": "^7.21.0", "eslint": "^7.22.0",
"eslint-plugin-import": "~2.22.1", "eslint-plugin-import": "~2.22.1",
"eslint-plugin-jsx-a11y": "~6.4.1", "eslint-plugin-jsx-a11y": "~6.4.1",
"eslint-plugin-promise": "~4.3.1", "eslint-plugin-promise": "~4.3.1",

View file

@ -370,7 +370,7 @@ RSpec.describe AccountsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
@ -402,7 +402,7 @@ RSpec.describe AccountsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns public Cache-Control header' do it 'returns public Cache-Control header' do
@ -428,7 +428,7 @@ RSpec.describe AccountsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
@ -446,7 +446,7 @@ RSpec.describe AccountsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do

View file

@ -43,7 +43,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
@ -88,7 +88,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
@ -116,7 +116,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do
@ -141,7 +141,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do

View file

@ -40,7 +40,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
end end
it 'returns application/activity+json' do 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 end
it 'returns orderedItems with followers from example.com' do it 'returns orderedItems with followers from example.com' do

View file

@ -46,7 +46,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns totalItems' do it 'returns totalItems' do
@ -85,7 +85,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns orderedItems with public or unlisted statuses' do it 'returns orderedItems with public or unlisted statuses' do
@ -133,7 +133,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns orderedItems with public or unlisted statuses' do it 'returns orderedItems with public or unlisted statuses' do
@ -159,7 +159,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns orderedItems with private statuses' do it 'returns orderedItems with private statuses' do
@ -185,7 +185,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns empty orderedItems' do it 'returns empty orderedItems' do
@ -210,7 +210,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it 'returns empty orderedItems' do it 'returns empty orderedItems' do

View file

@ -73,7 +73,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
end end
it 'returns application/activity+json' do 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 end
it_behaves_like 'cachable response' it_behaves_like 'cachable response'

View file

@ -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

View file

@ -22,8 +22,8 @@ describe ApplicationController, type: :controller do
get :index, format: :csv get :index, format: :csv
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type).to eq 'text/csv' expect(response.media_type).to eq 'text/csv'
expect(response.headers['Content-Disposition']).to eq 'attachment; filename="anonymous.csv"' expect(response.headers['Content-Disposition']).to start_with 'attachment; filename="anonymous.csv"'
expect(response.body).to eq user.account.username expect(response.body).to eq user.account.username
end end

View file

@ -8,7 +8,7 @@ describe WellKnown::HostMetaController, type: :controller do
get :show, format: :xml get :show, format: :xml
expect(response).to have_http_status(200) 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 expect(response.body).to eq <<XML
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">

View file

@ -8,7 +8,7 @@ describe WellKnown::KeybaseProofConfigController, type: :controller do
get :show get :show
expect(response).to have_http_status(200) 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 expect { JSON.parse(response.body) }.not_to raise_exception
end end
end end

View file

@ -8,7 +8,7 @@ describe WellKnown::NodeInfoController, type: :controller do
get :index get :index
expect(response).to have_http_status(200) 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 json = body_as_json
@ -23,7 +23,7 @@ describe WellKnown::NodeInfoController, type: :controller do
get :show get :show
expect(response).to have_http_status(200) 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 json = body_as_json

View file

@ -25,7 +25,7 @@ describe WellKnown::WebfingerController, type: :controller do
end end
it 'returns application/jrd+json' do 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 end
it 'returns links for the account' do it 'returns links for the account' do

View file

@ -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

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require Rails.root.join('app', 'lib', 'sanitize_config.rb')
describe Sanitize::Config do describe Sanitize::Config do
shared_examples 'common HTML sanitization' do shared_examples 'common HTML sanitization' do

View file

@ -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

View file

@ -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

View file

@ -6,7 +6,7 @@ describe "The catch all route" do
get "/test" get "/test"
expect(response.status).to eq 404 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 end
@ -15,7 +15,7 @@ describe "The catch all route" do
get "/test.test" get "/test.test"
expect(response.status).to eq 404 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 end
end end

View file

@ -6,7 +6,7 @@ describe "The host_meta route" do
get host_meta_url get host_meta_url
expect(response).to have_http_status(200) 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 end
end end

View file

@ -25,7 +25,7 @@ describe 'Link headers' do
end end
def link_header_with_type(type) 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] } link.attr_pairs.any? { |pair| pair == ['type', type] }
end end
end end

View file

@ -8,7 +8,7 @@ describe 'The webfinger route' do
get webfinger_url(resource: alice.to_webfinger_s) get webfinger_url(resource: alice.to_webfinger_s)
expect(response).to have_http_status(200) 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 end
@ -17,7 +17,7 @@ describe 'The webfinger route' do
get webfinger_url(resource: alice.to_webfinger_s, format: :json) get webfinger_url(resource: alice.to_webfinger_s, format: :json)
expect(response).to have_http_status(200) 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
it 'returns a json response for json accept header' do 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 get webfinger_url(resource: alice.to_webfinger_s), headers: headers
expect(response).to have_http_status(200) 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 end
end end

View file

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe UrlValidator, type: :validator do RSpec.describe URLValidator, type: :validator do
describe '#validate_each' do describe '#validate_each' do
before do before do
allow(validator).to receive(:compliant?).with(value) { compliant } allow(validator).to receive(:compliant?).with(value) { compliant }

131
yarn.lock
View file

@ -21,17 +21,17 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6"
integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== 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": "@babel/core@^7.1.0", "@babel/core@^7.13.10", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
version "7.13.8" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.8.tgz#c191d9c5871788a591d69ea1dc03e5843a3680fb" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559"
integrity sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg== integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
dependencies: dependencies:
"@babel/code-frame" "^7.12.13" "@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.0" "@babel/generator" "^7.13.9"
"@babel/helper-compilation-targets" "^7.13.8" "@babel/helper-compilation-targets" "^7.13.10"
"@babel/helper-module-transforms" "^7.13.0" "@babel/helper-module-transforms" "^7.13.0"
"@babel/helpers" "^7.13.0" "@babel/helpers" "^7.13.10"
"@babel/parser" "^7.13.4" "@babel/parser" "^7.13.10"
"@babel/template" "^7.12.13" "@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
@ -43,10 +43,10 @@
semver "^6.3.0" semver "^6.3.0"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.13.0": "@babel/generator@^7.13.0", "@babel/generator@^7.13.9":
version "7.13.0" version "7.13.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.0.tgz#bd00d4394ca22f220390c56a0b5b85568ec1ec0c" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
integrity sha512-zBZfgvBB/ywjx0Rgc2+BwoH/3H+lDtlgD4hBOpEv5LxRnYsm/753iRuLepqnYlynpjC3AdQxtxsoeHJoEEwOAw== integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
dependencies: dependencies:
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
jsesc "^2.5.1" jsesc "^2.5.1"
@ -82,10 +82,10 @@
"@babel/helper-annotate-as-pure" "^7.12.13" "@babel/helper-annotate-as-pure" "^7.12.13"
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.8": "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8":
version "7.13.8" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz#02bdb22783439afb11b2f009814bdd88384bd468" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c"
integrity sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A== integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==
dependencies: dependencies:
"@babel/compat-data" "^7.13.8" "@babel/compat-data" "^7.13.8"
"@babel/helper-validator-option" "^7.12.17" "@babel/helper-validator-option" "^7.12.17"
@ -274,10 +274,10 @@
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
"@babel/helpers@^7.13.0": "@babel/helpers@^7.13.10":
version "7.13.0" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8"
integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ== integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==
dependencies: dependencies:
"@babel/template" "^7.12.13" "@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.0"
@ -292,10 +292,10 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.4", "@babel/parser@^7.7.0": "@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.4" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409"
integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA== integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ==
"@babel/plugin-proposal-async-generator-functions@^7.13.8": "@babel/plugin-proposal-async-generator-functions@^7.13.8":
version "7.13.8" version "7.13.8"
@ -765,10 +765,10 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13"
"@babel/plugin-transform-runtime@^7.13.9": "@babel/plugin-transform-runtime@^7.13.10":
version "7.13.9" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.9.tgz#744d3103338a0d6c90dee0497558150b490cee07" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1"
integrity sha512-XCxkY/wBI6M6Jj2mlWxkmqbKPweRanszWbF3Tyut+hKh+PHcuIH/rSr/7lmmE7C3WW+HSIm2GT+d5jwmheuB0g== integrity sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.12.13" "@babel/helper-module-imports" "^7.12.13"
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
@ -828,13 +828,13 @@
"@babel/helper-create-regexp-features-plugin" "^7.12.13" "@babel/helper-create-regexp-features-plugin" "^7.12.13"
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13"
"@babel/preset-env@^7.13.9": "@babel/preset-env@^7.13.10":
version "7.13.9" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.9.tgz#3ee5f233316b10d066d7f379c6d1e13a96853654" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252"
integrity sha512-mcsHUlh2rIhViqMG823JpscLMesRt3QbMsv1+jhopXEb3W2wXvQ9QoiOlZI9ZbR3XqPtaFpZwEZKYqGJnGMZTQ== integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ==
dependencies: dependencies:
"@babel/compat-data" "^7.13.8" "@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-plugin-utils" "^7.13.0"
"@babel/helper-validator-option" "^7.12.17" "@babel/helper-validator-option" "^7.12.17"
"@babel/plugin-proposal-async-generator-functions" "^7.13.8" "@babel/plugin-proposal-async-generator-functions" "^7.13.8"
@ -939,10 +939,10 @@
dependencies: dependencies:
regenerator-runtime "^0.12.0" 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": "@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.9" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.9.tgz#97dbe2116e2630c489f22e0656decd60aaa1fcee" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA== integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
dependencies: dependencies:
regenerator-runtime "^0.13.4" 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" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== 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: combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 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: dependencies:
tcomb "^2.5.0" tcomb "^2.5.0"
css-loader@^5.1.1: css-loader@^5.1.2:
version "5.1.1" version "5.1.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.1.tgz#9362d444a0f7c08c148a109596715c904e252879" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.2.tgz#b93dba498ec948b543b49d4fab5017205d4f5c3e"
integrity sha512-5FfhpjwtuRgxqmusDidowqmLlcb+1HgnEDMsi2JhiUrZUcoc+cqw+mUtMIF/+OfeMYaaFCLYp1TaIt9H6I/fKA== integrity sha512-T7vTXHSx0KrVEg/xjcl7G01RcVXpcw4OELwDPvkr7izQNny85A84dK3dqrczuEfBcu7Yg7mdTjJLSTibRUoRZg==
dependencies: dependencies:
camelcase "^6.2.0" camelcase "^6.2.0"
cssesc "^3.0.0" cssesc "^3.0.0"
icss-utils "^5.1.0" icss-utils "^5.1.0"
loader-utils "^2.0.0" loader-utils "^2.0.0"
postcss "^8.2.6" postcss "^8.2.8"
postcss-modules-extract-imports "^3.0.0" postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0" postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0" postcss-modules-scope "^3.0.0"
@ -4336,10 +4341,10 @@ eslint@^2.7.0:
text-table "~0.2.0" text-table "~0.2.0"
user-home "^2.0.0" user-home "^2.0.0"
eslint@^7.21.0: eslint@^7.22.0:
version "7.21.0" version "7.22.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.22.0.tgz#07ecc61052fec63661a2cab6bd507127c07adc6f"
integrity sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg== integrity sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg==
dependencies: dependencies:
"@babel/code-frame" "7.12.11" "@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.0" "@eslint/eslintrc" "^0.4.0"
@ -4358,7 +4363,7 @@ eslint@^7.21.0:
file-entry-cache "^6.0.1" file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1" functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0" glob-parent "^5.0.0"
globals "^12.1.0" globals "^13.6.0"
ignore "^4.0.6" ignore "^4.0.6"
import-fresh "^3.0.0" import-fresh "^3.0.0"
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
@ -4366,7 +4371,7 @@ eslint@^7.21.0:
js-yaml "^3.13.1" js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1" json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1" levn "^0.4.1"
lodash "^4.17.20" lodash "^4.17.21"
minimatch "^3.0.4" minimatch "^3.0.4"
natural-compare "^1.4.0" natural-compare "^1.4.0"
optionator "^0.9.1" optionator "^0.9.1"
@ -5117,6 +5122,13 @@ globals@^12.1.0:
dependencies: dependencies:
type-fest "^0.8.1" 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: globals@^9.2.0:
version "9.18.0" version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" 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" source-map "^0.6.1"
supports-color "^6.1.0" supports-color "^6.1.0"
postcss@^8.2.6: postcss@^8.2.8:
version "8.2.6" version "8.2.8"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
dependencies: dependencies:
colorette "^1.2.1" colorette "^1.2.2"
nanoid "^3.1.20" nanoid "^3.1.20"
source-map "^0.6.1" source-map "^0.6.1"
@ -8998,10 +9010,10 @@ react-textarea-autosize@^8.3.2:
use-composed-ref "^1.0.0" use-composed-ref "^1.0.0"
use-latest "^1.0.0" use-latest "^1.0.0"
react-toggle@^4.1.1: react-toggle@^4.1.2:
version "4.1.1" version "4.1.2"
resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.1.1.tgz#2317f67bf918ea3508a96b09dd383efd9da572af" resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.1.2.tgz#b00500832f925ad524356d909821821ae39f6c52"
integrity sha512-+wXlMcSpg8SmnIXauMaZiKpR+r2wp2gMUteroejp2UTSqGTVvZLN+m9EhMzFARBKEw7KpQOwzCyfzeHeAndQGw== integrity sha512-4Ohw31TuYQdhWfA6qlKafeXx3IOH7t4ZHhmRdwsm1fQREwOBGxJT+I22sgHqR/w8JRdk+AeMCJXPImEFSrNXow==
dependencies: dependencies:
classnames "^2.2.5" 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" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== 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: type-fest@^0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"