~cytrogen/masto-fe

aa57f7e3e20ac66fa99ce20dab8cfccfce7c3e66 — Claire 2 years ago 9af04d5 + 5fae2de
Merge commit '5fae2de454806730742b7be7435ae1c4fb97cf3c' into glitch-soc/merge-upstream
M Gemfile => Gemfile +2 -2
@@ 5,7 5,7 @@ ruby '>= 3.0.0'

gem 'pkg-config', '~> 1.5'

gem 'puma', '~> 6.2'
gem 'puma', '~> 6.3'
gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'


@@ 17,7 17,7 @@ gem 'makara', '~> 0.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'

gem 'aws-sdk-s3', '~> 1.122', require: false
gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'

M Gemfile.lock => Gemfile.lock +14 -13
@@ 109,17 109,17 @@ GEM
    attr_required (1.0.1)
    awrence (1.2.1)
    aws-eventstream (1.2.0)
    aws-partitions (1.761.0)
    aws-sdk-core (3.172.0)
    aws-partitions (1.772.0)
    aws-sdk-core (3.174.0)
      aws-eventstream (~> 1, >= 1.0.2)
      aws-partitions (~> 1, >= 1.651.0)
      aws-sigv4 (~> 1.5)
      jmespath (~> 1, >= 1.6.1)
    aws-sdk-kms (1.64.0)
      aws-sdk-core (~> 3, >= 3.165.0)
    aws-sdk-kms (1.65.0)
      aws-sdk-core (~> 3, >= 3.174.0)
      aws-sigv4 (~> 1.1)
    aws-sdk-s3 (1.122.0)
      aws-sdk-core (~> 3, >= 3.165.0)
    aws-sdk-s3 (1.123.0)
      aws-sdk-core (~> 3, >= 3.174.0)
      aws-sdk-kms (~> 1)
      aws-sigv4 (~> 1.4)
    aws-sigv4 (1.5.2)


@@ 501,7 501,7 @@ GEM
      premailer (~> 1.7, >= 1.7.9)
    private_address_check (0.5.0)
    public_suffix (5.0.1)
    puma (6.2.2)
    puma (6.3.0)
      nio4r (~> 2.0)
    pundit (2.3.0)
      activesupport (>= 3.0.0)


@@ 544,8 544,9 @@ GEM
    rails-dom-testing (2.0.3)
      activesupport (>= 4.2.0)
      nokogiri (>= 1.6)
    rails-html-sanitizer (1.5.0)
      loofah (~> 2.19, >= 2.19.1)
    rails-html-sanitizer (1.6.0)
      loofah (~> 2.21)
      nokogiri (~> 1.14)
    rails-i18n (6.0.0)
      i18n (>= 0.7, < 2)
      railties (>= 6.0.0, < 7)


@@ 588,7 589,7 @@ GEM
    rspec-mocks (3.12.5)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.12.0)
    rspec-rails (6.0.2)
    rspec-rails (6.0.3)
      actionpack (>= 6.1)
      activesupport (>= 6.1)
      railties (>= 6.1)


@@ 648,7 649,7 @@ GEM
      redis (>= 4.5.0, < 5)
    sidekiq-bulk (0.2.0)
      sidekiq
    sidekiq-scheduler (5.0.2)
    sidekiq-scheduler (5.0.3)
      rufus-scheduler (~> 3.2)
      sidekiq (>= 6, < 8)
      tilt (>= 1.4.0)


@@ 770,7 771,7 @@ DEPENDENCIES
  active_model_serializers (~> 0.10)
  addressable (~> 2.8)
  annotate (~> 3.2)
  aws-sdk-s3 (~> 1.122)
  aws-sdk-s3 (~> 1.123)
  better_errors (~> 2.9)
  binding_of_caller (~> 1.0)
  blurhash (~> 0.1)


@@ 846,7 847,7 @@ DEPENDENCIES
  premailer-rails
  private_address_check (~> 0.5)
  public_suffix (~> 5.0)
  puma (~> 6.2)
  puma (~> 6.3)
  pundit (~> 2.3)
  rack (~> 2.2.7)
  rack-attack (~> 6.6)

M app/controllers/settings/imports_controller.rb => app/controllers/settings/imports_controller.rb +4 -0
@@ 12,6 12,7 @@ class Settings::ImportsController < Settings::BaseController
    muting: 'muted_accounts_failures.csv',
    domain_blocking: 'blocked_domains_failures.csv',
    bookmarks: 'bookmarks_failures.csv',
    lists: 'lists_failures.csv',
  }.freeze

  TYPE_TO_HEADERS_MAP = {


@@ 20,6 21,7 @@ class Settings::ImportsController < Settings::BaseController
    muting: ['Account address', 'Hide notifications'],
    domain_blocking: false,
    bookmarks: false,
    lists: false,
  }.freeze

  def index


@@ 49,6 51,8 @@ class Settings::ImportsController < Settings::BaseController
              csv << [row.data['domain']]
            when :bookmarks
              csv << [row.data['uri']]
            when :lists
              csv << [row.data['list_name'], row.data['acct']]
            end
          end
        end

M app/helpers/settings_helper.rb => app/helpers/settings_helper.rb +0 -4
@@ 5,10 5,6 @@ module SettingsHelper
    LanguagesHelper::SUPPORTED_LOCALES.keys
  end

  def hash_to_object(hash)
    HashObject.new(hash)
  end

  def session_device_icon(session)
    device = session.detection.device


M app/javascript/mastodon/components/account.jsx => app/javascript/mastodon/components/account.jsx +7 -3
@@ 143,7 143,7 @@ class Account extends ImmutablePureComponent {
    const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));

    if (firstVerifiedField) {
      verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} /></>;
      verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
    }

    return (


@@ 154,9 154,13 @@ class Account extends ImmutablePureComponent {
              <Avatar account={account} size={size} />
            </div>

            <div>
            <div className='account__contents'>
              <DisplayName account={account} />
              {!minimal && <><ShortNumber value={account.get('followers_count')} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}</>}
              {!minimal && (
                <div className='account__details'>
                  <ShortNumber value={account.get('followers_count')} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}
                </div>
              )}
            </div>
          </Link>


