Merge commit '5fae2de454806730742b7be7435ae1c4fb97cf3c' into glitch-soc/merge-upstream

pull/62/head
Claire 2023-06-10 15:17:08 +02:00
commit aa57f7e3e2
28 changed files with 792 additions and 229 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ class BulkImport < ApplicationRecord
muting: 2,
domain_blocking: 3,
bookmarks: 4,
lists: 5,
}
enum state: {

View File

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

View File

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

View File

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

View File

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

View File

@ -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] = /(?:
(?:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3
spec/fixtures/files/lists.csv vendored Normal file
View File

@ -0,0 +1,3 @@
Mastodon project,gargron@example.com
Mastodon project,mastodon@example.com
test,foo@example.com
1 Mastodon project gargron@example.com
2 Mastodon project mastodon@example.com
3 test foo@example.com

View File

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

View File

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

View File

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

View File

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

View File

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

283
yarn.lock
View File

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