diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2c75736375..6c09cddd51 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -896,7 +896,6 @@ Rails/WhereExists: - 'app/validators/vote_validator.rb' - 'app/workers/move_worker.rb' - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' - - 'lib/mastodon/cli/email_domain_blocks.rb' - 'lib/tasks/tests.rake' - 'spec/controllers/api/v1/accounts/notes_controller_spec.rb' - 'spec/controllers/api/v1/tags_controller_spec.rb' @@ -958,7 +957,6 @@ Style/FormatStringToken: Exclude: - 'app/models/privacy_policy.rb' - 'config/initializers/devise.rb' - - 'lib/mastodon/cli/maintenance.rb' - 'lib/paperclip/color_extractor.rb' # This cop supports unsafe autocorrection (--autocorrect-all). diff --git a/Gemfile b/Gemfile index 6a444acd25..400afef4af 100644 --- a/Gemfile +++ b/Gemfile @@ -59,7 +59,7 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' -gem 'nokogiri', '~> 1.14' +gem 'nokogiri', '~> 1.15' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' diff --git a/Gemfile.lock b/Gemfile.lock index 8b3ccc7970..5f61144d89 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -439,8 +439,8 @@ GEM net-protocol net-ssh (7.1.0) nio4r (2.5.9) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) racc (~> 1.4) nsa (0.2.8) activesupport (>= 4.2, < 7) @@ -642,7 +642,7 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.8) + sidekiq (6.5.9) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -829,7 +829,7 @@ DEPENDENCIES mime-types (~> 3.4.1) net-http (~> 0.3.2) net-ldap (~> 0.18) - nokogiri (~> 1.14) + nokogiri (~> 1.15) nsa (~> 0.2) oj (~> 3.14) omniauth (~> 1.9) diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx index e8340235e8..59522eb8fd 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.jsx +++ b/app/javascript/mastodon/features/interaction_modal/index.jsx @@ -13,7 +13,7 @@ import { registrationsOpen } from 'mastodon/initial_state'; const mapStateToProps = (state, { accountId }) => ({ displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), - signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], '/auth/sign_up') || '/auth/sign_up', + signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up', }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 8e76bf51ed..15bc1ace59 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -168,8 +168,9 @@ const makeMapStateToProps = () => { }; const truncate = (str, num) => { - if (str.length > num) { - return str.slice(0, num) + '…'; + const arr = Array.from(str); + if (arr.length > num) { + return arr.slice(0, num).join('') + '…'; } else { return str; } diff --git a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx index a4289d45db..abae34f7fd 100644 --- a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx +++ b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx @@ -17,7 +17,7 @@ const SignInBanner = () => { let signupButton; - const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], '/auth/sign_up') || '/auth/sign_up'); + const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up'); if (registrationsOpen) { signupButton = ( diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index a2000e5967..a3406e5a57 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -19,7 +19,7 @@ class FetchResourceService < BaseService private - def process(url, terminal = false) + def process(url, terminal: false) @url = url perform_request { |response| process_response(response, terminal) } diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb index 417f126ccd..33520df25d 100644 --- a/lib/mastodon/cli/accounts.rb +++ b/lib/mastodon/cli/accounts.rb @@ -113,12 +113,7 @@ module Mastodon::CLI say('OK', :green) say("New password: #{password}") else - user.errors.each do |error| - say('Failure/Error: ', :red) - say(error.attribute) - say(" #{error.type}", :red) - end - + report_errors(user.errors) exit(1) end end @@ -189,12 +184,7 @@ module Mastodon::CLI say('OK', :green) say("New password: #{password}") if options[:reset_password] else - user.errors.each do |error| - say('Failure/Error: ', :red) - say(error.attribute) - say(" #{error.type}", :red) - end - + report_errors(user.errors) exit(1) end end @@ -217,7 +207,6 @@ module Mastodon::CLI exit(1) end - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' account = nil if username.present? @@ -234,9 +223,9 @@ module Mastodon::CLI end end - say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run}") - DeleteAccountService.new.call(account, reserve_email: false) unless options[:dry_run] - say("OK#{dry_run}", :green) + say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run_mode_suffix}") + DeleteAccountService.new.call(account, reserve_email: false) unless dry_run? + say("OK#{dry_run_mode_suffix}", :green) end option :force, type: :boolean, aliases: [:f], description: 'Override public key check' @@ -291,7 +280,7 @@ module Mastodon::CLI Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck(:uri).each do |uri| say("Duplicates found for #{uri}") begin - ActivityPub::FetchRemoteAccountService.new.call(uri) unless options[:dry_run] + ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run? rescue => e say("Error processing #{uri}: #{e}", :red) end @@ -332,7 +321,6 @@ module Mastodon::CLI LONG_DESC def cull(*domains) skip_threshold = 7.days.ago - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' skip_domains = Concurrent::Set.new query = Account.remote.where(protocol: :activitypub) @@ -350,7 +338,7 @@ module Mastodon::CLI end if [404, 410].include?(code) - DeleteAccountService.new.call(account, reserve_username: false) unless options[:dry_run] + DeleteAccountService.new.call(account, reserve_username: false) unless dry_run? 1 else # Touch account even during dry run to avoid getting the account into the window again @@ -358,7 +346,7 @@ module Mastodon::CLI end end - say("Visited #{processed} accounts, removed #{culled}#{dry_run}", :green) + say("Visited #{processed} accounts, removed #{culled}#{dry_run_mode_suffix}", :green) unless skip_domains.empty? say('The following domains were not available during the check:', :yellow) @@ -381,21 +369,19 @@ module Mastodon::CLI specified with space-separated USERNAMES. LONG_DESC def refresh(*usernames) - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' - if options[:domain] || options[:all] scope = Account.remote scope = scope.where(domain: options[:domain]) if options[:domain] processed, = parallelize_with_progress(scope) do |account| - next if options[:dry_run] + next if dry_run? account.reset_avatar! account.reset_header! account.save end - say("Refreshed #{processed} accounts#{dry_run}", :green, true) + say("Refreshed #{processed} accounts#{dry_run_mode_suffix}", :green, true) elsif !usernames.empty? usernames.each do |user| user, domain = user.split('@') @@ -406,7 +392,7 @@ module Mastodon::CLI exit(1) end - next if options[:dry_run] + next if dry_run? begin account.reset_avatar! @@ -417,7 +403,7 @@ module Mastodon::CLI end end - say("OK#{dry_run}", :green) + say("OK#{dry_run_mode_suffix}", :green) else say('No account(s) given', :red) exit(1) @@ -568,8 +554,6 @@ module Mastodon::CLI - not muted/blocked by us LONG_DESC def prune - dry_run = options[:dry_run] ? ' (dry run)' : '' - query = Account.remote.where.not(actor_type: %i(Application Service)) query = query.where('NOT EXISTS (SELECT 1 FROM mentions WHERE account_id = accounts.id)') query = query.where('NOT EXISTS (SELECT 1 FROM favourites WHERE account_id = accounts.id)') @@ -585,11 +569,11 @@ module Mastodon::CLI next if account.suspended? next if account.silenced? - account.destroy unless options[:dry_run] + account.destroy unless dry_run? 1 end - say("OK, pruned #{deleted} accounts#{dry_run}", :green) + say("OK, pruned #{deleted} accounts#{dry_run_mode_suffix}", :green) end option :force, type: :boolean @@ -667,6 +651,14 @@ module Mastodon::CLI private + def report_errors(errors) + errors.each do |error| + say('Failure/Error: ', :red) + say(error.attribute) + say(" #{error.type}", :red) + end + end + def rotate_keys_for_account(account, delay = 0) if account.nil? say('No such account', :red) diff --git a/lib/mastodon/cli/domains.rb b/lib/mastodon/cli/domains.rb index d885c95638..d17b253681 100644 --- a/lib/mastodon/cli/domains.rb +++ b/lib/mastodon/cli/domains.rb @@ -34,7 +34,6 @@ module Mastodon::CLI When the --purge-domain-blocks option is given, also purge matching domain blocks. LONG_DESC def purge(*domains) - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' domains = domains.map { |domain| TagManager.instance.normalize_domain(domain) } account_scope = Account.none domain_block_scope = DomainBlock.none @@ -79,23 +78,23 @@ module Mastodon::CLI # Actually perform the deletions processed, = parallelize_with_progress(account_scope) do |account| - DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] + DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless dry_run? end - say("Removed #{processed} accounts#{dry_run}", :green) + say("Removed #{processed} accounts#{dry_run_mode_suffix}", :green) if options[:purge_domain_blocks] domain_block_count = domain_block_scope.count - domain_block_scope.in_batches.destroy_all unless options[:dry_run] - say("Removed #{domain_block_count} domain blocks#{dry_run}", :green) + domain_block_scope.in_batches.destroy_all unless dry_run? + say("Removed #{domain_block_count} domain blocks#{dry_run_mode_suffix}", :green) end custom_emojis_count = emoji_scope.count - emoji_scope.in_batches.destroy_all unless options[:dry_run] + emoji_scope.in_batches.destroy_all unless dry_run? - Instance.refresh unless options[:dry_run] + Instance.refresh unless dry_run? - say("Removed #{custom_emojis_count} custom emojis#{dry_run}", :green) + say("Removed #{custom_emojis_count} custom emojis#{dry_run_mode_suffix}", :green) end option :concurrency, type: :numeric, default: 50, aliases: [:c] diff --git a/lib/mastodon/cli/email_domain_blocks.rb b/lib/mastodon/cli/email_domain_blocks.rb index abbbdfe31c..88a84ecb42 100644 --- a/lib/mastodon/cli/email_domain_blocks.rb +++ b/lib/mastodon/cli/email_domain_blocks.rb @@ -39,7 +39,7 @@ module Mastodon::CLI processed = 0 domains.each do |domain| - if EmailDomainBlock.where(domain: domain).exists? + if EmailDomainBlock.exists?(domain: domain) say("#{domain} is already blocked.", :yellow) skipped += 1 next @@ -60,7 +60,7 @@ module Mastodon::CLI (email_domain_block.other_domains || []).uniq.each do |hostname| another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: email_domain_block) - if EmailDomainBlock.where(domain: hostname).exists? + if EmailDomainBlock.exists?(domain: hostname) say("#{hostname} is already blocked.", :yellow) skipped += 1 next diff --git a/lib/mastodon/cli/feeds.rb b/lib/mastodon/cli/feeds.rb index 342b550ca3..34617e7538 100644 --- a/lib/mastodon/cli/feeds.rb +++ b/lib/mastodon/cli/feeds.rb @@ -18,14 +18,12 @@ module Mastodon::CLI Otherwise, a single user specified by USERNAME. LONG_DESC def build(username = nil) - dry_run = options[:dry_run] ? '(DRY RUN)' : '' - if options[:all] || username.nil? processed, = parallelize_with_progress(Account.joins(:user).merge(User.active)) do |account| - PrecomputeFeedService.new.call(account) unless options[:dry_run] + PrecomputeFeedService.new.call(account) unless dry_run? end - say("Regenerated feeds for #{processed} accounts #{dry_run}", :green, true) + say("Regenerated feeds for #{processed} accounts #{dry_run_mode_suffix}", :green, true) elsif username.present? account = Account.find_local(username) @@ -34,9 +32,9 @@ module Mastodon::CLI exit(1) end - PrecomputeFeedService.new.call(account) unless options[:dry_run] + PrecomputeFeedService.new.call(account) unless dry_run? - say("OK #{dry_run}", :green, true) + say("OK #{dry_run_mode_suffix}", :green, true) else say('No account(s) given', :red) exit(1) diff --git a/lib/mastodon/cli/helper.rb b/lib/mastodon/cli/helper.rb index b277e16ebd..78931b9a22 100644 --- a/lib/mastodon/cli/helper.rb +++ b/lib/mastodon/cli/helper.rb @@ -15,6 +15,10 @@ module Mastodon::CLI options[:dry_run] end + def dry_run_mode_suffix + dry_run? ? ' (DRY RUN)' : '' + end + def create_progress_bar(total = nil) ProgressBar.create(total: total, format: '%c/%u |%b%i| %e') end diff --git a/lib/mastodon/cli/main.rb b/lib/mastodon/cli/main.rb index e61a6f9c46..1594eadce8 100644 --- a/lib/mastodon/cli/main.rb +++ b/lib/mastodon/cli/main.rb @@ -94,7 +94,7 @@ module Mastodon::CLI exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain - unless options[:dry_run] + unless dry_run? prompt.warn('This operation WILL NOT be reversible. It can also take a long time.') prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.') prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.') @@ -104,12 +104,11 @@ module Mastodon::CLI inboxes = Account.inboxes processed = 0 - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' - Setting.registrations_mode = 'none' unless options[:dry_run] + Setting.registrations_mode = 'none' unless dry_run? if inboxes.empty? - Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless options[:dry_run] + Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless dry_run? prompt.ok('It seems like your server has not federated with anything') prompt.ok('You can shut it down and delete it any time') return @@ -126,7 +125,7 @@ module Mastodon::CLI json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account)) - unless options[:dry_run] + unless dry_run? ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url| [json, account.id, inbox_url] end @@ -140,7 +139,7 @@ module Mastodon::CLI Account.local.without_suspended.find_each { |account| delete_account.call(account) } Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) } - prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}") + prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run_mode_suffix}") prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data') rescue TTY::Reader::InputInterrupt exit(1) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index b107480359..d604973320 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -225,9 +225,8 @@ module Mastodon::CLI @prompt.warn "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}" @prompt.warn 'Please reach out to them and set another address with `tootctl account modify` or delete them.' - i = 0 - users.each do |user| - user.update!(email: "#{i} " + user.email) + users.each_with_index do |user, index| + user.update!(email: "#{index} " + user.email) end end @@ -552,7 +551,16 @@ module Mastodon::CLI @prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.' accounts.each_with_index do |account, idx| - @prompt.say format('%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s', idx, account.username, account.created_at, account.updated_at, account.user&.last_sign_in_at&.to_s || 'N/A', account.account_stat&.statuses_count || 0, account.account_stat&.last_status_at || 'N/A') + @prompt.say format( + '%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s', + index: idx, + username: account.username, + created_at: account.created_at, + updated_at: account.updated_at, + last_log_in_at: account.user&.last_sign_in_at&.to_s || 'N/A', + status_count: account.account_stat&.statuses_count || 0, + last_status_at: account.account_stat&.last_status_at || 'N/A' + ) end @prompt.say 'Please chose the one to keep unchanged, other ones will be automatically renamed.' diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb index 045f6351ad..40b270ffb2 100644 --- a/lib/mastodon/cli/media.rb +++ b/lib/mastodon/cli/media.rb @@ -35,12 +35,12 @@ module Mastodon::CLI say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true) exit(1) end + if options[:include_follows] && !(options[:prune_profiles] || options[:remove_headers]) say('--include-follows can only be used with --prune-profiles or --remove-headers', :red, true) exit(1) end - time_ago = options[:days].days.ago - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' + time_ago = options[:days].days.ago if options[:prune_profiles] || options[:remove_headers] processed, aggregate = parallelize_with_progress(Account.remote.where({ last_webfingered_at: ..time_ago, updated_at: ..time_ago })) do |account| @@ -51,7 +51,7 @@ module Mastodon::CLI size = (account.header_file_size || 0) size += (account.avatar_file_size || 0) if options[:prune_profiles] - unless options[:dry_run] + unless dry_run? account.header.destroy account.avatar.destroy if options[:prune_profiles] account.save! @@ -60,7 +60,7 @@ module Mastodon::CLI size end - say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run}", :green, true) + say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run_mode_suffix}", :green, true) end unless options[:prune_profiles] || options[:remove_headers] @@ -69,7 +69,7 @@ module Mastodon::CLI size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0) - unless options[:dry_run] + unless dry_run? media_attachment.file.destroy media_attachment.thumbnail.destroy media_attachment.save @@ -78,7 +78,7 @@ module Mastodon::CLI size end - say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true) + say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true) end end @@ -97,7 +97,6 @@ module Mastodon::CLI progress = create_progress_bar(nil) reclaimed_bytes = 0 removed = 0 - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' prefix = options[:prefix] case Paperclip::Attachment.default_options[:storage] @@ -123,7 +122,7 @@ module Mastodon::CLI record_map = preload_records_from_mixed_objects(objects) objects.each do |object| - object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !options[:dry_run] + object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !dry_run? path_segments = object.key.split('/') path_segments.delete('cache') @@ -145,7 +144,7 @@ module Mastodon::CLI next unless attachment.blank? || !attachment.variant?(file_name) begin - object.delete unless options[:dry_run] + object.delete unless dry_run? reclaimed_bytes += object.size removed += 1 @@ -194,7 +193,7 @@ module Mastodon::CLI begin size = File.size(path) - unless options[:dry_run] + unless dry_run? File.delete(path) begin FileUtils.rmdir(File.dirname(path), parents: true) @@ -216,7 +215,7 @@ module Mastodon::CLI progress.total = progress.progress progress.finish - say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true) + say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run_mode_suffix}", :green, true) end option :account, type: :string @@ -246,8 +245,6 @@ module Mastodon::CLI not be re-downloaded. To force re-download of every URL, use --force. DESC def refresh - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' - if options[:status] scope = MediaAttachment.where(status_id: options[:status]) elsif options[:account] @@ -274,7 +271,7 @@ module Mastodon::CLI next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?) next if DomainBlock.reject_media?(media_attachment.account.domain) - unless options[:dry_run] + unless dry_run? media_attachment.reset_file! media_attachment.reset_thumbnail! media_attachment.save @@ -283,7 +280,7 @@ module Mastodon::CLI media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0) end - say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true) + say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true) end desc 'usage', 'Calculate disk space consumed by Mastodon' diff --git a/lib/mastodon/cli/preview_cards.rb b/lib/mastodon/cli/preview_cards.rb index c66bcb504a..2df3d095da 100644 --- a/lib/mastodon/cli/preview_cards.rb +++ b/lib/mastodon/cli/preview_cards.rb @@ -27,7 +27,6 @@ module Mastodon::CLI DESC def remove time_ago = options[:days].days.ago - dry_run = options[:dry_run] ? ' (DRY RUN)' : '' link = options[:link] ? 'link-type ' : '' scope = PreviewCard.cached scope = scope.where(type: :link) if options[:link] @@ -38,7 +37,7 @@ module Mastodon::CLI size = preview_card.image_file_size - unless options[:dry_run] + unless dry_run? preview_card.image.destroy preview_card.save end @@ -46,7 +45,7 @@ module Mastodon::CLI size end - say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true) + say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true) end end end diff --git a/lib/mastodon/cli/upgrade.rb b/lib/mastodon/cli/upgrade.rb index e5c86b3d73..88390da5bf 100644 --- a/lib/mastodon/cli/upgrade.rb +++ b/lib/mastodon/cli/upgrade.rb @@ -17,7 +17,6 @@ module Mastodon::CLI LONG_DESC def storage_schema progress = create_progress_bar(nil) - dry_run = dry_run? ? ' (DRY RUN)' : '' records = 0 klasses = [ @@ -69,7 +68,7 @@ module Mastodon::CLI progress.total = progress.progress progress.finish - say("Upgraded storage schema of #{records} records#{dry_run}", :green, true) + say("Upgraded storage schema of #{records} records#{dry_run_mode_suffix}", :green, true) end private diff --git a/package.json b/package.json index d96e9b189b..244fe7b991 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^6.4.3", "js-yaml": "^4.1.0", - "jsdom": "^22.0.0", + "jsdom": "^22.1.0", "lodash": "^4.17.21", "mark-loader": "^0.1.6", "marky": "^1.2.5", @@ -83,7 +83,7 @@ "path-complete-extname": "^1.0.0", "pg": "^8.5.0", "pg-connection-string": "^2.6.0", - "postcss": "^8.4.23", + "postcss": "^8.4.24", "postcss-loader": "^4.3.0", "prop-types": "^15.8.1", "punycode": "^2.3.0", @@ -150,18 +150,18 @@ "@types/intl": "^1.2.0", "@types/jest": "^29.5.1", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.14.194", + "@types/lodash": "^4.14.195", "@types/npmlog": "^4.1.4", "@types/object-assign": "^4.0.30", "@types/pg": "^8.6.6", "@types/prop-types": "^15.7.5", "@types/punycode": "^2.1.0", - "@types/react": "^18.0.26", + "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", "@types/react-helmet": "^6.1.6", "@types/react-immutable-proptypes": "^2.1.0", "@types/react-intl": "2.3.18", - "@types/react-motion": "^0.0.33", + "@types/react-motion": "^0.0.34", "@types/react-overlays": "^3.1.0", "@types/react-router-dom": "^5.3.3", "@types/react-select": "^5.0.1", @@ -175,8 +175,8 @@ "@types/uuid": "^9.0.0", "@types/webpack": "^4.41.33", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^5.59.7", - "@typescript-eslint/parser": "^5.59.7", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", "babel-jest": "^29.5.0", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index 25f1311d40..bcb353aeee 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -4,9 +4,577 @@ require 'rails_helper' require 'mastodon/cli/accounts' describe Mastodon::CLI::Accounts do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#create' do + shared_examples 'a new user with given email address and username' do + it 'creates a new user with the specified email address' do + cli.invoke(:create, arguments, options) + + expect(User.find_by(email: options[:email])).to be_present + end + + it 'creates a new local account with the specified username' do + cli.invoke(:create, arguments, options) + + expect(Account.find_local('tootctl_username')).to be_present + end + + it 'returns "OK" and newly generated password' do + allow(SecureRandom).to receive(:hex).and_return('test_password') + + expect { cli.invoke(:create, arguments, options) }.to output( + a_string_including("OK\nNew password: test_password") + ).to_stdout + end + end + + context 'when required USERNAME and --email are provided' do + let(:arguments) { ['tootctl_username'] } + + context 'with USERNAME and --email only' do + let(:options) { { email: 'tootctl@example.com' } } + + it_behaves_like 'a new user with given email address and username' + + context 'with invalid --email value' do + let(:options) { { email: 'invalid' } } + + it 'exits with an error message' do + expect { cli.invoke(:create, arguments, options) }.to output( + a_string_including('Failure/Error: email') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + + context 'with --confirmed option' do + let(:options) { { email: 'tootctl@example.com', confirmed: true } } + + it_behaves_like 'a new user with given email address and username' + + it 'creates a new user with confirmed status' do + cli.invoke(:create, arguments, options) + + user = User.find_by(email: options[:email]) + + expect(user.confirmed?).to be(true) + end + end + + context 'with --approve option' do + let(:options) { { email: 'tootctl@example.com', approve: true } } + + before do + Form::AdminSettings.new(registrations_mode: 'approved').save + end + + it_behaves_like 'a new user with given email address and username' + + it 'creates a new user with approved status' do + cli.invoke(:create, arguments, options) + + user = User.find_by(email: options[:email]) + + expect(user.approved?).to be(true) + end + end + + context 'with --role option' do + context 'when role exists' do + let(:default_role) { Fabricate(:user_role) } + let(:options) { { email: 'tootctl@example.com', role: default_role.name } } + + it_behaves_like 'a new user with given email address and username' + + it 'creates a new user and assigns the specified role' do + cli.invoke(:create, arguments, options) + + role = User.find_by(email: options[:email])&.role + + expect(role.name).to eq(default_role.name) + end + end + + context 'when role does not exist' do + let(:options) { { email: 'tootctl@example.com', role: '404' } } + + it 'exits with an error message indicating the role name was not found' do + expect { cli.invoke(:create, arguments, options) }.to output( + a_string_including('Cannot find user role with that name') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + + context 'with --reattach option' do + context "when account's user is present" do + let(:options) { { email: 'tootctl_new@example.com', reattach: true } } + let(:user) { Fabricate.build(:user, email: 'tootctl@example.com') } + + before do + Fabricate(:account, username: 'tootctl_username', user: user) + end + + it 'returns an error message indicating the username is already taken' do + expect { cli.invoke(:create, arguments, options) }.to output( + a_string_including("The chosen username is currently in use\nUse --force to reattach it anyway and delete the other user") + ).to_stdout + end + + context 'with --force option' do + let(:options) { { email: 'tootctl_new@example.com', reattach: true, force: true } } + + it 'reattaches the account to the new user and deletes the previous user' do + cli.invoke(:create, arguments, options) + + user = Account.find_local('tootctl_username')&.user + + expect(user.email).to eq(options[:email]) + end + end + end + + context "when account's user is not present" do + let(:options) { { email: 'tootctl@example.com', reattach: true } } + + before do + Fabricate(:account, username: 'tootctl_username', user: nil) + end + + it_behaves_like 'a new user with given email address and username' + end + end + end + + context 'when required --email option is not provided' do + let(:arguments) { ['tootctl_username'] } + + it 'raises a required argument missing error (Thor::RequiredArgumentMissingError)' do + expect { cli.invoke(:create, arguments) } + .to raise_error(Thor::RequiredArgumentMissingError) + end + end + end + + describe '#modify' do + context 'when the given username is not found' do + let(:arguments) { ['non_existent_username'] } + + it 'exits with an error message indicating the user was not found' do + expect { cli.invoke(:modify, arguments) }.to output( + a_string_including('No user with such username') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when the given username is found' do + let(:user) { Fabricate(:user) } + let(:arguments) { [user.account.username] } + + context 'when no option is provided' do + it 'returns a successful message' do + expect { cli.invoke(:modify, arguments) }.to output( + a_string_including('OK') + ).to_stdout + end + + it 'does not modify the user' do + cli.invoke(:modify, arguments) + + expect(user).to eq(user.reload) + end + end + + context 'with --role option' do + context 'when the given role is not found' do + let(:options) { { role: '404' } } + + it 'exits with an error message indicating the role was not found' do + expect { cli.invoke(:modify, arguments, options) }.to output( + a_string_including('Cannot find user role with that name') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when the given role is found' do + let(:default_role) { Fabricate(:user_role) } + let(:options) { { role: default_role.name } } + + it "updates the user's role to the specified role" do + cli.invoke(:modify, arguments, options) + + role = user.reload.role + + expect(role.name).to eq(default_role.name) + end + end + end + + context 'with --remove-role option' do + let(:options) { { remove_role: true } } + let(:role) { Fabricate(:user_role) } + let(:user) { Fabricate(:user, role: role) } + + it "removes the user's role successfully" do + cli.invoke(:modify, arguments, options) + + role = user.reload.role + + expect(role.name).to be_empty + end + end + + context 'with --email option' do + let(:user) { Fabricate(:user, email: 'old_email@email.com') } + let(:options) { { email: 'new_email@email.com' } } + + it "sets the user's unconfirmed email to the provided email address" do + cli.invoke(:modify, arguments, options) + + expect(user.reload.unconfirmed_email).to eq(options[:email]) + end + + it "does not update the user's original email address" do + cli.invoke(:modify, arguments, options) + + expect(user.reload.email).to eq('old_email@email.com') + end + + context 'with --confirm option' do + let(:user) { Fabricate(:user, email: 'old_email@email.com', confirmed_at: nil) } + let(:options) { { email: 'new_email@email.com', confirm: true } } + + it "updates the user's email address to the provided email" do + cli.invoke(:modify, arguments, options) + + expect(user.reload.email).to eq(options[:email]) + end + + it "sets the user's email address as confirmed" do + cli.invoke(:modify, arguments, options) + + expect(user.reload.confirmed?).to be(true) + end + end + end + + context 'with --confirm option' do + let(:user) { Fabricate(:user, confirmed_at: nil) } + let(:options) { { confirm: true } } + + it "confirms the user's email address" do + cli.invoke(:modify, arguments, options) + + expect(user.reload.confirmed?).to be(true) + end + end + + context 'with --approve option' do + let(:user) { Fabricate(:user, approved: false) } + let(:options) { { approve: true } } + + before do + Form::AdminSettings.new(registrations_mode: 'approved').save + end + + it 'approves the user' do + expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.approved }.from(false).to(true) + end + end + + context 'with --disable option' do + let(:user) { Fabricate(:user, disabled: false) } + let(:options) { { disable: true } } + + it 'disables the user' do + expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.disabled }.from(false).to(true) + end + end + + context 'with --enable option' do + let(:user) { Fabricate(:user, disabled: true) } + let(:options) { { enable: true } } + + it 'enables the user' do + expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.disabled }.from(true).to(false) + end + end + + context 'with --reset-password option' do + let(:options) { { reset_password: true } } + + it 'returns a new password for the user' do + allow(SecureRandom).to receive(:hex).and_return('new_password') + + expect { cli.invoke(:modify, arguments, options) }.to output( + a_string_including('new_password') + ).to_stdout + end + end + + context 'with --disable-2fa option' do + let(:user) { Fabricate(:user, otp_required_for_login: true) } + let(:options) { { disable_2fa: true } } + + it 'disables the two-factor authentication for the user' do + expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.otp_required_for_login }.from(true).to(false) + end + end + + context 'when provided data is invalid' do + let(:user) { Fabricate(:user) } + let(:options) { { email: 'invalid' } } + + it 'exits with an error message' do + expect { cli.invoke(:modify, arguments, options) }.to output( + a_string_including('Failure/Error: email') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + end + + describe '#delete' do + let(:account) { Fabricate(:account) } + let(:arguments) { [account.username] } + let(:options) { { email: account.user.email } } + let(:delete_account_service) { instance_double(DeleteAccountService) } + + before do + allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) + allow(delete_account_service).to receive(:call) + end + + context 'when both username and --email are provided' do + it 'exits with an error message indicating that only one should be used' do + expect { cli.invoke(:delete, arguments, options) }.to output( + a_string_including('Use username or --email, not both') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when neither username nor --email are provided' do + it 'exits with an error message indicating that no username was provided' do + expect { cli.invoke(:delete) }.to output( + a_string_including('No username provided') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when username is provided' do + it 'deletes the specified user successfully' do + cli.invoke(:delete, arguments) + + expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once + end + + context 'with --dry-run option' do + let(:options) { { dry_run: true } } + + it 'does not delete the specified user' do + cli.invoke(:delete, arguments, options) + + expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false) + end + + it 'outputs a successful message in dry run mode' do + expect { cli.invoke(:delete, arguments, options) }.to output( + a_string_including('OK (DRY RUN)') + ).to_stdout + end + end + + context 'when the given username is not found' do + let(:arguments) { ['non_existent_username'] } + + it 'exits with an error message indicating that no user was found' do + expect { cli.invoke(:delete, arguments) }.to output( + a_string_including('No user with such username') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + + context 'when --email is provided' do + it 'deletes the specified user successfully' do + cli.invoke(:delete, nil, options) + + expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once + end + + context 'with --dry-run option' do + let(:options) { { email: account.user.email, dry_run: true } } + + it 'does not delete the user' do + cli.invoke(:delete, nil, options) + + expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false) + end + + it 'outputs a successful message in dry run mode' do + expect { cli.invoke(:delete, nil, options) }.to output( + a_string_including('OK (DRY RUN)') + ).to_stdout + end + end + + context 'when the given email address is not found' do + let(:options) { { email: '404@example.com' } } + + it 'exits with an error message indicating that no user was found' do + expect { cli.invoke(:delete, nil, options) }.to output( + a_string_including('No user with such email') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + end + + describe '#approve' do + let(:total_users) { 10 } + + before do + Form::AdminSettings.new(registrations_mode: 'approved').save + Fabricate.times(total_users, :user) + end + + context 'with --all option' do + it 'approves all pending registrations' do + cli.invoke(:approve, nil, all: true) + + expect(User.pluck(:approved).all?(true)).to be(true) + end + end + + context 'with --number option' do + context 'when the number is positive' do + let(:options) { { number: 3 } } + + it 'approves the earliest n pending registrations' do + cli.invoke(:approve, nil, options) + + n_earliest_pending_registrations = User.order(created_at: :asc).first(options[:number]) + + expect(n_earliest_pending_registrations.all?(&:approved?)).to be(true) + end + + it 'does not approve the remaining pending registrations' do + cli.invoke(:approve, nil, options) + + pending_registrations = User.order(created_at: :asc).last(total_users - options[:number]) + + expect(pending_registrations.all?(&:approved?)).to be(false) + end + end + + context 'when the number is negative' do + it 'exits with an error message indicating that the number must be positive' do + expect { cli.invoke(:approve, nil, number: -1) }.to output( + a_string_including('Number must be positive') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when the given number is greater than the number of users' do + let(:options) { { number: total_users * 2 } } + + it 'approves all users' do + cli.invoke(:approve, nil, options) + + expect(User.pluck(:approved).all?(true)).to be(true) + end + + it 'does not raise any error' do + expect { cli.invoke(:approve, nil, options) } + .to_not raise_error + end + end + end + + context 'with username argument' do + context 'when the given username is found' do + let(:user) { User.last } + let(:arguments) { [user.account.username] } + + it 'approves the specified user successfully' do + cli.invoke(:approve, arguments) + + expect(user.reload.approved?).to be(true) + end + end + + context 'when the given username is not found' do + let(:arguments) { ['non_existent_username'] } + + it 'exits with an error message indicating that no such account was found' do + expect { cli.invoke(:approve, arguments) }.to output( + a_string_including('No such account') + ).to_stdout + .and raise_error(SystemExit) + end + end + end + end + + describe '#follow' do + context 'when the given username is not found' do + let(:arguments) { ['non_existent_username'] } + + it 'exits with an error message indicating that no account with the given username was found' do + expect { cli.invoke(:follow, arguments) }.to output( + a_string_including('No such account') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when the given username is found' do + let!(:target_account) { Fabricate(:account) } + let!(:follower_bob) { Fabricate(:account, username: 'bob') } + let!(:follower_rony) { Fabricate(:account, username: 'rony') } + let!(:follower_charles) { Fabricate(:account, username: 'charles') } + let(:follow_service) { instance_double(FollowService, call: nil) } + let(:scope) { Account.local.without_suspended } + + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(follower_bob) + .and_yield(follower_rony) + .and_yield(follower_charles) + .and_return([3, nil]) + allow(FollowService).to receive(:new).and_return(follow_service) + end + + it 'makes all local accounts follow the target account' do + cli.follow(target_account.username) + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once + expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once + expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once + end + + it 'displays a successful message' do + expect { cli.follow(target_account.username) }.to output( + a_string_including('OK, followed target from 3 accounts') + ).to_stdout + end + end + end end diff --git a/yarn.lock b/yarn.lock index 773070a0a3..92d0db2ec3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2087,10 +2087,10 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/lodash@^4.14.194": - version "4.14.194" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" - integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== +"@types/lodash@^4.14.195": + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== "@types/mime@*": version "3.0.1" @@ -2156,12 +2156,7 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0" integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA== -"@types/prop-types@*": - version "15.7.3" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== - -"@types/prop-types@^15.7.5": +"@types/prop-types@*", "@types/prop-types@^15.7.5": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== @@ -2208,10 +2203,10 @@ resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.18.tgz#fd2d8b7f4d0a1dd05b5f1784ab0d7fe1786a690d" integrity sha512-DVNJs49zUxKRZng8VuILE886Yihdsf3yLr5vHk9zJrmF8SyRSK3sxNSvikAKxNkv9hX55XBTJShz6CkJnbNjgg== -"@types/react-motion@^0.0.33": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@types/react-motion/-/react-motion-0.0.33.tgz#c156c400ace995584990344cc0239e41f411f425" - integrity sha512-R9grd4EwdDBcKKq7Zhszd8ukyy2BLKN6ooNI0V39nUl/sui+m7VI94cdebYemBteoPHmO7J7BZk+cIf+Xnk4TA== +"@types/react-motion@^0.0.34": + version "0.0.34" + resolved "https://registry.yarnpkg.com/@types/react-motion/-/react-motion-0.0.34.tgz#789ff2063e2f7fbb6085d291135c442e8b35291a" + integrity sha512-/rFI22Vg4Xzb47hXtS06WkzUGRu+Vb3yDleuxiqzGj0JbXYXQUCgwSa2ZU12K7ubKi4C8xsdIN3xt4Z4fjSdPw== dependencies: "@types/react" "*" @@ -2298,10 +2293,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.0.26": - version "18.2.6" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.6.tgz#5cd53ee0d30ffc193b159d3516c8c8ad2f19d571" - integrity sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA== +"@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.0.26", "@types/react@^18.2.7": + version "18.2.7" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.7.tgz#dfb4518042a3117a045b8c222316f83414a783b3" + integrity sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2444,15 +2439,15 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz#e470af414f05ecfdc05a23e9ce6ec8f91db56fe2" - integrity sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA== +"@typescript-eslint/eslint-plugin@^5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.8.tgz#1e7a3e5318ece22251dfbc5c9c6feeb4793cc509" + integrity sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.7" - "@typescript-eslint/type-utils" "5.59.7" - "@typescript-eslint/utils" "5.59.7" + "@typescript-eslint/scope-manager" "5.59.8" + "@typescript-eslint/type-utils" "5.59.8" + "@typescript-eslint/utils" "5.59.8" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -2460,31 +2455,31 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.7.tgz#02682554d7c1028b89aa44a48bf598db33048caa" - integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ== +"@typescript-eslint/parser@^5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.8.tgz#60cbb00671d86cf746044ab797900b1448188567" + integrity sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw== dependencies: - "@typescript-eslint/scope-manager" "5.59.7" - "@typescript-eslint/types" "5.59.7" - "@typescript-eslint/typescript-estree" "5.59.7" + "@typescript-eslint/scope-manager" "5.59.8" + "@typescript-eslint/types" "5.59.8" + "@typescript-eslint/typescript-estree" "5.59.8" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz#0243f41f9066f3339d2f06d7f72d6c16a16769e2" - integrity sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ== +"@typescript-eslint/scope-manager@5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz#ff4ad4fec6433647b817c4a7d4b4165d18ea2fa8" + integrity sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig== dependencies: - "@typescript-eslint/types" "5.59.7" - "@typescript-eslint/visitor-keys" "5.59.7" + "@typescript-eslint/types" "5.59.8" + "@typescript-eslint/visitor-keys" "5.59.8" -"@typescript-eslint/type-utils@5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz#89c97291371b59eb18a68039857c829776f1426d" - integrity sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ== +"@typescript-eslint/type-utils@5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.8.tgz#aa6c029a9d7706d26bbd25eb4666398781df6ea2" + integrity sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA== dependencies: - "@typescript-eslint/typescript-estree" "5.59.7" - "@typescript-eslint/utils" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.8" + "@typescript-eslint/utils" "5.59.8" debug "^4.3.4" tsutils "^3.21.0" @@ -2493,10 +2488,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== -"@typescript-eslint/types@5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.7.tgz#6f4857203fceee91d0034ccc30512d2939000742" - integrity sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A== +"@typescript-eslint/types@5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.8.tgz#212e54414733618f5d0fd50b2da2717f630aebf8" + integrity sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w== "@typescript-eslint/typescript-estree@5.59.0": version "5.59.0" @@ -2511,30 +2506,30 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz#b887acbd4b58e654829c94860dbff4ac55c5cff8" - integrity sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ== +"@typescript-eslint/typescript-estree@5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz#801a7b1766481629481b3b0878148bd7a1f345d7" + integrity sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg== dependencies: - "@typescript-eslint/types" "5.59.7" - "@typescript-eslint/visitor-keys" "5.59.7" + "@typescript-eslint/types" "5.59.8" + "@typescript-eslint/visitor-keys" "5.59.8" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.7.tgz#7adf068b136deae54abd9a66ba5a8780d2d0f898" - integrity sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ== +"@typescript-eslint/utils@5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.8.tgz#34d129f35a2134c67fdaf024941e8f96050dca2b" + integrity sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.7" - "@typescript-eslint/types" "5.59.7" - "@typescript-eslint/typescript-estree" "5.59.7" + "@typescript-eslint/scope-manager" "5.59.8" + "@typescript-eslint/types" "5.59.8" + "@typescript-eslint/typescript-estree" "5.59.8" eslint-scope "^5.1.1" semver "^7.3.7" @@ -2546,12 +2541,12 @@ "@typescript-eslint/types" "5.59.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.59.7": - version "5.59.7" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz#09c36eaf268086b4fbb5eb9dc5199391b6485fc5" - integrity sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ== +"@typescript-eslint/visitor-keys@5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz#aa6a7ef862add919401470c09e1609392ef3cc40" + integrity sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ== dependencies: - "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/types" "5.59.8" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.9.0": @@ -7477,10 +7472,10 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsdom@^22.0.0: - version "22.0.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.0.0.tgz#3295c6992c70089c4b8f5cf060489fddf7ee9816" - integrity sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw== +jsdom@^22.1.0: + version "22.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8" + integrity sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw== dependencies: abab "^2.0.6" cssstyle "^3.0.0" @@ -9263,10 +9258,10 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.15, postcss@^8.4.23: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== +postcss@^8.2.15, postcss@^8.4.23, postcss@^8.4.24: + version "8.4.24" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df" + integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== dependencies: nanoid "^3.3.6" picocolors "^1.0.0"