M app/javascript/mastodon/components/poll.jsx => app/javascript/mastodon/components/poll.jsx +4 -4
@@ 57,9 57,9 @@ class Poll extends ImmutablePureComponent {
  };

  static getDerivedStateFromProps (props, state) {
    const { poll, intl } = props;
    const { poll } = props;
    const expires_at = poll.get('expires_at');
    const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
    const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
    return (expired === state.expired) ? null : { expired };
  }



@@ 76,10 76,10 @@ class Poll extends ImmutablePureComponent {
  }

  _setupTimer () {
    const { poll, intl } = this.props;
    const { poll } = this.props;
    clearTimeout(this._timer);
    if (!this.state.expired) {
      const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
      const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
      this._timer = setTimeout(() => {
        this.setState({ expired: true });
      }, delay);

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +16 -1
@@ 7814,13 7814,28 @@ noscript {
  }
}

.account__contents {
  overflow: hidden;
}

.account__details {
  display: flex;
  flex-wrap: wrap;
  column-gap: 1em;
}

.verified-badge {
  display: inline-flex;
  align-items: center;
  color: $valid-value-color;
  gap: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  > span {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  a {
    color: inherit;

D app/lib/hash_object.rb => app/lib/hash_object.rb +0 -10
@@ 1,10 0,0 @@
# frozen_string_literal: true

class HashObject
  def initialize(hash)
    hash.each do |k, v|
      instance_variable_set("@#{k}", v)
      self.class.send(:define_method, k, proc { instance_variable_get("@#{k}") })
    end
  end
end

M app/models/bulk_import.rb => app/models/bulk_import.rb +1 -0
@@ 30,6 30,7 @@ class BulkImport < ApplicationRecord
    muting: 2,
    domain_blocking: 3,
    bookmarks: 4,
    lists: 5,
  }

  enum state: {

M app/models/form/import.rb => app/models/form/import.rb +12 -7
@@ 18,6 18,7 @@ class Form::Import
    muting: ['Account address', 'Hide notifications'],
    domain_blocking: ['#domain'],
    bookmarks: ['#uri'],
    lists: ['List name', 'Account address'],
  }.freeze

  KNOWN_FIRST_HEADERS = EXPECTED_HEADERS_BY_TYPE.values.map(&:first).uniq.freeze


@@ 30,6 31,7 @@ class Form::Import
    'Hide notifications' => 'hide_notifications',
    '#domain' => 'domain',
    '#uri' => 'uri',
    'List name' => 'list_name',
  }.freeze

  class EmptyFileError < StandardError; end


@@ 48,6 50,7 @@ class Form::Import
    return :muting if data.original_filename&.start_with?('mutes') || data.original_filename&.start_with?('muted_accounts')
    return :domain_blocking if data.original_filename&.start_with?('domain_blocks') || data.original_filename&.start_with?('blocked_domains')
    return :bookmarks if data.original_filename&.start_with?('bookmarks')
    return :lists if data.original_filename&.start_with?('lists')
  end

  # Whether the uploaded CSV file seems to correspond to a different import type than the one selected


@@ 76,14 79,16 @@ class Form::Import

  private

  def default_csv_header
  def default_csv_headers
    case type.to_sym
    when :following, :blocking, :muting
      'Account address'
      ['Account address']
    when :domain_blocking
      '#domain'
      ['#domain']
    when :bookmarks
      '#uri'
      ['#uri']
    when :lists
      ['List name', 'Account address']
    end
  end



@@ 98,7 103,7 @@ class Form::Import
        field&.split(',')&.map(&:strip)&.presence
      when 'Account address'
        field.strip.gsub(/\A@/, '')
      when '#domain', '#uri'
      when '#domain', '#uri', 'List name'
        field.strip
      else
        field


@@ 109,7 114,7 @@ class Form::Import
    @csv_data.take(1) # Ensure the headers are read
    raise EmptyFileError if @csv_data.headers == true

    @csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: [default_csv_header], converters: csv_converter) unless KNOWN_FIRST_HEADERS.include?(@csv_data.headers&.first)
    @csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: default_csv_headers, converters: csv_converter) unless KNOWN_FIRST_HEADERS.include?(@csv_data.headers&.first)
    @csv_data
  end



@@ 133,7 138,7 @@ class Form::Import
  def validate_data
    return if data.nil?
    return errors.add(:data, I18n.t('imports.errors.too_large')) if data.size > FILE_SIZE_LIMIT
    return errors.add(:data, I18n.t('imports.errors.incompatible_type')) unless csv_data.headers.include?(default_csv_header)
    return errors.add(:data, I18n.t('imports.errors.incompatible_type')) unless default_csv_headers.all? { |header| csv_data.headers.include?(header) }

    errors.add(:data, I18n.t('imports.errors.over_rows_processing_limit', count: ROWS_PROCESSING_LIMIT)) if csv_row_count > ROWS_PROCESSING_LIMIT


M app/services/bulk_import_row_service.rb => app/services/bulk_import_row_service.rb +7 -1
@@ 7,7 7,7 @@ class BulkImportRowService
    @type    = row.bulk_import.type.to_sym

    case @type
    when :following, :blocking, :muting
    when :following, :blocking, :muting, :lists
      target_acct     = @data['acct']
      target_domain   = domain(target_acct)
      @target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) }


@@ 33,6 33,12 @@ class BulkImportRowService
      return false unless StatusPolicy.new(@account, @target_status).show?

      @account.bookmarks.find_or_create_by!(status: @target_status)
    when :lists
      list = @account.owned_lists.find_or_create_by!(title: @data['list_name'])

      FollowService.new.call(@account, @target_account) unless @account.id == @target_account.id

      list.accounts << @target_account
    end

    true

M app/services/bulk_import_service.rb => app/services/bulk_import_service.rb +22 -0
@@ 16,6 16,8 @@ class BulkImportService < BaseService
      import_domain_blocks!
    when :bookmarks
      import_bookmarks!
    when :lists
      import_lists!
    end

    @import.update!(state: :finished, finished_at: Time.now.utc) if @import.processed_items == @import.total_items


@@ 157,4 159,24 @@ class BulkImportService < BaseService
      [row.id]
    end
  end

  def import_lists!
    rows = @import.rows.to_a

    if @import.overwrite?
      included_lists = rows.map { |row| row.data['list_name'] }.uniq

      @account.owned_lists.where.not(title: included_lists).destroy_all

      # As list membership changes do not retroactively change timeline
      # contents, simplify things by just clearing everything
      @account.owned_lists.find_each do |list|
        list.list_accounts.destroy_all
      end
    end

    Import::RowWorker.push_bulk(rows) do |row|
      [row.id]
    end
  end
end

M app/views/settings/imports/index.html.haml => app/views/settings/imports/index.html.haml +1 -1
@@ 3,7 3,7 @@

= simple_form_for @import, url: settings_imports_path do |f|
  .field-group
    = f.input :type, as: :grouped_select, collection: { constructive: %i(following bookmarks), destructive: %i(muting blocking domain_blocking) }, wrapper: :with_block_label, include_blank: false, label_method: ->(type) { I18n.t("imports.types.#{type}") }, group_label_method: ->(group) { I18n.t("imports.type_groups.#{group.first}") }, group_method: :last, hint: t('imports.preface')
    = f.input :type, as: :grouped_select, collection: { constructive: %i(following bookmarks lists), destructive: %i(muting blocking domain_blocking) }, wrapper: :with_block_label, include_blank: false, label_method: ->(type) { I18n.t("imports.types.#{type}") }, group_label_method: ->(group) { I18n.t("imports.type_groups.#{group.first}") }, group_method: :last, hint: t('imports.preface')

  .fields-row
    .fields-group.fields-row__column.fields-row__column-6

M config/initializers/twitter_regex.rb => config/initializers/twitter_regex.rb +1 -1
@@ 25,7 25,7 @@ module Twitter::TwitterText
      \)
    /iox
    UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
    REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@#{UCHARS}]/iou
    REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@\^#{UCHARS}]/iou
    REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
    REGEXEN[:valid_url_path] = /(?:
      (?:

A db/migrate/20230531153942_add_primary_key_to_accounts_tags_join_table.rb => db/migrate/20230531153942_add_primary_key_to_accounts_tags_join_table.rb +24 -0
@@ 0,0 1,24 @@
# frozen_string_literal: true

class AddPrimaryKeyToAccountsTagsJoinTable < ActiveRecord::Migration[6.1]
  disable_ddl_transaction!

  def up
    ActiveRecord::Base.transaction do
      safety_assured do
        execute 'ALTER TABLE accounts_tags ADD PRIMARY KEY USING INDEX index_accounts_tags_on_tag_id_and_account_id'

        # Rename for consistency as the primary key's name is not represented in db/schema.rb
        execute 'ALTER INDEX index_accounts_tags_on_tag_id_and_account_id RENAME TO accounts_tags_pkey'
      end
    end
  end

  def down
    safety_assured do
      # I have found no way to demote the primary key to an index, instead, re-create the index
      execute 'CREATE UNIQUE INDEX CONCURRENTLY index_accounts_tags_on_tag_id_and_account_id ON accounts_tags (tag_id, account_id)'
      execute 'ALTER TABLE accounts_tags DROP CONSTRAINT accounts_tags_pkey'
    end
  end
end

A db/migrate/20230531154811_add_primary_key_to_statuses_tags_join_table.rb => db/migrate/20230531154811_add_primary_key_to_statuses_tags_join_table.rb +24 -0
@@ 0,0 1,24 @@
# frozen_string_literal: true

class AddPrimaryKeyToStatusesTagsJoinTable < ActiveRecord::Migration[6.1]
  disable_ddl_transaction!

  def up
    ActiveRecord::Base.transaction do
      safety_assured do
        execute 'ALTER TABLE statuses_tags ADD PRIMARY KEY USING INDEX index_statuses_tags_on_tag_id_and_status_id'

        # Rename for consistency as the primary key's name is not represented in db/schema.rb
        execute 'ALTER INDEX index_statuses_tags_on_tag_id_and_status_id RENAME TO statuses_tags_pkey'
      end
    end
  end

  def down
    safety_assured do
      # I have found no way to demote the primary key to an index, instead, re-create the index
      execute 'CREATE UNIQUE INDEX CONCURRENTLY index_statuses_tags_on_tag_id_and_status_id ON statuses_tags (tag_id, status_id)'
      execute 'ALTER TABLE statuses_tags DROP CONSTRAINT statuses_tags_pkey'
    end
  end
end

M db/schema.rb => db/schema.rb +3 -5
@@ 10,7 10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2023_05_24_194155) do
ActiveRecord::Schema.define(version: 2023_05_31_154811) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"


@@ 194,11 194,10 @@ ActiveRecord::Schema.define(version: 2023_05_24_194155) do
    t.index ["url"], name: "index_accounts_on_url", opclass: :text_pattern_ops, where: "(url IS NOT NULL)"
  end

  create_table "accounts_tags", id: false, force: :cascade do |t|
  create_table "accounts_tags", primary_key: ["tag_id", "account_id"], force: :cascade do |t|
    t.bigint "account_id", null: false
    t.bigint "tag_id", null: false
    t.index ["account_id", "tag_id"], name: "index_accounts_tags_on_account_id_and_tag_id"
    t.index ["tag_id", "account_id"], name: "index_accounts_tags_on_tag_id_and_account_id", unique: true
  end

  create_table "admin_action_logs", force: :cascade do |t|


@@ 982,11 981,10 @@ ActiveRecord::Schema.define(version: 2023_05_24_194155) do
    t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
  end

  create_table "statuses_tags", id: false, force: :cascade do |t|
  create_table "statuses_tags", primary_key: ["tag_id", "status_id"], force: :cascade do |t|
    t.bigint "status_id", null: false
    t.bigint "tag_id", null: false
    t.index ["status_id"], name: "index_statuses_tags_on_status_id"
    t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true
  end

  create_table "system_keys", force: :cascade do |t|

M lib/mastodon/cli/base.rb => lib/mastodon/cli/base.rb +25 -2
@@ 4,16 4,39 @@ require_relative '../../../config/boot'
require_relative '../../../config/environment'

require 'thor'
require_relative 'helper'
require_relative 'progress_helper'

module Mastodon
  module CLI
    class Base < Thor
      include CLI::Helper
      include ProgressHelper

      def self.exit_on_failure?
        true
      end

      private

      def pastel
        @pastel ||= Pastel.new
      end

      def dry_run?
        options[:dry_run]
      end

      def dry_run_mode_suffix
        dry_run? ? ' (DRY RUN)' : ''
      end

      def reset_connection_pools!
        ActiveRecord::Base.establish_connection(
          ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first.configuration_hash
            .dup
            .tap { |config| config['pool'] = options[:concurrency] + 1 }
        )
        RedisConfiguration.establish_pool(options[:concurrency])
      end
    end
  end
end

R lib/mastodon/cli/helper.rb => lib/mastodon/cli/progress_helper.rb +14 -16
@@ 9,23 9,19 @@ HttpLog.configuration.logger = dev_null
Paperclip.options[:log]      = false
Chewy.logger                 = dev_null

module Mastodon::CLI
  module Helper
    def dry_run?
      options[:dry_run]
    end
require 'ruby-progressbar/outputs/null'

    def dry_run_mode_suffix
      dry_run? ? ' (DRY RUN)' : ''
    end
module Mastodon::CLI
  module ProgressHelper
    PROGRESS_FORMAT = '%c/%u |%b%i| %e'

    def create_progress_bar(total = nil)
      ProgressBar.create(total: total, format: '%c/%u |%b%i| %e')
    end

    def reset_connection_pools!
      ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env].dup.tap { |config| config['pool'] = options[:concurrency] + 1 })
      RedisConfiguration.establish_pool(options[:concurrency])
      ProgressBar.create(
        {
          total: total,
          format: PROGRESS_FORMAT,
        }.merge(progress_output_options)
      )
    end

    def parallelize_with_progress(scope)


@@ 82,8 78,10 @@ module Mastodon::CLI
      [total.value, aggregate.value]
    end

    def pastel
      @pastel ||= Pastel.new
    private

    def progress_output_options
      Rails.env.test? ? { output: ProgressBar::Outputs::Null } : {}
    end
  end
end

M lib/mastodon/cli/search.rb => lib/mastodon/cli/search.rb +22 -9
@@ 29,15 29,7 @@ module Mastodon::CLI
      database will be imported into the indices, unless overridden with --no-import.
    LONG_DESC
    def deploy
      if options[:concurrency] < 1
        say('Cannot run with this concurrency setting, must be at least 1', :red)
        exit(1)
      end

      if options[:batch_size] < 1
        say('Cannot run with this batch_size setting, must be at least 1', :red)
        exit(1)
      end
      verify_deploy_options!

      indices = if options[:only]
                  options[:only].map { |str| "#{str.camelize}Index".constantize }


@@ 98,5 90,26 @@ module Mastodon::CLI

      say("Indexed #{added} records, de-indexed #{removed}", :green, true)
    end

    private

    def verify_deploy_options!
      verify_deploy_concurrency!
      verify_deploy_batch_size!
    end

    def verify_deploy_concurrency!
      return unless options[:concurrency] < 1

      say('Cannot run with this concurrency setting, must be at least 1', :red)
      exit(1)
    end

    def verify_deploy_batch_size!
      return unless options[:batch_size] < 1

      say('Cannot run with this batch_size setting, must be at least 1', :red)
      exit(1)
    end
  end
end

M package.json => package.json +8 -8
@@ 140,12 140,12 @@
    "webpack-cli": "^3.3.12",
    "webpack-merge": "^5.9.0",
    "wicg-inert": "^3.1.2",
    "workbox-expiration": "^6.6.0",
    "workbox-precaching": "^6.6.0",
    "workbox-routing": "^6.6.0",
    "workbox-strategies": "^6.6.0",
    "workbox-webpack-plugin": "^6.6.0",
    "workbox-window": "^6.6.0",
    "workbox-expiration": "^7.0.0",
    "workbox-precaching": "^7.0.0",
    "workbox-routing": "^7.0.0",
    "workbox-strategies": "^7.0.0",
    "workbox-webpack-plugin": "^7.0.0",
    "workbox-window": "^7.0.0",
    "ws": "^8.12.1"
  },
  "devDependencies": {


@@ 158,7 158,7 @@
    "@types/express": "^4.17.17",
    "@types/http-link-header": "^1.0.3",
    "@types/intl": "^1.2.0",
    "@types/jest": "^29.5.1",
    "@types/jest": "^29.5.2",
    "@types/js-yaml": "^4.0.5",
    "@types/lodash": "^4.14.195",
    "@types/npmlog": "^4.1.4",


@@ 192,7 192,7 @@
    "eslint-import-resolver-typescript": "^3.5.5",
    "eslint-plugin-formatjs": "^4.10.1",
    "eslint-plugin-import": "~2.27.5",
    "eslint-plugin-jsdoc": "^45.0.0",
    "eslint-plugin-jsdoc": "^46.1.0",
    "eslint-plugin-jsx-a11y": "~6.7.1",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-promise": "~6.1.1",

A spec/fixtures/files/lists.csv => spec/fixtures/files/lists.csv +3 -0
@@ 0,0 1,3 @@
Mastodon project,gargron@example.com
Mastodon project,mastodon@example.com
test,foo@example.com

D spec/lib/hash_object_spec.rb => spec/lib/hash_object_spec.rb +0 -9
@@ 1,9 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

describe HashObject do
  it 'has methods corresponding to hash properties' do
    expect(HashObject.new(key: 'value').key).to eq 'value'
  end
end

M spec/lib/mastodon/cli/accounts_spec.rb => spec/lib/mastodon/cli/accounts_spec.rb +336 -0
@@ 662,4 662,340 @@ describe Mastodon::CLI::Accounts do
      end
    end
  end

  describe '#refresh' do
    context 'with --all option' do
      let!(:local_account)              { Fabricate(:account, domain: nil) }
      let!(:remote_account_example_com) { Fabricate(:account, domain: 'example.com') }
      let!(:account_example_net)        { Fabricate(:account, domain: 'example.net') }
      let(:scope)                       { Account.remote }

      before do
        allow(cli).to receive(:parallelize_with_progress).and_yield(remote_account_example_com)
                                                         .and_yield(account_example_net)
                                                         .and_return([2, nil])
        cli.options = { all: true }
      end

      it 'refreshes the avatar for all remote accounts' do
        allow(remote_account_example_com).to receive(:reset_avatar!)
        allow(account_example_net).to receive(:reset_avatar!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(remote_account_example_com).to have_received(:reset_avatar!).once
        expect(account_example_net).to have_received(:reset_avatar!).once
      end

      it 'does not refresh avatar for local accounts' do
        allow(local_account).to receive(:reset_avatar!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(local_account).to_not have_received(:reset_avatar!)
      end

      it 'refreshes the header for all remote accounts' do
        allow(remote_account_example_com).to receive(:reset_header!)
        allow(account_example_net).to receive(:reset_header!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(remote_account_example_com).to have_received(:reset_header!).once
        expect(account_example_net).to have_received(:reset_header!).once
      end

      it 'does not refresh the header for local accounts' do
        allow(local_account).to receive(:reset_header!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(local_account).to_not have_received(:reset_header!)
      end

      it 'displays a successful message' do
        expect { cli.refresh }.to output(
          a_string_including('Refreshed 2 accounts')
        ).to_stdout
      end

      context 'with --dry-run option' do
        before do
          cli.options = { all: true, dry_run: true }
        end

        it 'does not refresh the avatar for any account' do
          allow(local_account).to receive(:reset_avatar!)
          allow(remote_account_example_com).to receive(:reset_avatar!)
          allow(account_example_net).to receive(:reset_avatar!)

          cli.refresh

          expect(cli).to have_received(:parallelize_with_progress).with(scope).once
          expect(local_account).to_not have_received(:reset_avatar!)
          expect(remote_account_example_com).to_not have_received(:reset_avatar!)
          expect(account_example_net).to_not have_received(:reset_avatar!)
        end

        it 'does not refresh the header for any account' do
          allow(local_account).to receive(:reset_header!)
          allow(remote_account_example_com).to receive(:reset_header!)
          allow(account_example_net).to receive(:reset_header!)

          cli.refresh

          expect(cli).to have_received(:parallelize_with_progress).with(scope).once
          expect(local_account).to_not have_received(:reset_header!)
          expect(remote_account_example_com).to_not have_received(:reset_header!)
          expect(account_example_net).to_not have_received(:reset_header!)
        end

        it 'displays a successful message with (DRY RUN)' do
          expect { cli.refresh }.to output(
            a_string_including('Refreshed 2 accounts (DRY RUN)')
          ).to_stdout
        end
      end
    end

    context 'with a list of accts' do
      let!(:account_example_com_a) { Fabricate(:account, domain: 'example.com') }
      let!(:account_example_com_b) { Fabricate(:account, domain: 'example.com') }
      let!(:account_example_net)   { Fabricate(:account, domain: 'example.net') }
      let(:arguments)              { [account_example_com_a.acct, account_example_com_b.acct] }

      before do
        allow(Account).to receive(:find_remote).with(account_example_com_a.username, account_example_com_a.domain).and_return(account_example_com_a)
        allow(Account).to receive(:find_remote).with(account_example_com_b.username, account_example_com_b.domain).and_return(account_example_com_b)
        allow(Account).to receive(:find_remote).with(account_example_net.username, account_example_net.domain).and_return(account_example_net)
      end

      it 'resets the avatar for the specified accounts' do
        allow(account_example_com_a).to receive(:reset_avatar!)
        allow(account_example_com_b).to receive(:reset_avatar!)

        cli.refresh(*arguments)

        expect(account_example_com_a).to have_received(:reset_avatar!).once
        expect(account_example_com_b).to have_received(:reset_avatar!).once
      end

      it 'does not reset the avatar for unspecified accounts' do
        allow(account_example_net).to receive(:reset_avatar!)

        cli.refresh(*arguments)

        expect(account_example_net).to_not have_received(:reset_avatar!)
      end

      it 'resets the header for the specified accounts' do
        allow(account_example_com_a).to receive(:reset_header!)
        allow(account_example_com_b).to receive(:reset_header!)

        cli.refresh(*arguments)

        expect(account_example_com_a).to have_received(:reset_header!).once
        expect(account_example_com_b).to have_received(:reset_header!).once
      end

      it 'does not reset the header for unspecified accounts' do
        allow(account_example_net).to receive(:reset_header!)

        cli.refresh(*arguments)

        expect(account_example_net).to_not have_received(:reset_header!)
      end

      context 'when an UnexpectedResponseError is raised' do
        it 'displays a failure message' do
          allow(account_example_com_a).to receive(:reset_avatar!).and_raise(Mastodon::UnexpectedResponseError)

          expect { cli.refresh(*arguments) }
            .to output(
              a_string_including("Account failed: #{account_example_com_a.username}@#{account_example_com_a.domain}")
            ).to_stdout
        end
      end

      context 'when a specified account is not found' do
        it 'exits with an error message' do
          allow(Account).to receive(:find_remote).with(account_example_com_b.username, account_example_com_b.domain).and_return(nil)

          expect { cli.refresh(*arguments) }.to output(
            a_string_including('No such account')
          ).to_stdout
            .and raise_error(SystemExit)
        end
      end

      context 'with --dry-run option' do
        before do
          cli.options = { dry_run: true }
        end

        it 'does not refresh the avatar for any account' do
          allow(account_example_com_a).to receive(:reset_avatar!)
          allow(account_example_com_b).to receive(:reset_avatar!)

          cli.refresh(*arguments)

          expect(account_example_com_a).to_not have_received(:reset_avatar!)
          expect(account_example_com_b).to_not have_received(:reset_avatar!)
        end

        it 'does not refresh the header for any account' do
          allow(account_example_com_a).to receive(:reset_header!)
          allow(account_example_com_b).to receive(:reset_header!)

          cli.refresh(*arguments)

          expect(account_example_com_a).to_not have_received(:reset_header!)
          expect(account_example_com_b).to_not have_received(:reset_header!)
        end
      end
    end

    context 'with --domain option' do
      let!(:account_example_com_a) { Fabricate(:account, domain: 'example.com') }
      let!(:account_example_com_b) { Fabricate(:account, domain: 'example.com') }
      let!(:account_example_net)   { Fabricate(:account, domain: 'example.net') }
      let(:domain)                 { 'example.com' }
      let(:scope)                  { Account.remote.where(domain: domain) }

      before do
        allow(cli).to receive(:parallelize_with_progress).and_yield(account_example_com_a)
                                                         .and_yield(account_example_com_b)
                                                         .and_return([2, nil])

        cli.options = { domain: domain }
      end

      it 'refreshes the avatar for all accounts on specified domain' do
        allow(account_example_com_a).to receive(:reset_avatar!)
        allow(account_example_com_b).to receive(:reset_avatar!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(account_example_com_a).to have_received(:reset_avatar!).once
        expect(account_example_com_b).to have_received(:reset_avatar!).once
      end

      it 'does not refresh the avatar for accounts outside specified domain' do
        allow(account_example_net).to receive(:reset_avatar!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(account_example_net).to_not have_received(:reset_avatar!)
      end

      it 'refreshes the header for all accounts on specified domain' do
        allow(account_example_com_a).to receive(:reset_header!)
        allow(account_example_com_b).to receive(:reset_header!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope)
        expect(account_example_com_a).to have_received(:reset_header!).once
        expect(account_example_com_b).to have_received(:reset_header!).once
      end

      it 'does not refresh the header for accounts outside specified domain' do
        allow(account_example_net).to receive(:reset_header!)

        cli.refresh

        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
        expect(account_example_net).to_not have_received(:reset_header!)
      end
    end

    context 'when neither a list of accts nor options are provided' do
      it 'exits with an error message' do
        expect { cli.refresh }.to output(
          a_string_including('No account(s) given')
        ).to_stdout
          .and raise_error(SystemExit)
      end
    end
  end

  describe '#rotate' do
    context 'when neither username nor --all option are given' do
      it 'exits with an error message' do
        expect { cli.rotate }.to output(
          a_string_including('No account(s) given')
        ).to_stdout
          .and raise_error(SystemExit)
      end
    end

    context 'when a username is given' do
      let(:account) { Fabricate(:account) }

      it 'correctly rotates keys for the specified account' do
        old_private_key = account.private_key
        old_public_key = account.public_key

        cli.rotate(account.username)
        account.reload

        expect(account.private_key).to_not eq(old_private_key)
        expect(account.public_key).to_not eq(old_public_key)
      end

      it 'broadcasts the new keys for the specified account' do
        allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)

        cli.rotate(account.username)

        expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
      end

      context 'when the given username is not found' do
        it 'exits with an error message when the specified username is not found' do
          expect { cli.rotate('non_existent_username') }.to output(
            a_string_including('No such account')
          ).to_stdout
            .and raise_error(SystemExit)
        end
      end
    end

    context 'when --all option is provided' do
      let(:accounts) { Fabricate.times(3, :account) }
      let(:options)  { { all: true } }

      before do
        allow(Account).to receive(:local).and_return(Account.where(id: accounts.map(&:id)))
        cli.options = { all: true }
      end

      it 'correctly rotates keys for all local accounts' do
        old_private_keys = accounts.map(&:private_key)
        old_public_keys = accounts.map(&:public_key)

        cli.rotate
        accounts.each(&:reload)

        expect(accounts.map(&:private_key)).to_not eq(old_private_keys)
        expect(accounts.map(&:public_key)).to_not eq(old_public_keys)
      end

      it 'broadcasts the new keys for each account' do
        allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)

        cli.rotate

        accounts.each do |account|
          expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
        end
      end
    end
  end
end

M spec/models/form/import_spec.rb => spec/models/form/import_spec.rb +10 -0
@@ 86,6 86,7 @@ RSpec.describe Form::Import do
    it_behaves_like 'too many CSV rows', 'muting', 'imports.txt', 1
    it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2
    it_behaves_like 'too many CSV rows', 'bookmarks', 'bookmark-imports.txt', 3
    it_behaves_like 'too many CSV rows', 'lists', 'lists.csv', 2

    # Importing list of addresses with no headers into various types
    it_behaves_like 'valid import', 'following', 'imports.txt'


@@ 98,6 99,9 @@ RSpec.describe Form::Import do
    # Importing bookmarks list with no headers into expected type
    it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt'

    # Importing lists with no headers into expected type
    it_behaves_like 'valid import', 'lists', 'lists.csv'

    # Importing followed accounts with headers into various compatible types
    it_behaves_like 'valid import', 'following', 'following_accounts.csv'
    it_behaves_like 'valid import', 'blocking', 'following_accounts.csv'


@@ 273,6 277,12 @@ RSpec.describe Form::Import do
      { 'acct' => 'user@test.com', 'hide_notifications' => false },
    ]

    it_behaves_like 'on successful import', 'lists', 'merge', 'lists.csv', [
      { 'acct' => 'gargron@example.com', 'list_name' => 'Mastodon project' },
      { 'acct' => 'mastodon@example.com', 'list_name' => 'Mastodon project' },
      { 'acct' => 'foo@example.com', 'list_name' => 'test' },
    ]

    # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
    #
    # https://github.com/mastodon/mastodon/issues/20571

M spec/services/bulk_import_row_service_spec.rb => spec/services/bulk_import_row_service_spec.rb +72 -0
@@ 91,5 91,77 @@ RSpec.describe BulkImportRowService do
        end
      end
    end

    context 'when importing a list row' do
      let(:import_type) { 'lists' }
      let(:target_account) { Fabricate(:account) }
      let(:data) do
        { 'acct' => target_account.acct, 'list_name' => 'my list' }
      end

      shared_examples 'common behavior' do
        context 'when the target account is already followed' do
          before do
            account.follow!(target_account)
          end

          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end

        context 'when the user already requested to follow the target account' do
          before do
            account.request_follow!(target_account)
          end

          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end

        context 'when the target account is neither followed nor requested' do
          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end

        context 'when the target account is the user themself' do
          let(:target_account) { account }

          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end
      end

      context 'when the list does not exist yet' do
        include_examples 'common behavior'
      end

      context 'when the list exists' do
        before do
          Fabricate(:list, account: account, title: 'my list')
        end

        include_examples 'common behavior'
      end
    end
  end
end

M spec/services/fetch_link_card_service_spec.rb => spec/services/fetch_link_card_service_spec.rb +10 -0
@@ 12,6 12,7 @@ RSpec.describe FetchLinkCardService, type: :service do
    stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
    stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
    stub_request(:get, 'https://github.com/qbi/WannaCry').to_return(status: 404)
    stub_request(:get, 'http://example.com/test?data=file.gpx%5E1').to_return(status: 200)
    stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
    stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))



@@ 87,6 88,15 @@ RSpec.describe FetchLinkCardService, type: :service do
        expect(a_request(:get, 'http://example.com/sjis')).to_not have_been_made
      end
    end

    context do
      let(:status) { Fabricate(:status, text: 'test http://example.com/test?data=file.gpx^1') }

      it 'does fetch URLs with a caret in search params' do
        expect(a_request(:get, 'http://example.com/test?data=file.gpx')).to_not have_been_made
        expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once
      end
    end
  end

  context 'with a remote status' do

M yarn.lock => yarn.lock +154 -137
@@ 1743,6 1743,15 @@
    "@jridgewell/set-array" "^1.0.0"
    "@jridgewell/sourcemap-codec" "^1.4.10"

"@jridgewell/gen-mapping@^0.3.0":
  version "0.3.3"
  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
  integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
  dependencies:
    "@jridgewell/set-array" "^1.0.1"
    "@jridgewell/sourcemap-codec" "^1.4.10"
    "@jridgewell/trace-mapping" "^0.3.9"

"@jridgewell/gen-mapping@^0.3.2":
  version "0.3.2"
  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"


@@ 1767,6 1776,14 @@
  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==

"@jridgewell/source-map@^0.3.2":
  version "0.3.3"
  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda"
  integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==
  dependencies:
    "@jridgewell/gen-mapping" "^0.3.0"
    "@jridgewell/trace-mapping" "^0.3.9"

"@jridgewell/sourcemap-codec@1.4.14":
  version "1.4.14"
  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"


@@ 2231,10 2248,10 @@
  dependencies:
    "@types/istanbul-lib-report" "*"

"@types/jest@*", "@types/jest@^29.5.1":
  version "29.5.1"
  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47"
  integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==
"@types/jest@*", "@types/jest@^29.5.2":
  version "29.5.2"
  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.2.tgz#86b4afc86e3a8f3005b297ed8a72494f89e6395b"
  integrity sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==
  dependencies:
    expect "^29.0.0"
    pretty-format "^29.0.0"


@@ 3007,9 3024,9 @@ ansi-regex@^2.0.0:
  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=

ansi-regex@^4.1.0:
  version "4.1.0"
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
  version "4.1.1"
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
  integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==

ansi-regex@^5.0.0, ansi-regex@^5.0.1:
  version "5.0.1"


@@ 5264,10 5281,10 @@ eslint-plugin-import@~2.27.5:
    semver "^6.3.0"
    tsconfig-paths "^3.14.1"

eslint-plugin-jsdoc@^45.0.0:
  version "45.0.0"
  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-45.0.0.tgz#6be84e4842a7138cc571a907ea9c31c42eaac5c0"
  integrity sha512-l2+Jcs/Ps7oFA+SWY+0sweU/e5LgricnEl6EsDlyRTF5y0+NWL1y9Qwz9PHwHAxtdJq6lxPjEQWmYLMkvhzD4g==
eslint-plugin-jsdoc@^46.1.0:
  version "46.1.0"
  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.1.0.tgz#3ff932b70bc25f3745049f525a789faed7c948da"
  integrity sha512-NpjpSuWR+Wwxzmssji7AVty1Vu0JvI7v+cTj+Rw1nKVjGv2eMvLGM/SI4VpgTXp82JbLtFOsA2QYLHT3YSmASA==
  dependencies:
    "@es-joy/jsdoccomment" "~0.39.4"
    are-docs-informative "^0.0.2"


@@ 7860,9 7877,9 @@ loader-utils@^1.2.3, loader-utils@^1.4.0:
    json5 "^1.0.1"

loader-utils@^2.0.0:
  version "2.0.0"
  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
  integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
  version "2.0.4"
  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
  integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
  dependencies:
    big.js "^5.2.2"
    emojis-list "^3.0.0"


@@ 10782,7 10799,7 @@ source-map@^0.7.3:
  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
  integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==

source-map@^0.8.0-beta.0, source-map@~0.8.0-beta.0:
source-map@^0.8.0-beta.0:
  version "0.8.0-beta.0"
  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11"
  integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==


@@ 10863,9 10880,9 @@ sprintf-js@~1.0.2:
  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=

ssri@^8.0.0:
  version "8.0.0"
  resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808"
  integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==
  version "8.0.1"
  resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
  integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
  dependencies:
    minipass "^3.1.1"



@@ 11359,13 11376,13 @@ terser-webpack-plugin@^1.4.3, terser-webpack-plugin@^4.2.3:
    webpack-sources "^1.4.3"

terser@^5.0.0, terser@^5.3.4:
  version "5.13.1"
  resolved "https://registry.yarnpkg.com/terser/-/terser-5.13.1.tgz#66332cdc5a01b04a224c9fad449fc1a18eaa1799"
  integrity sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==
  version "5.17.6"
  resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de"
  integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==
  dependencies:
    "@jridgewell/source-map" "^0.3.2"
    acorn "^8.5.0"
    commander "^2.20.0"
    source-map "~0.8.0-beta.0"
    source-map-support "~0.5.20"

tesseract.js-core@^2.2.0:


@@ 12267,25 12284,25 @@ word-wrap@^1.2.3, word-wrap@~1.2.3:
  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==

workbox-background-sync@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f"
  integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg==
workbox-background-sync@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5"
  integrity sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==
  dependencies:
    idb "^7.0.1"
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-broadcast-update@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e"
  integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ==
workbox-broadcast-update@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz#7f611ca1a94ba8ac0aa40fa171c9713e0f937d22"
  integrity sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==
  dependencies:
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-build@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0"
  integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw==
workbox-build@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.0.0.tgz#02ab5ef2991b3369b8b9395703f08912212769b4"
  integrity sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg==
  dependencies:
    "@apideck/better-ajv-errors" "^0.3.1"
    "@babel/core" "^7.11.1"


@@ 12309,132 12326,132 @@ workbox-build@6.6.1:
    strip-comments "^2.0.1"
    tempy "^0.6.0"
    upath "^1.2.0"
    workbox-background-sync "6.6.1"
    workbox-broadcast-update "6.6.1"
    workbox-cacheable-response "6.6.1"
    workbox-core "6.6.1"
    workbox-expiration "6.6.1"
    workbox-google-analytics "6.6.1"
    workbox-navigation-preload "6.6.1"
    workbox-precaching "6.6.1"
    workbox-range-requests "6.6.1"
    workbox-recipes "6.6.1"
    workbox-routing "6.6.1"
    workbox-strategies "6.6.1"
    workbox-streams "6.6.1"
    workbox-sw "6.6.1"
    workbox-window "6.6.1"

workbox-cacheable-response@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9"
  integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag==
  dependencies:
    workbox-core "6.6.1"

workbox-core@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265"
  integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==

workbox-expiration@6.6.1, workbox-expiration@^6.6.0:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
  integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
    workbox-background-sync "7.0.0"
    workbox-broadcast-update "7.0.0"
    workbox-cacheable-response "7.0.0"
    workbox-core "7.0.0"
    workbox-expiration "7.0.0"
    workbox-google-analytics "7.0.0"
    workbox-navigation-preload "7.0.0"
    workbox-precaching "7.0.0"
    workbox-range-requests "7.0.0"
    workbox-recipes "7.0.0"
    workbox-routing "7.0.0"
    workbox-strategies "7.0.0"
    workbox-streams "7.0.0"
    workbox-sw "7.0.0"
    workbox-window "7.0.0"

workbox-cacheable-response@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz#ee27c036728189eed69d25a135013053277482d2"
  integrity sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==
  dependencies:
    workbox-core "7.0.0"

workbox-core@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.0.0.tgz#dec114ec923cc2adc967dd9be1b8a0bed50a3545"
  integrity sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==

workbox-expiration@7.0.0, workbox-expiration@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.0.0.tgz#3d90bcf2a7577241de950f89784f6546b66c2baa"
  integrity sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==
  dependencies:
    idb "^7.0.1"
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-google-analytics@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d"
  integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA==
workbox-google-analytics@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz#603b2c4244af1e85de0fb26287d4e17d3293452a"
  integrity sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==
  dependencies:
    workbox-background-sync "6.6.1"
    workbox-core "6.6.1"
    workbox-routing "6.6.1"
    workbox-strategies "6.6.1"
    workbox-background-sync "7.0.0"
    workbox-core "7.0.0"
    workbox-routing "7.0.0"
    workbox-strategies "7.0.0"

workbox-navigation-preload@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059"
  integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA==
workbox-navigation-preload@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz#4913878dbbd97057181d57baa18d2bbdde085c6c"
  integrity sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==
  dependencies:
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-precaching@6.6.1, workbox-precaching@^6.6.0:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
  integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
workbox-precaching@7.0.0, workbox-precaching@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.0.0.tgz#3979ba8033aadf3144b70e9fe631d870d5fbaa03"
  integrity sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA==
  dependencies:
    workbox-core "6.6.1"
    workbox-routing "6.6.1"
    workbox-strategies "6.6.1"
    workbox-core "7.0.0"
    workbox-routing "7.0.0"
    workbox-strategies "7.0.0"

workbox-range-requests@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39"
  integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g==
workbox-range-requests@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz#97511901e043df27c1aa422adcc999a7751f52ed"
  integrity sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==
  dependencies:
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-recipes@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae"
  integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g==
workbox-recipes@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.0.0.tgz#1a6a01c8c2dfe5a41eef0fed3fe517e8a45c6514"
  integrity sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww==
  dependencies:
    workbox-cacheable-response "6.6.1"
    workbox-core "6.6.1"
    workbox-expiration "6.6.1"
    workbox-precaching "6.6.1"
    workbox-routing "6.6.1"
    workbox-strategies "6.6.1"
    workbox-cacheable-response "7.0.0"
    workbox-core "7.0.0"
    workbox-expiration "7.0.0"
    workbox-precaching "7.0.0"
    workbox-routing "7.0.0"
    workbox-strategies "7.0.0"

workbox-routing@6.6.1, workbox-routing@^6.6.0:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581"
  integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg==
workbox-routing@7.0.0, workbox-routing@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.0.0.tgz#6668438a06554f60645aedc77244a4fe3a91e302"
  integrity sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA==
  dependencies:
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-strategies@6.6.1, workbox-strategies@^6.6.0:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf"
  integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw==
workbox-strategies@7.0.0, workbox-strategies@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.0.0.tgz#dcba32b3f3074476019049cc490fe1a60ea73382"
  integrity sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA==
  dependencies:
    workbox-core "6.6.1"
    workbox-core "7.0.0"

workbox-streams@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26"
  integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q==
workbox-streams@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.0.0.tgz#36722aecd04785f88b6f709e541c094fc658c0f9"
  integrity sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==
  dependencies:
    workbox-core "6.6.1"
    workbox-routing "6.6.1"
    workbox-core "7.0.0"
    workbox-routing "7.0.0"

workbox-sw@6.6.1:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c"
  integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ==
workbox-sw@7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.0.0.tgz#7350126411e3de1409f7ec243df8d06bb5b08b86"
  integrity sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA==

workbox-webpack-plugin@^6.6.0:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531"
  integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA==
workbox-webpack-plugin@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz#6c61661a2cacde1239192a5877a041a2943d1a55"
  integrity sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw==
  dependencies:
    fast-json-stable-stringify "^2.1.0"
    pretty-bytes "^5.4.1"
    upath "^1.2.0"
    webpack-sources "^1.4.3"
    workbox-build "6.6.1"
    workbox-build "7.0.0"

workbox-window@6.6.1, workbox-window@^6.6.0:
  version "6.6.1"
  resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e"
  integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ==
workbox-window@7.0.0, workbox-window@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.0.0.tgz#a683ab33c896e4f16786794eac7978fc98a25d08"
  integrity sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==
  dependencies:
    "@types/trusted-types" "^2.0.2"
    workbox-core "6.6.1"
    workbox-core "7.0.0"

wrap-ansi@^5.1.0:
  version "5.1.0"


@@ 12485,9 12502,9 @@ write-file-atomic@^5.0.1:
    signal-exit "^4.0.1"

ws@^6.2.1:
  version "6.2.1"
  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
  integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
  version "6.2.2"
  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
  integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
  dependencies:
    async-limiter "~1.0.0"