~cytrogen/masto-fe

d77fbbed73e501f414f42f177fd63f2f527ed068 — Claire 2 years ago f89f3a8 + f877aa9
Merge commit 'f877aa9d70d0d600961989b8e97c0e0ce3ac1db6' into glitch-soc/merge-upstream

Conflicts:
- `.github/dependabot.yml`:
  Upstream made changes, but we had removed it.
  Discarded upstream changes.
- `.rubocop_todo.yml`:
  Upstream regenerated the file, we had some glitch-soc-specific ignores.
- `app/models/account_statuses_filter.rb`:
  Minor upstream code style change where glitch-soc had slightly different code
  due to handling of local-only posts.
  Updated to match upstream's code style.
- `app/models/status.rb`:
  Upstream moved ActiveRecord callback definitions, glitch-soc had an extra one.
  Moved the definitions as upstream did.
- `app/services/backup_service.rb`:
  Upstream rewrote a lot of the backup service, glitch-soc had changes because
  of exporting local-only posts.
  Took upstream changes and added back code to deal with local-only posts.
- `config/routes.rb`:
  Upstream split the file into different files, while glitch-soc had a few
  extra routes.
  Extra routes added to `config/routes/settings.rb`, `config/routes/api.rb`
  and `config/routes/admin.rb`
- `db/schema.rb`:
  Upstream has new migrations, while glitch-soc had an extra migration.
  Updated the expected serial number to match upstream's.
- `lib/mastodon/version.rb`:
  Upstream added support to set version tags from environment variables, while
  glitch-soc has an extra `+glitch` tag.
  Changed the code to support upstream's feature but prepending a `+glitch`.
- `spec/lib/activitypub/activity/create_spec.rb`:
  Minor code style change upstream, while glitch-soc has extra tests due to
  `directMessage` handling.
  Applied upstream's changes while keeping glitch-soc's extra tests.
- `spec/models/concerns/account_interactions_spec.rb`:
  Minor code style change upstream, while glitch-soc has extra tests.
  Applied upstream's changes while keeping glitch-soc's extra tests.
428 files changed, 6088 insertions(+), 3273 deletions(-)

M .github/workflows/build-image.yml
M .github/workflows/build-nightly.yml
M .github/workflows/lint-js.yml
M .github/workflows/test-ruby.yml
M .profile
M .rubocop.yml
M .rubocop_todo.yml
M Aptfile
M Dockerfile
M Gemfile
M Gemfile.lock
M app/controllers/admin/domain_blocks_controller.rb
M app/controllers/api/v1/media_controller.rb
M app/controllers/api/v2/media_controller.rb
M app/controllers/authorize_interactions_controller.rb
M app/controllers/concerns/signature_verification.rb
M app/controllers/intents_controller.rb
M app/controllers/media_proxy_controller.rb
M app/controllers/settings/exports_controller.rb
M app/controllers/settings/imports_controller.rb
M app/controllers/settings/preferences/appearance_controller.rb
R app/controllers/settings/{preferences_controller => preferences/base_controller}.rb
M app/controllers/settings/preferences/notifications_controller.rb
M app/controllers/settings/preferences/other_controller.rb
M app/controllers/well_known/webfinger_controller.rb
M app/helpers/application_helper.rb
D app/javascript/mastodon/actions/app.js
A app/javascript/mastodon/actions/app.ts
R app/javascript/mastodon/{blurhash.js => blurhash.ts}
R app/javascript/mastodon/{compare_id.js => compare_id.ts}
R app/javascript/mastodon/components/{blurhash.jsx => blurhash.tsx}
M app/javascript/mastodon/components/column_back_button.jsx
R app/javascript/mastodon/components/{icon_button.jsx => icon_button.tsx}
M app/javascript/mastodon/components/media_gallery.jsx
M app/javascript/mastodon/components/picture_in_picture_placeholder.jsx
M app/javascript/mastodon/components/status.jsx
M app/javascript/mastodon/containers/compose_container.jsx
M app/javascript/mastodon/containers/mastodon.jsx
M app/javascript/mastodon/features/audio/index.jsx
M app/javascript/mastodon/features/status/components/card.jsx
M app/javascript/mastodon/features/ui/index.jsx
M app/javascript/mastodon/features/video/index.jsx
R app/javascript/mastodon/{is_mobile.js => is_mobile.ts}
M app/javascript/mastodon/main.jsx
R app/javascript/mastodon/{permissions.js => permissions.ts}
M app/javascript/mastodon/reducers/meta.js
D app/javascript/mastodon/reducers/missed_updates.js
A app/javascript/mastodon/reducers/missed_updates.ts
M app/javascript/mastodon/reducers/notifications.js
R app/javascript/mastodon/{scroll.js => scroll.ts}
M app/javascript/mastodon/store/configureStore.js
R app/javascript/mastodon/utils/{base64.js => base64.ts}
R app/javascript/mastodon/utils/{filters.js => filters.ts}
R app/javascript/mastodon/utils/{hashtags.js => hashtags.ts}
R app/javascript/mastodon/utils/{numbers.js => numbers.ts}
M app/javascript/styles/mastodon/components.scss
M app/javascript/types/resources.ts
A app/javascript/types/util.ts
M app/lib/activity_tracker.rb
M app/lib/activitypub/activity/announce.rb
M app/lib/activitypub/activity/create.rb
M app/lib/activitypub/activity/delete.rb
M app/lib/activitypub/case_transform.rb
M app/lib/feed_manager.rb
M app/lib/importer/accounts_index_importer.rb
M app/lib/importer/tags_index_importer.rb
M app/lib/permalink_redirector.rb
A app/lib/vacuum/imports_vacuum.rb
M app/lib/webfinger_resource.rb
M app/models/account.rb
M app/models/account_conversation.rb
M app/models/account_migration.rb
M app/models/account_statuses_filter.rb
M app/models/account_suggestions/source.rb
M app/models/admin/appeal_filter.rb
M app/models/admin/status_filter.rb
M app/models/announcement_reaction.rb
M app/models/block.rb
A app/models/bulk_import.rb
A app/models/bulk_import_row.rb
M app/models/concerns/account_associations.rb
M app/models/concerns/account_interactions.rb
A app/models/concerns/account_search.rb
M app/models/concerns/lockable.rb
A app/models/concerns/status_safe_reblog_insert.rb
M app/models/follow_request.rb
A app/models/form/import.rb
M app/models/import.rb
M app/models/list_account.rb
M app/models/media_attachment.rb
M app/models/relationship_filter.rb
M app/models/session_activation.rb
M app/models/status.rb
M app/models/trends/history.rb
M app/models/trends/preview_card_filter.rb
M app/models/trends/status_filter.rb
M app/presenters/status_relationships_presenter.rb
M app/services/activitypub/fetch_remote_actor_service.rb
M app/services/activitypub/fetch_remote_status_service.rb
M app/services/activitypub/process_account_service.rb
M app/services/activitypub/process_status_update_service.rb
M app/services/backup_service.rb
A app/services/bulk_import_row_service.rb
A app/services/bulk_import_service.rb
M app/services/fetch_link_card_service.rb
M app/services/fetch_resource_service.rb
M app/services/follow_migration_service.rb
M app/services/import_service.rb
M app/services/remove_status_service.rb
M app/services/resolve_account_service.rb
M app/services/suspend_account_service.rb
M app/services/tag_search_service.rb
M app/services/unfollow_service.rb
M app/services/unsuspend_account_service.rb
M app/services/vote_service.rb
M app/validators/email_mx_validator.rb
D app/validators/import_validator.rb
M app/validators/vote_validator.rb
A app/views/settings/imports/index.html.haml
M app/views/settings/imports/show.html.haml
A app/workers/bulk_import_worker.rb
M app/workers/distribution_worker.rb
M app/workers/import/relationship_worker.rb
A app/workers/import/row_worker.rb
M app/workers/import_worker.rb
M app/workers/move_worker.rb
M app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb
M app/workers/scheduler/vacuum_scheduler.rb
M babel.config.js
M config/deploy.rb
M config/environments/development.rb
M config/environments/test.rb
M config/i18n-tasks.yml
M config/initializers/paperclip.rb
M config/locales/en.yml
M config/navigation.rb
M config/routes.rb
A config/routes/admin.rb
A config/routes/api.rb
A config/routes/settings.rb
A db/migrate/20230330135507_create_bulk_imports.rb
A db/migrate/20230330140036_create_bulk_import_rows.rb
A db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb
M db/schema.rb
M lib/mastodon/accounts_cli.rb
M lib/mastodon/emoji_cli.rb
M lib/mastodon/maintenance_cli.rb
M lib/mastodon/media_cli.rb
M lib/mastodon/version.rb
M lib/paperclip/color_extractor.rb
M lib/terrapin/multi_pipe_extensions.rb
M package.json
M spec/config/initializers/rack_attack_spec.rb
M spec/controllers/about_controller_spec.rb
M spec/controllers/accounts_controller_spec.rb
M spec/controllers/activitypub/collections_controller_spec.rb
M spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
M spec/controllers/activitypub/inboxes_controller_spec.rb
M spec/controllers/activitypub/outboxes_controller_spec.rb
M spec/controllers/activitypub/replies_controller_spec.rb
M spec/controllers/admin/account_moderation_notes_controller_spec.rb
M spec/controllers/admin/accounts_controller_spec.rb
M spec/controllers/admin/action_logs_controller_spec.rb
M spec/controllers/admin/base_controller_spec.rb
M spec/controllers/admin/change_emails_controller_spec.rb
M spec/controllers/admin/confirmations_controller_spec.rb
M spec/controllers/admin/dashboard_controller_spec.rb
M spec/controllers/admin/disputes/appeals_controller_spec.rb
M spec/controllers/admin/domain_allows_controller_spec.rb
M spec/controllers/admin/domain_blocks_controller_spec.rb
M spec/controllers/admin/email_domain_blocks_controller_spec.rb
M spec/controllers/admin/export_domain_allows_controller_spec.rb
M spec/controllers/admin/export_domain_blocks_controller_spec.rb
M spec/controllers/admin/instances_controller_spec.rb
M spec/controllers/admin/reports/actions_controller_spec.rb
M spec/controllers/admin/settings/branding_controller_spec.rb
M spec/controllers/admin/statuses_controller_spec.rb
M spec/controllers/admin/tags_controller_spec.rb
M spec/controllers/api/base_controller_spec.rb
M spec/controllers/api/oembed_controller_spec.rb
M spec/controllers/api/v1/accounts/pins_controller_spec.rb
M spec/controllers/api/v1/accounts/relationships_controller_spec.rb
M spec/controllers/api/v1/accounts/search_controller_spec.rb
M spec/controllers/api/v1/accounts_controller_spec.rb
M spec/controllers/api/v1/admin/account_actions_controller_spec.rb
M spec/controllers/api/v1/admin/accounts_controller_spec.rb
M spec/controllers/api/v1/admin/domain_allows_controller_spec.rb
M spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb
M spec/controllers/api/v1/admin/reports_controller_spec.rb
M spec/controllers/api/v1/announcements/reactions_controller_spec.rb
M spec/controllers/api/v1/announcements_controller_spec.rb
M spec/controllers/api/v1/apps_controller_spec.rb
M spec/controllers/api/v1/blocks_controller_spec.rb
M spec/controllers/api/v1/bookmarks_controller_spec.rb
M spec/controllers/api/v1/conversations_controller_spec.rb
M spec/controllers/api/v1/custom_emojis_controller_spec.rb
M spec/controllers/api/v1/domain_blocks_controller_spec.rb
M spec/controllers/api/v1/emails/confirmations_controller_spec.rb
M spec/controllers/api/v1/endorsements_controller_spec.rb
M spec/controllers/api/v1/favourites_controller_spec.rb
M spec/controllers/api/v1/filters_controller_spec.rb
M spec/controllers/api/v1/follow_requests_controller_spec.rb
M spec/controllers/api/v1/followed_tags_controller_spec.rb
M spec/controllers/api/v1/instances/activity_controller_spec.rb
M spec/controllers/api/v1/instances/peers_controller_spec.rb
M spec/controllers/api/v1/instances_controller_spec.rb
M spec/controllers/api/v1/lists/accounts_controller_spec.rb
M spec/controllers/api/v1/lists_controller_spec.rb
M spec/controllers/api/v1/markers_controller_spec.rb
M spec/controllers/api/v1/media_controller_spec.rb
M spec/controllers/api/v1/mutes_controller_spec.rb
M spec/controllers/api/v1/notifications_controller_spec.rb
M spec/controllers/api/v1/polls/votes_controller_spec.rb
M spec/controllers/api/v1/polls_controller_spec.rb
M spec/controllers/api/v1/reports_controller_spec.rb
M spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
M spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
M spec/controllers/api/v1/statuses_controller_spec.rb
M spec/controllers/api/v1/suggestions_controller_spec.rb
M spec/controllers/api/v1/tags_controller_spec.rb
M spec/controllers/api/v1/trends/tags_controller_spec.rb
M spec/controllers/api/v2/admin/accounts_controller_spec.rb
M spec/controllers/api/v2/filters/keywords_controller_spec.rb
M spec/controllers/api/v2/filters/statuses_controller_spec.rb
M spec/controllers/api/v2/filters_controller_spec.rb
M spec/controllers/api/v2/search_controller_spec.rb
M spec/controllers/api/web/embeds_controller_spec.rb
M spec/controllers/application_controller_spec.rb
M spec/controllers/auth/challenges_controller_spec.rb
M spec/controllers/auth/confirmations_controller_spec.rb
M spec/controllers/auth/passwords_controller_spec.rb
M spec/controllers/auth/registrations_controller_spec.rb
M spec/controllers/auth/sessions_controller_spec.rb
M spec/controllers/concerns/account_controller_concern_spec.rb
M spec/controllers/concerns/accountable_concern_spec.rb
M spec/controllers/concerns/cache_concern_spec.rb
M spec/controllers/concerns/challengable_concern_spec.rb
M spec/controllers/concerns/export_controller_concern_spec.rb
M spec/controllers/concerns/localized_spec.rb
M spec/controllers/concerns/rate_limit_headers_spec.rb
M spec/controllers/concerns/signature_verification_spec.rb
M spec/controllers/concerns/user_tracking_concern_spec.rb
M spec/controllers/disputes/appeals_controller_spec.rb
M spec/controllers/disputes/strikes_controller_spec.rb
M spec/controllers/emojis_controller_spec.rb
M spec/controllers/follower_accounts_controller_spec.rb
M spec/controllers/following_accounts_controller_spec.rb
M spec/controllers/home_controller_spec.rb
M spec/controllers/instance_actors_controller_spec.rb
M spec/controllers/intents_controller_spec.rb
M spec/controllers/oauth/authorizations_controller_spec.rb
M spec/controllers/oauth/tokens_controller_spec.rb
M spec/controllers/settings/applications_controller_spec.rb
M spec/controllers/settings/imports_controller_spec.rb
M spec/controllers/settings/preferences/appearance_controller_spec.rb
A spec/controllers/settings/preferences/base_controller_spec.rb
M spec/controllers/settings/profiles_controller_spec.rb
M spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb
M spec/controllers/statuses_cleanup_controller_spec.rb
M spec/controllers/statuses_controller_spec.rb
M spec/controllers/tags_controller_spec.rb
M spec/controllers/well_known/host_meta_controller_spec.rb
M spec/controllers/well_known/nodeinfo_controller_spec.rb
M spec/controllers/well_known/webfinger_controller_spec.rb
A spec/fabricators/bulk_import_fabricator.rb
A spec/fabricators/bulk_import_row_fabricator.rb
A spec/fixtures/files/empty.csv
A spec/fixtures/files/following_accounts.csv
A spec/fixtures/files/muted_accounts.csv
M spec/helpers/accounts_helper_spec.rb
M spec/helpers/admin/account_moderation_notes_helper_spec.rb
M spec/helpers/admin/action_logs_helper_spec.rb
M spec/helpers/application_helper_spec.rb
M spec/helpers/flashes_helper_spec.rb
M spec/helpers/formatting_helper_spec.rb
M spec/helpers/home_helper_spec.rb
M spec/helpers/jsonld_helper_spec.rb
M spec/helpers/routing_helper_spec.rb
M spec/lib/activitypub/activity/accept_spec.rb
M spec/lib/activitypub/activity/announce_spec.rb
M spec/lib/activitypub/activity/create_spec.rb
M spec/lib/activitypub/activity/follow_spec.rb
M spec/lib/activitypub/activity/reject_spec.rb
M spec/lib/activitypub/adapter_spec.rb
M spec/lib/connection_pool/shared_connection_pool_spec.rb
M spec/lib/connection_pool/shared_timed_stack_spec.rb
M spec/lib/emoji_formatter_spec.rb
M spec/lib/entity_cache_spec.rb
M spec/lib/feed_manager_spec.rb
M spec/lib/html_aware_formatter_spec.rb
M spec/lib/link_details_extractor_spec.rb
A spec/lib/mastodon/settings_cli_spec.rb
M spec/lib/ostatus/tag_manager_spec.rb
M spec/lib/request_pool_spec.rb
M spec/lib/scope_transformer_spec.rb
M spec/lib/status_cache_hydrator_spec.rb
M spec/lib/status_reach_finder_spec.rb
M spec/lib/text_formatter_spec.rb
A spec/lib/vacuum/imports_vacuum_spec.rb
M spec/mailers/admin_mailer_spec.rb
M spec/mailers/notification_mailer_spec.rb
M spec/mailers/user_mailer_spec.rb
M spec/models/account/field_spec.rb
M spec/models/account_alias_spec.rb
M spec/models/account_conversation_spec.rb
M spec/models/account_deletion_request_spec.rb
M spec/models/account_domain_block_spec.rb
M spec/models/account_migration_spec.rb
M spec/models/account_moderation_note_spec.rb
M spec/models/account_spec.rb
M spec/models/account_statuses_cleanup_policy_spec.rb
M spec/models/admin/account_action_spec.rb
M spec/models/admin/action_log_spec.rb
M spec/models/announcement_mute_spec.rb
M spec/models/announcement_reaction_spec.rb
M spec/models/announcement_spec.rb
M spec/models/backup_spec.rb
M spec/models/block_spec.rb
M spec/models/canonical_email_block_spec.rb
M spec/models/concerns/account_interactions_spec.rb
M spec/models/concerns/remotable_spec.rb
M spec/models/conversation_mute_spec.rb
M spec/models/conversation_spec.rb
M spec/models/custom_emoji_filter_spec.rb
M spec/models/custom_emoji_spec.rb
M spec/models/custom_filter_keyword_spec.rb
M spec/models/custom_filter_spec.rb
M spec/models/device_spec.rb
M spec/models/domain_block_spec.rb
M spec/models/email_domain_block_spec.rb
M spec/models/encrypted_message_spec.rb
M spec/models/export_spec.rb
M spec/models/favourite_spec.rb
M spec/models/featured_tag_spec.rb
M spec/models/follow_recommendation_suppression_spec.rb
M spec/models/follow_request_spec.rb
M spec/models/follow_spec.rb
A spec/models/form/import_spec.rb
M spec/models/home_feed_spec.rb
M spec/models/identity_spec.rb
M spec/models/import_spec.rb
M spec/models/invite_spec.rb
M spec/models/list_account_spec.rb
M spec/models/list_spec.rb
M spec/models/login_activity_spec.rb
M spec/models/media_attachment_spec.rb
M spec/models/mention_spec.rb
M spec/models/mute_spec.rb
M spec/models/notification_spec.rb
M spec/models/poll_vote_spec.rb
M spec/models/preview_card_spec.rb
M spec/models/preview_card_trend_spec.rb
M spec/models/public_feed_spec.rb
M spec/models/relay_spec.rb
M spec/models/remote_follow_spec.rb
M spec/models/report_spec.rb
M spec/models/scheduled_status_spec.rb
M spec/models/session_activation_spec.rb
M spec/models/setting_spec.rb
M spec/models/site_upload_spec.rb
M spec/models/status_pin_spec.rb
M spec/models/status_spec.rb
M spec/models/status_stat_spec.rb
M spec/models/status_trend_spec.rb
M spec/models/system_key_spec.rb
M spec/models/tag_follow_spec.rb
M spec/models/unavailable_domain_spec.rb
M spec/models/user_invite_request_spec.rb
M spec/models/user_role_spec.rb
M spec/models/user_spec.rb
M spec/models/web/push_subscription_spec.rb
M spec/models/web/setting_spec.rb
M spec/models/webauthn_credentials_spec.rb
M spec/models/webhook_spec.rb
M spec/policies/account_moderation_note_policy_spec.rb
M spec/policies/account_policy_spec.rb
M spec/policies/backup_policy_spec.rb
M spec/policies/custom_emoji_policy_spec.rb
M spec/policies/domain_block_policy_spec.rb
M spec/policies/email_domain_block_policy_spec.rb
M spec/policies/instance_policy_spec.rb
M spec/policies/invite_policy_spec.rb
M spec/policies/relay_policy_spec.rb
M spec/policies/report_note_policy_spec.rb
M spec/policies/report_policy_spec.rb
M spec/policies/settings_policy_spec.rb
M spec/policies/tag_policy_spec.rb
M spec/policies/user_policy_spec.rb
M spec/presenters/account_relationships_presenter_spec.rb
M spec/presenters/status_relationships_presenter_spec.rb
M spec/rails_helper.rb
A spec/requests/cache_spec.rb
M spec/requests/link_headers_spec.rb
M spec/services/account_search_service_spec.rb
M spec/services/account_statuses_cleanup_service_spec.rb
M spec/services/activitypub/fetch_remote_status_service_spec.rb
M spec/services/activitypub/process_account_service_spec.rb
M spec/services/activitypub/process_status_update_service_spec.rb
A spec/services/backup_service_spec.rb
A spec/services/bulk_import_row_service_spec.rb
A spec/services/bulk_import_service_spec.rb
M spec/services/fetch_link_card_service_spec.rb
M spec/services/fetch_oembed_service_spec.rb
M spec/services/fetch_remote_status_service_spec.rb
M spec/services/follow_service_spec.rb
M spec/services/import_service_spec.rb
M spec/services/notify_service_spec.rb
M spec/services/process_mentions_service_spec.rb
M spec/services/reblog_service_spec.rb
M spec/services/report_service_spec.rb
M spec/services/resolve_account_service_spec.rb
M spec/services/resolve_url_service_spec.rb
M spec/services/search_service_spec.rb
M spec/services/unallow_domain_service_spec.rb
M spec/services/verify_link_service_spec.rb
M spec/spec_helper.rb
M spec/validators/disallowed_hashtags_validator_spec.rb
M spec/validators/email_mx_validator_spec.rb
M spec/validators/follow_limit_validator_spec.rb
M spec/validators/poll_validator_spec.rb
M spec/validators/status_pin_validator_spec.rb
M spec/validators/unreserved_username_validator_spec.rb
M spec/validators/url_validator_spec.rb
A spec/workers/bulk_import_worker_spec.rb
A spec/workers/import/row_worker_spec.rb
M spec/workers/move_worker_spec.rb
M spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb
M yarn.lock
M .github/workflows/build-image.yml => .github/workflows/build-image.yml +7 -0
@@ 43,9 43,16 @@ jobs:
            type=edge,branch=main
            type=sha,prefix=,format=long

      - name: Generate version suffix
        id: version_vars
        if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main'
        run: |
          echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT

      - uses: docker/build-push-action@v4
        with:
          context: .
          build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }}
          platforms: linux/amd64,linux/arm64
          provenance: false
          builder: ${{ steps.buildx.outputs.name }}

M .github/workflows/build-nightly.yml => .github/workflows/build-nightly.yml +6 -0
@@ 41,9 41,15 @@ jobs:
          labels: |
            org.opencontainers.image.description=Nightly build image used for testing purposes

      - name: Generate version suffix
        id: version_vars
        run: |
          echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT

      - uses: docker/build-push-action@v4
        with:
          context: .
          build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }}
          platforms: linux/amd64,linux/arm64
          provenance: false
          builder: ${{ steps.buildx.outputs.name }}

M .github/workflows/lint-js.yml => .github/workflows/lint-js.yml +1 -1
@@ 48,7 48,7 @@ jobs:
        run: yarn --frozen-lockfile

      - name: ESLint
        run: yarn test:lint:js
        run: yarn test:lint:js --max-warnings 0

      - name: Typecheck
        run: yarn test:typecheck

M .github/workflows/test-ruby.yml => .github/workflows/test-ruby.yml +12 -3
@@ 9,7 9,6 @@ on:
env:
  BUNDLE_CLEAN: true
  BUNDLE_FROZEN: true
  BUNDLE_WITHOUT: 'development production'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}


@@ 19,8 18,17 @@ jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      fail-fast: true
      matrix:
        mode:
          - production
          - test
    env:
      RAILS_ENV: test
      RAILS_ENV: ${{ matrix.mode }}
      BUNDLE_WITH: ${{ matrix.mode }}
      OTP_SECRET: precompile_placeholder
      SECRET_KEY_BASE: precompile_placeholder

    steps:
      - uses: actions/checkout@v3


@@ 50,6 58,7 @@ jobs:
          ./bin/rails assets:precompile

      - uses: actions/upload-artifact@v3
        if: matrix.mode == 'test'
        with:
          path: |-
            ./public/assets


@@ 97,7 106,7 @@ jobs:
      PAM_ENABLED: true
      PAM_DEFAULT_SERVICE: pam_test
      PAM_CONTROLLED_SERVICE: pam_test_controlled
      BUNDLE_WITH: 'pam_authentication'
      BUNDLE_WITH: 'pam_authentication test'
      CI_JOBS: ${{ matrix.ci_job }}/4

    strategy:

M .profile => .profile +1 -1
@@ 1,1 1,1 @@
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread

M .rubocop.yml => .rubocop.yml +8 -0
@@ 65,6 65,7 @@ Metrics/AbcSize:
Metrics/BlockLength:
  CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
  Exclude:
    - 'config/routes.rb'
    - 'lib/mastodon/*_cli.rb'
    - 'lib/tasks/*.rake'
    - 'app/models/concerns/account_associations.rb'


@@ 85,6 86,7 @@ Metrics/BlockLength:
    - 'config/initializers/simple_form.rb'
    - 'config/navigation.rb'
    - 'config/routes.rb'
    - 'config/routes/*.rb'
    - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb'
    - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb'
    - 'lib/paperclip/gif_transcoder.rb'


@@ 130,6 132,7 @@ Metrics/ClassLength:
    - 'app/services/activitypub/process_account_service.rb'
    - 'app/services/activitypub/process_status_update_service.rb'
    - 'app/services/backup_service.rb'
    - 'app/services/bulk_import_service.rb'
    - 'app/services/delete_account_service.rb'
    - 'app/services/fan_out_on_write_service.rb'
    - 'app/services/fetch_link_card_service.rb'


@@ 158,6 161,11 @@ Metrics/MethodLength:
Metrics/ModuleLength:
  CountAsOne: [array, heredoc]

# Reason: Prevailing style is argument file paths
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
Rails/FilePath:
  EnforcedStyle: arguments

# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus
Rails/HttpStatus:

M .rubocop_todo.yml => .rubocop_todo.yml +36 -552
@@ 22,13 22,6 @@ Layout/ArgumentAlignment:
    - 'config/initializers/session_store.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: empty_lines, no_empty_lines
Layout/EmptyLinesAroundBlockBody:
  Exclude:
    - 'config/routes.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
  Exclude:


@@ 106,28 99,6 @@ Lint/AmbiguousOperatorPrecedence:
  Exclude:
    - 'config/initializers/rack_attack.rb'

# Configuration parameters: AllowedMethods.
# AllowedMethods: enums
Lint/ConstantDefinitionInBlock:
  Exclude:
    - 'spec/controllers/api/base_controller_spec.rb'
    - 'spec/controllers/application_controller_spec.rb'
    - 'spec/controllers/concerns/accountable_concern_spec.rb'
    - 'spec/controllers/concerns/signature_verification_spec.rb'
    - 'spec/lib/activitypub/adapter_spec.rb'
    - 'spec/lib/connection_pool/shared_connection_pool_spec.rb'
    - 'spec/lib/connection_pool/shared_timed_stack_spec.rb'
    - 'spec/models/concerns/remotable_spec.rb'

# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches.
Lint/DuplicateBranch:
  Exclude:
    - 'app/lib/permalink_redirector.rb'
    - 'app/models/account_statuses_filter.rb'
    - 'app/validators/email_mx_validator.rb'
    - 'app/validators/vote_validator.rb'
    - 'lib/mastodon/maintenance_cli.rb'

# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
  Exclude:


@@ 168,11 139,6 @@ Lint/EmptyBlock:
    - 'spec/models/user_role_spec.rb'
    - 'spec/models/web/setting_spec.rb'

# Configuration parameters: AllowComments.
Lint/EmptyClass:
  Exclude:
    - 'spec/controllers/api/base_controller_spec.rb'

Lint/NonLocalExitFromIterator:
  Exclude:
    - 'app/helpers/jsonld_helper.rb'


@@ 228,6 194,12 @@ Metrics/AbcSize:
  Exclude:
    - 'app/serializers/initial_state_serializer.rb'

# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
# AllowedMethods: refine
Metrics/BlockLength:
  Exclude:
    - 'app/models/concerns/status_safe_reblog_insert.rb'

# Configuration parameters: CountBlocks, Max.
Metrics/BlockNesting:
  Exclude:


@@ 305,42 277,6 @@ Naming/VariableNumber:
    - 'spec/models/user_spec.rb'
    - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'

# Configuration parameters: MinSize.
Performance/CollectionLiteralInLoop:
  Exclude:
    - 'app/models/admin/appeal_filter.rb'
    - 'app/models/admin/status_filter.rb'
    - 'app/models/relationship_filter.rb'
    - 'app/models/trends/preview_card_filter.rb'
    - 'app/models/trends/status_filter.rb'
    - 'app/presenters/status_relationships_presenter.rb'
    - 'app/services/fetch_resource_service.rb'
    - 'app/services/suspend_account_service.rb'
    - 'app/services/unsuspend_account_service.rb'
    - 'config/deploy.rb'
    - 'lib/mastodon/media_cli.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/Count:
  Exclude:
    - 'app/lib/importer/accounts_index_importer.rb'
    - 'app/lib/importer/tags_index_importer.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeMultiline.
Performance/DeletePrefix:
  Exclude:
    - 'app/controllers/authorize_interactions_controller.rb'
    - 'app/controllers/concerns/signature_verification.rb'
    - 'app/controllers/intents_controller.rb'
    - 'app/lib/activitypub/case_transform.rb'
    - 'app/lib/permalink_redirector.rb'
    - 'app/lib/webfinger_resource.rb'
    - 'app/services/activitypub/fetch_remote_actor_service.rb'
    - 'app/services/backup_service.rb'
    - 'app/services/resolve_account_service.rb'
    - 'app/services/tag_search_service.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/MapCompact:
  Exclude:


@@ 360,23 296,6 @@ Performance/MapCompact:
    - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
    - 'spec/presenters/status_relationships_presenter_spec.rb'

Performance/MethodObjectAsBlock:
  Exclude:
    - 'app/models/account_suggestions/source.rb'
    - 'spec/models/export_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowRegexpMatch.
Performance/RedundantEqualityComparisonBlock:
  Exclude:
    - 'spec/requests/link_headers_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
  Exclude:
    - 'config/initializers/paperclip.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeMultiline.
Performance/StartWith:


@@ 384,23 303,6 @@ Performance/StartWith:
    - 'app/lib/extractor.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: OnlySumOrWithInitialValue.
Performance/Sum:
  Exclude:
    - 'app/lib/activity_tracker.rb'
    - 'app/models/trends/history.rb'
    - 'lib/paperclip/color_extractor.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/TimesMap:
  Exclude:
    - 'spec/controllers/api/v1/blocks_controller_spec.rb'
    - 'spec/controllers/api/v1/mutes_controller_spec.rb'
    - 'spec/lib/feed_manager_spec.rb'
    - 'spec/lib/request_pool_spec.rb'
    - 'spec/models/account_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/UnfreezeString:
  Exclude:
    - 'app/lib/rss/builder.rb'


@@ 428,116 330,6 @@ RSpec/AnyInstance:
    - 'spec/workers/activitypub/delivery_worker_spec.rb'
    - 'spec/workers/web/push_notification_worker_spec.rb'

# Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without
RSpec/ContextWording:
  Exclude:
    - 'spec/config/initializers/rack_attack_spec.rb'
    - 'spec/controllers/accounts_controller_spec.rb'
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
    - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
    - 'spec/controllers/admin/domain_blocks_controller_spec.rb'
    - 'spec/controllers/admin/reports/actions_controller_spec.rb'
    - 'spec/controllers/admin/statuses_controller_spec.rb'
    - 'spec/controllers/api/v1/accounts/relationships_controller_spec.rb'
    - 'spec/controllers/api/v1/accounts_controller_spec.rb'
    - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
    - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb'
    - 'spec/controllers/api/v1/instances/activity_controller_spec.rb'
    - 'spec/controllers/api/v1/instances/peers_controller_spec.rb'
    - 'spec/controllers/api/v1/media_controller_spec.rb'
    - 'spec/controllers/api/v2/filters_controller_spec.rb'
    - 'spec/controllers/application_controller_spec.rb'
    - 'spec/controllers/auth/registrations_controller_spec.rb'
    - 'spec/controllers/auth/sessions_controller_spec.rb'
    - 'spec/controllers/concerns/cache_concern_spec.rb'
    - 'spec/controllers/concerns/challengable_concern_spec.rb'
    - 'spec/controllers/concerns/localized_spec.rb'
    - 'spec/controllers/concerns/rate_limit_headers_spec.rb'
    - 'spec/controllers/instance_actors_controller_spec.rb'
    - 'spec/controllers/settings/applications_controller_spec.rb'
    - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
    - 'spec/controllers/statuses_controller_spec.rb'
    - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb'
    - 'spec/helpers/jsonld_helper_spec.rb'
    - 'spec/helpers/routing_helper_spec.rb'
    - 'spec/lib/activitypub/activity/accept_spec.rb'
    - 'spec/lib/activitypub/activity/announce_spec.rb'
    - 'spec/lib/activitypub/activity/create_spec.rb'
    - 'spec/lib/activitypub/activity/follow_spec.rb'
    - 'spec/lib/activitypub/activity/reject_spec.rb'
    - 'spec/lib/advanced_text_formatter_spec.rb'
    - 'spec/lib/emoji_formatter_spec.rb'
    - 'spec/lib/entity_cache_spec.rb'
    - 'spec/lib/feed_manager_spec.rb'
    - 'spec/lib/html_aware_formatter_spec.rb'
    - 'spec/lib/link_details_extractor_spec.rb'
    - 'spec/lib/ostatus/tag_manager_spec.rb'
    - 'spec/lib/scope_transformer_spec.rb'
    - 'spec/lib/status_cache_hydrator_spec.rb'
    - 'spec/lib/status_reach_finder_spec.rb'
    - 'spec/lib/text_formatter_spec.rb'
    - 'spec/models/account/field_spec.rb'
    - 'spec/models/account_spec.rb'
    - 'spec/models/admin/account_action_spec.rb'
    - 'spec/models/concerns/account_interactions_spec.rb'
    - 'spec/models/concerns/remotable_spec.rb'
    - 'spec/models/custom_emoji_filter_spec.rb'
    - 'spec/models/custom_emoji_spec.rb'
    - 'spec/models/email_domain_block_spec.rb'
    - 'spec/models/media_attachment_spec.rb'
    - 'spec/models/notification_spec.rb'
    - 'spec/models/remote_follow_spec.rb'
    - 'spec/models/report_spec.rb'
    - 'spec/models/session_activation_spec.rb'
    - 'spec/models/setting_spec.rb'
    - 'spec/models/status_spec.rb'
    - 'spec/models/web/push_subscription_spec.rb'
    - 'spec/policies/account_moderation_note_policy_spec.rb'
    - 'spec/policies/account_policy_spec.rb'
    - 'spec/policies/backup_policy_spec.rb'
    - 'spec/policies/custom_emoji_policy_spec.rb'
    - 'spec/policies/domain_block_policy_spec.rb'
    - 'spec/policies/email_domain_block_policy_spec.rb'
    - 'spec/policies/instance_policy_spec.rb'
    - 'spec/policies/invite_policy_spec.rb'
    - 'spec/policies/relay_policy_spec.rb'
    - 'spec/policies/report_note_policy_spec.rb'
    - 'spec/policies/report_policy_spec.rb'
    - 'spec/policies/settings_policy_spec.rb'
    - 'spec/policies/tag_policy_spec.rb'
    - 'spec/policies/user_policy_spec.rb'
    - 'spec/presenters/account_relationships_presenter_spec.rb'
    - 'spec/presenters/status_relationships_presenter_spec.rb'
    - 'spec/services/account_search_service_spec.rb'
    - 'spec/services/account_statuses_cleanup_service_spec.rb'
    - 'spec/services/activitypub/fetch_remote_status_service_spec.rb'
    - 'spec/services/activitypub/process_account_service_spec.rb'
    - 'spec/services/activitypub/process_status_update_service_spec.rb'
    - 'spec/services/fetch_link_card_service_spec.rb'
    - 'spec/services/fetch_oembed_service_spec.rb'
    - 'spec/services/fetch_remote_status_service_spec.rb'
    - 'spec/services/follow_service_spec.rb'
    - 'spec/services/import_service_spec.rb'
    - 'spec/services/notify_service_spec.rb'
    - 'spec/services/process_mentions_service_spec.rb'
    - 'spec/services/reblog_service_spec.rb'
    - 'spec/services/report_service_spec.rb'
    - 'spec/services/resolve_account_service_spec.rb'
    - 'spec/services/resolve_url_service_spec.rb'
    - 'spec/services/search_service_spec.rb'
    - 'spec/services/unallow_domain_service_spec.rb'
    - 'spec/services/verify_link_service_spec.rb'
    - 'spec/validators/disallowed_hashtags_validator_spec.rb'
    - 'spec/validators/email_mx_validator_spec.rb'
    - 'spec/validators/follow_limit_validator_spec.rb'
    - 'spec/validators/poll_validator_spec.rb'
    - 'spec/validators/status_pin_validator_spec.rb'
    - 'spec/validators/unreserved_username_validator_spec.rb'
    - 'spec/validators/url_validator_spec.rb'
    - 'spec/workers/move_worker_spec.rb'
    - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SkipBlocks, EnforcedStyle.
# SupportedStyles: described_class, explicit


@@ 701,7 493,6 @@ RSpec/InstanceVariable:
    - 'spec/controllers/statuses_cleanup_controller_spec.rb'
    - 'spec/models/concerns/account_finder_concern_spec.rb'
    - 'spec/models/concerns/account_interactions_spec.rb'
    - 'spec/models/concerns/remotable_spec.rb'
    - 'spec/models/public_feed_spec.rb'
    - 'spec/serializers/activitypub/note_serializer_spec.rb'
    - 'spec/serializers/activitypub/update_poll_serializer_spec.rb'


@@ 709,17 500,6 @@ RSpec/InstanceVariable:
    - 'spec/services/search_service_spec.rb'
    - 'spec/services/unblock_domain_service_spec.rb'

RSpec/LeakyConstantDeclaration:
  Exclude:
    - 'spec/controllers/api/base_controller_spec.rb'
    - 'spec/controllers/application_controller_spec.rb'
    - 'spec/controllers/concerns/accountable_concern_spec.rb'
    - 'spec/controllers/concerns/signature_verification_spec.rb'
    - 'spec/lib/activitypub/adapter_spec.rb'
    - 'spec/lib/connection_pool/shared_connection_pool_spec.rb'
    - 'spec/lib/connection_pool/shared_timed_stack_spec.rb'
    - 'spec/models/concerns/remotable_spec.rb'

RSpec/LetSetup:
  Exclude:
    - 'spec/controllers/admin/accounts_controller_spec.rb'


@@ 745,6 525,7 @@ RSpec/LetSetup:
    - 'spec/controllers/following_accounts_controller_spec.rb'
    - 'spec/controllers/oauth/authorized_applications_controller_spec.rb'
    - 'spec/controllers/oauth/tokens_controller_spec.rb'
    - 'spec/controllers/settings/imports_controller_spec.rb'
    - 'spec/lib/activitypub/activity/delete_spec.rb'
    - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb'
    - 'spec/models/account_spec.rb'


@@ 759,6 540,7 @@ RSpec/LetSetup:
    - 'spec/services/activitypub/process_collection_service_spec.rb'
    - 'spec/services/batched_remove_status_service_spec.rb'
    - 'spec/services/block_domain_service_spec.rb'
    - 'spec/services/bulk_import_service_spec.rb'
    - 'spec/services/delete_account_service_spec.rb'
    - 'spec/services/import_service_spec.rb'
    - 'spec/services/notify_service_spec.rb'


@@ 831,17 613,6 @@ RSpec/MultipleExpectations:
RSpec/MultipleMemoizedHelpers:
  Max: 21

# This cop supports safe autocorrection (--autocorrect).
RSpec/MultipleSubjects:
  Exclude:
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
    - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
    - 'spec/controllers/activitypub/outboxes_controller_spec.rb'
    - 'spec/controllers/api/web/embeds_controller_spec.rb'
    - 'spec/controllers/emojis_controller_spec.rb'
    - 'spec/controllers/follower_accounts_controller_spec.rb'
    - 'spec/controllers/following_accounts_controller_spec.rb'

# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
  Max: 6


@@ 867,181 638,6 @@ RSpec/PredicateMatcher:
    - 'spec/models/user_spec.rb'
    - 'spec/services/post_status_service_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Inferences.
RSpec/Rails/InferredSpecType:
  Exclude:
    - 'spec/controllers/about_controller_spec.rb'
    - 'spec/controllers/accounts_controller_spec.rb'
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
    - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
    - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
    - 'spec/controllers/activitypub/outboxes_controller_spec.rb'
    - 'spec/controllers/activitypub/replies_controller_spec.rb'
    - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb'
    - 'spec/controllers/admin/accounts_controller_spec.rb'
    - 'spec/controllers/admin/action_logs_controller_spec.rb'
    - 'spec/controllers/admin/base_controller_spec.rb'
    - 'spec/controllers/admin/change_emails_controller_spec.rb'
    - 'spec/controllers/admin/confirmations_controller_spec.rb'
    - 'spec/controllers/admin/dashboard_controller_spec.rb'
    - 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
    - 'spec/controllers/admin/domain_allows_controller_spec.rb'
    - 'spec/controllers/admin/domain_blocks_controller_spec.rb'
    - 'spec/controllers/admin/email_domain_blocks_controller_spec.rb'
    - 'spec/controllers/admin/export_domain_allows_controller_spec.rb'
    - 'spec/controllers/admin/export_domain_blocks_controller_spec.rb'
    - 'spec/controllers/admin/instances_controller_spec.rb'
    - 'spec/controllers/admin/settings/branding_controller_spec.rb'
    - 'spec/controllers/admin/tags_controller_spec.rb'
    - 'spec/controllers/api/oembed_controller_spec.rb'
    - 'spec/controllers/api/v1/accounts/pins_controller_spec.rb'
    - 'spec/controllers/api/v1/accounts/search_controller_spec.rb'
    - 'spec/controllers/api/v1/accounts_controller_spec.rb'
    - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb'
    - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
    - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
    - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
    - 'spec/controllers/api/v1/admin/reports_controller_spec.rb'
    - 'spec/controllers/api/v1/announcements/reactions_controller_spec.rb'
    - 'spec/controllers/api/v1/announcements_controller_spec.rb'
    - 'spec/controllers/api/v1/apps_controller_spec.rb'
    - 'spec/controllers/api/v1/blocks_controller_spec.rb'
    - 'spec/controllers/api/v1/bookmarks_controller_spec.rb'
    - 'spec/controllers/api/v1/conversations_controller_spec.rb'
    - 'spec/controllers/api/v1/custom_emojis_controller_spec.rb'
    - 'spec/controllers/api/v1/domain_blocks_controller_spec.rb'
    - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb'
    - 'spec/controllers/api/v1/endorsements_controller_spec.rb'
    - 'spec/controllers/api/v1/favourites_controller_spec.rb'
    - 'spec/controllers/api/v1/filters_controller_spec.rb'
    - 'spec/controllers/api/v1/follow_requests_controller_spec.rb'
    - 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
    - 'spec/controllers/api/v1/instances/activity_controller_spec.rb'
    - 'spec/controllers/api/v1/instances/peers_controller_spec.rb'
    - 'spec/controllers/api/v1/instances_controller_spec.rb'
    - 'spec/controllers/api/v1/lists_controller_spec.rb'
    - 'spec/controllers/api/v1/markers_controller_spec.rb'
    - 'spec/controllers/api/v1/media_controller_spec.rb'
    - 'spec/controllers/api/v1/mutes_controller_spec.rb'
    - 'spec/controllers/api/v1/notifications_controller_spec.rb'
    - 'spec/controllers/api/v1/polls/votes_controller_spec.rb'
    - 'spec/controllers/api/v1/polls_controller_spec.rb'
    - 'spec/controllers/api/v1/reports_controller_spec.rb'
    - 'spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb'
    - 'spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb'
    - 'spec/controllers/api/v1/statuses_controller_spec.rb'
    - 'spec/controllers/api/v1/suggestions_controller_spec.rb'
    - 'spec/controllers/api/v1/tags_controller_spec.rb'
    - 'spec/controllers/api/v1/trends/tags_controller_spec.rb'
    - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
    - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
    - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
    - 'spec/controllers/api/v2/filters_controller_spec.rb'
    - 'spec/controllers/api/v2/search_controller_spec.rb'
    - 'spec/controllers/application_controller_spec.rb'
    - 'spec/controllers/auth/challenges_controller_spec.rb'
    - 'spec/controllers/auth/confirmations_controller_spec.rb'
    - 'spec/controllers/auth/passwords_controller_spec.rb'
    - 'spec/controllers/auth/registrations_controller_spec.rb'
    - 'spec/controllers/auth/sessions_controller_spec.rb'
    - 'spec/controllers/concerns/account_controller_concern_spec.rb'
    - 'spec/controllers/concerns/cache_concern_spec.rb'
    - 'spec/controllers/concerns/challengable_concern_spec.rb'
    - 'spec/controllers/concerns/export_controller_concern_spec.rb'
    - 'spec/controllers/concerns/localized_spec.rb'
    - 'spec/controllers/concerns/signature_verification_spec.rb'
    - 'spec/controllers/concerns/user_tracking_concern_spec.rb'
    - 'spec/controllers/disputes/appeals_controller_spec.rb'
    - 'spec/controllers/disputes/strikes_controller_spec.rb'
    - 'spec/controllers/home_controller_spec.rb'
    - 'spec/controllers/instance_actors_controller_spec.rb'
    - 'spec/controllers/intents_controller_spec.rb'
    - 'spec/controllers/oauth/authorizations_controller_spec.rb'
    - 'spec/controllers/oauth/tokens_controller_spec.rb'
    - 'spec/controllers/settings/imports_controller_spec.rb'
    - 'spec/controllers/settings/profiles_controller_spec.rb'
    - 'spec/controllers/statuses_cleanup_controller_spec.rb'
    - 'spec/controllers/tags_controller_spec.rb'
    - 'spec/controllers/well_known/host_meta_controller_spec.rb'
    - 'spec/controllers/well_known/nodeinfo_controller_spec.rb'
    - 'spec/controllers/well_known/webfinger_controller_spec.rb'
    - 'spec/helpers/accounts_helper_spec.rb'
    - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb'
    - 'spec/helpers/admin/action_logs_helper_spec.rb'
    - 'spec/helpers/flashes_helper_spec.rb'
    - 'spec/helpers/formatting_helper_spec.rb'
    - 'spec/helpers/home_helper_spec.rb'
    - 'spec/helpers/routing_helper_spec.rb'
    - 'spec/mailers/admin_mailer_spec.rb'
    - 'spec/mailers/notification_mailer_spec.rb'
    - 'spec/mailers/user_mailer_spec.rb'
    - 'spec/models/account/field_spec.rb'
    - 'spec/models/account_alias_spec.rb'
    - 'spec/models/account_conversation_spec.rb'
    - 'spec/models/account_deletion_request_spec.rb'
    - 'spec/models/account_domain_block_spec.rb'
    - 'spec/models/account_migration_spec.rb'
    - 'spec/models/account_moderation_note_spec.rb'
    - 'spec/models/account_spec.rb'
    - 'spec/models/account_statuses_cleanup_policy_spec.rb'
    - 'spec/models/admin/account_action_spec.rb'
    - 'spec/models/admin/action_log_spec.rb'
    - 'spec/models/announcement_mute_spec.rb'
    - 'spec/models/announcement_reaction_spec.rb'
    - 'spec/models/announcement_spec.rb'
    - 'spec/models/backup_spec.rb'
    - 'spec/models/block_spec.rb'
    - 'spec/models/canonical_email_block_spec.rb'
    - 'spec/models/conversation_mute_spec.rb'
    - 'spec/models/conversation_spec.rb'
    - 'spec/models/custom_emoji_spec.rb'
    - 'spec/models/custom_filter_keyword_spec.rb'
    - 'spec/models/custom_filter_spec.rb'
    - 'spec/models/device_spec.rb'
    - 'spec/models/domain_block_spec.rb'
    - 'spec/models/email_domain_block_spec.rb'
    - 'spec/models/encrypted_message_spec.rb'
    - 'spec/models/favourite_spec.rb'
    - 'spec/models/featured_tag_spec.rb'
    - 'spec/models/follow_recommendation_suppression_spec.rb'
    - 'spec/models/follow_request_spec.rb'
    - 'spec/models/follow_spec.rb'
    - 'spec/models/home_feed_spec.rb'
    - 'spec/models/identity_spec.rb'
    - 'spec/models/import_spec.rb'
    - 'spec/models/invite_spec.rb'
    - 'spec/models/list_account_spec.rb'
    - 'spec/models/list_spec.rb'
    - 'spec/models/login_activity_spec.rb'
    - 'spec/models/media_attachment_spec.rb'
    - 'spec/models/mention_spec.rb'
    - 'spec/models/mute_spec.rb'
    - 'spec/models/notification_spec.rb'
    - 'spec/models/poll_vote_spec.rb'
    - 'spec/models/preview_card_spec.rb'
    - 'spec/models/preview_card_trend_spec.rb'
    - 'spec/models/public_feed_spec.rb'
    - 'spec/models/relay_spec.rb'
    - 'spec/models/scheduled_status_spec.rb'
    - 'spec/models/session_activation_spec.rb'
    - 'spec/models/setting_spec.rb'
    - 'spec/models/site_upload_spec.rb'
    - 'spec/models/status_pin_spec.rb'
    - 'spec/models/status_spec.rb'
    - 'spec/models/status_stat_spec.rb'
    - 'spec/models/status_trend_spec.rb'
    - 'spec/models/system_key_spec.rb'
    - 'spec/models/tag_follow_spec.rb'
    - 'spec/models/unavailable_domain_spec.rb'
    - 'spec/models/user_invite_request_spec.rb'
    - 'spec/models/user_role_spec.rb'
    - 'spec/models/user_spec.rb'
    - 'spec/models/web/push_subscription_spec.rb'
    - 'spec/models/web/setting_spec.rb'
    - 'spec/models/webauthn_credentials_spec.rb'
    - 'spec/models/webhook_spec.rb'

RSpec/RepeatedExample:
  Exclude:
    - 'spec/policies/status_policy_spec.rb'


@@ 1120,7 716,6 @@ RSpec/VerifiedDoubles:
    - 'spec/controllers/api/web/embeds_controller_spec.rb'
    - 'spec/controllers/auth/sessions_controller_spec.rb'
    - 'spec/controllers/disputes/appeals_controller_spec.rb'
    - 'spec/controllers/settings/imports_controller_spec.rb'
    - 'spec/helpers/statuses_helper_spec.rb'
    - 'spec/lib/suspicious_sign_in_detector_spec.rb'
    - 'spec/models/account/field_spec.rb'


@@ 1148,19 743,6 @@ RSpec/VerifiedDoubles:
    - 'spec/workers/feed_insert_worker_spec.rb'
    - 'spec/workers/regeneration_worker_spec.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/ActiveRecordCallbacksOrder:
  Exclude:
    - 'app/models/account.rb'
    - 'app/models/account_conversation.rb'
    - 'app/models/announcement_reaction.rb'
    - 'app/models/block.rb'
    - 'app/models/media_attachment.rb'
    - 'app/models/session_activation.rb'
    - 'app/models/status.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController:
  Exclude:


@@ 1216,12 798,6 @@ Rails/CreateTableWithTimestamps:
    - 'db/migrate/20220824233535_create_status_trends.rb'
    - 'db/migrate/20221006061337_create_preview_card_trends.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Severity.
Rails/DeprecatedActiveModelErrorsMethods:
  Exclude:
    - 'lib/mastodon/accounts_cli.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Severity.
Rails/DuplicateAssociation:


@@ 1235,74 811,6 @@ Rails/Exit:
  Exclude:
    - 'config/boot.rb'

# Configuration parameters: EnforcedStyle.
# SupportedStyles: slashes, arguments
Rails/FilePath:
  Exclude:
    - 'app/lib/themes.rb'
    - 'app/models/setting.rb'
    - 'app/validators/reaction_validator.rb'
    - 'config/environments/test.rb'
    - 'config/initializers/locale.rb'
    - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb'
    - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb'
    - 'db/migrate/20171028221157_add_reblogs_to_follows.rb'
    - 'db/migrate/20171107143332_add_memorial_to_accounts.rb'
    - 'db/migrate/20171107143624_add_disabled_to_users.rb'
    - 'db/migrate/20171109012327_add_moderator_to_accounts.rb'
    - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb'
    - 'db/migrate/20180615122121_add_autofollow_to_invites.rb'
    - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb'
    - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb'
    - 'db/migrate/20181010141500_add_silent_to_mentions.rb'
    - 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb'
    - 'db/migrate/20181018205649_add_unread_to_account_conversations.rb'
    - 'db/migrate/20181127130500_identity_id_to_bigint.rb'
    - 'db/migrate/20181127165847_add_show_replies_to_lists.rb'
    - 'db/migrate/20190201012802_add_overwrite_to_imports.rb'
    - 'db/migrate/20190306145741_add_lock_version_to_polls.rb'
    - 'db/migrate/20190307234537_add_approved_to_users.rb'
    - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb'
    - 'db/migrate/20191212003415_increase_backup_size.rb'
    - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb'
    - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb'
    - 'db/migrate/20200917192924_add_notify_to_follows.rb'
    - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb'
    - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb'
    - 'db/migrate/20211231080958_add_category_to_reports.rb'
    - 'db/migrate/20220613110834_add_action_to_custom_filters.rb'
    - 'db/post_migrate/20220307083603_optimize_null_index_conversations_uri.rb'
    - 'db/post_migrate/20220310060545_optimize_null_index_statuses_in_reply_to_account_id.rb'
    - 'db/post_migrate/20220310060556_optimize_null_index_statuses_in_reply_to_id.rb'
    - 'db/post_migrate/20220310060614_optimize_null_index_media_attachments_scheduled_status_id.rb'
    - 'db/post_migrate/20220310060626_optimize_null_index_media_attachments_shortcode.rb'
    - 'db/post_migrate/20220310060641_optimize_null_index_users_reset_password_token.rb'
    - 'db/post_migrate/20220310060653_optimize_null_index_users_created_by_application_id.rb'
    - 'db/post_migrate/20220310060706_optimize_null_index_statuses_uri.rb'
    - 'db/post_migrate/20220310060722_optimize_null_index_accounts_moved_to_account_id.rb'
    - 'db/post_migrate/20220310060740_optimize_null_index_oauth_access_tokens_refresh_token.rb'
    - 'db/post_migrate/20220310060750_optimize_null_index_accounts_url.rb'
    - 'db/post_migrate/20220310060809_optimize_null_index_oauth_access_tokens_resource_owner_id.rb'
    - 'db/post_migrate/20220310060833_optimize_null_index_announcement_reactions_custom_emoji_id.rb'
    - 'db/post_migrate/20220310060854_optimize_null_index_appeals_approved_by_account_id.rb'
    - 'db/post_migrate/20220310060913_optimize_null_index_account_migrations_target_account_id.rb'
    - 'db/post_migrate/20220310060926_optimize_null_index_appeals_rejected_by_account_id.rb'
    - 'db/post_migrate/20220310060939_optimize_null_index_list_accounts_follow_id.rb'
    - 'db/post_migrate/20220310060959_optimize_null_index_web_push_subscriptions_access_token_id.rb'
    - 'db/post_migrate/20220613110802_remove_whole_word_from_custom_filters.rb'
    - 'db/post_migrate/20220613110903_remove_irreversible_from_custom_filters.rb'
    - 'db/post_migrate/20220617202502_migrate_roles.rb'
    - 'db/seeds.rb'
    - 'db/seeds/03_roles.rb'
    - 'lib/tasks/branding.rake'
    - 'lib/tasks/emojis.rake'
    - 'lib/tasks/repo.rake'
    - 'spec/controllers/admin/custom_emojis_controller_spec.rb'
    - 'spec/fabricators/custom_emoji_fabricator.rb'
    - 'spec/fabricators/site_upload_fabricator.rb'
    - 'spec/rails_helper.rb'
    - 'spec/spec_helper.rb'

# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasAndBelongsToMany:


@@ 1445,12 953,29 @@ Rails/SkipsModelValidations:
    - 'spec/services/follow_service_spec.rb'
    - 'spec/services/update_account_service_spec.rb'

Rails/TransactionExitStatement:
# Configuration parameters: Include.
# Include: db/**/*.rb
Rails/ThreeStateBooleanColumn:
  Exclude:
    - 'app/lib/activitypub/activity/announce.rb'
    - 'app/lib/activitypub/activity/create.rb'
    - 'app/lib/activitypub/activity/delete.rb'
    - 'app/services/activitypub/process_account_service.rb'
    - 'db/migrate/20160325130944_add_admin_to_users.rb'
    - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
    - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
    - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
    - 'db/migrate/20170209184350_add_reply_to_statuses.rb'
    - 'db/migrate/20170330163835_create_imports.rb'
    - 'db/migrate/20170905165803_add_local_to_statuses.rb'
    - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
    - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
    - 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
    - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
    - 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
    - 'db/migrate/20210609202149_create_login_activities.rb'
    - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
    - 'db/migrate/20211031031021_create_preview_card_providers.rb'
    - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
    - 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
    - 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
    - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'

# Configuration parameters: Include.
# Include: app/models/**/*.rb


@@ 1519,12 1044,6 @@ Style/CaseEquality:
  Exclude:
    - 'config/initializers/trusted_proxies.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: MinBranchesCount.
Style/CaseLikeIf:
  Exclude:
    - 'app/controllers/concerns/signature_verification.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: ==, equal?, eql?


@@ 1542,16 1061,10 @@ Style/CombinableLoops:
    - 'app/models/form/custom_emoji_batch.rb'
    - 'app/models/form/ip_block_batch.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Style/ConcatArrayLiterals:
  Exclude:
    - 'app/lib/feed_manager.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedVars.
Style/FetchEnvVar:
  Exclude:
    - 'app/helpers/application_helper.rb'
    - 'app/lib/redis_configuration.rb'
    - 'app/lib/translation_service.rb'
    - 'config/environments/development.rb'


@@ 2001,7 1514,6 @@ Style/GuardClause:
    - 'app/controllers/auth/passwords_controller.rb'
    - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb'
    - 'app/lib/activitypub/activity/block.rb'
    - 'app/lib/connection_pool/shared_connection_pool.rb'
    - 'app/lib/request.rb'
    - 'app/lib/request_pool.rb'
    - 'app/lib/webfinger.rb'


@@ 2036,7 1548,6 @@ Style/HashAsLastArrayItem:
  Exclude:
    - 'app/controllers/admin/statuses_controller.rb'
    - 'app/controllers/api/v1/statuses_controller.rb'
    - 'app/models/account.rb'
    - 'app/models/concerns/account_counters.rb'
    - 'app/models/concerns/status_threading_concern.rb'
    - 'app/models/status.rb'


@@ 2044,19 1555,6 @@ Style/HashAsLastArrayItem:
    - 'app/services/notify_service.rb'
    - 'db/migrate/20181024224956_migrate_account_conversations.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
# SupportedShorthandSyntax: always, never, either, consistent
Style/HashSyntax:
  Exclude:
    - 'app/helpers/application_helper.rb'
    - 'app/models/media_attachment.rb'
    - 'lib/terrapin/multi_pipe_extensions.rb'
    - 'spec/controllers/admin/reports/actions_controller_spec.rb'
    - 'spec/controllers/admin/statuses_controller_spec.rb'
    - 'spec/controllers/concerns/signature_verification_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues:
  Exclude:


@@ 2074,22 1572,8 @@ Style/IfUnlessModifier:
# Configuration parameters: InverseMethods, InverseBlocks.
Style/InverseMethods:
  Exclude:
    - 'app/controllers/concerns/signature_verification.rb'
    - 'app/helpers/jsonld_helper.rb'
    - 'app/lib/activitypub/activity/create.rb'
    - 'app/lib/activitypub/activity/move.rb'
    - 'app/lib/feed_manager.rb'
    - 'app/lib/link_details_extractor.rb'
    - 'app/models/concerns/attachmentable.rb'
    - 'app/models/concerns/remotable.rb'
    - 'app/models/custom_filter.rb'
    - 'app/models/webhook.rb'
    - 'app/services/activitypub/process_status_update_service.rb'
    - 'app/services/fetch_link_card_service.rb'
    - 'app/services/search_service.rb'
    - 'app/services/update_account_service.rb'
    - 'app/workers/web/push_notification_worker.rb'
    - 'lib/paperclip/color_extractor.rb'
    - 'spec/controllers/activitypub/replies_controller_spec.rb'

# This cop supports safe autocorrection (--autocorrect).


@@ 2110,12 1594,10 @@ Style/MapToHash:
# SupportedStyles: literals, strict
Style/MutableConstant:
  Exclude:
    - 'app/models/account.rb'
    - 'app/models/tag.rb'
    - 'app/services/delete_account_service.rb'
    - 'config/initializers/twitter_regex.rb'
    - 'lib/mastodon/migration_warning.rb'
    - 'spec/controllers/api/base_controller_spec.rb'

# This cop supports safe autocorrection (--autocorrect).
Style/NilLambda:


@@ 2199,7 1681,6 @@ Style/RedundantRegexpEscape:
Style/RegexpLiteral:
  Exclude:
    - 'app/lib/link_details_extractor.rb'
    - 'app/lib/permalink_redirector.rb'
    - 'app/lib/plain_text_formatter.rb'
    - 'app/lib/tag_manager.rb'
    - 'app/lib/text_formatter.rb'


@@ 2321,11 1802,14 @@ Style/TrailingCommaInHashLiteral:
    - 'config/environments/test.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: WordRegex.
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
# SupportedStyles: percent, brackets
Style/WordArray:
  EnforcedStyle: percent
  MinSize: 6
  Exclude:
    - 'app/helpers/languages_helper.rb'
    - 'config/initializers/cors.rb'
    - 'spec/controllers/settings/imports_controller_spec.rb'
    - 'spec/models/form/import_spec.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.

M Aptfile => Aptfile +1 -0
@@ 1,4 1,5 @@
ffmpeg
libopenblas0-pthread
libpq-dev
libxdamage1
libxfixes3

M Dockerfile => Dockerfile +7 -1
@@ 41,6 41,10 @@ RUN apt-get update && \

FROM node:${NODE_VERSION}

# Use those args to specify your own version flags & suffixes
ARG MASTODON_VERSION_FLAGS=""
ARG MASTODON_VERSION_SUFFIX=""

ARG UID="991"
ARG GID="991"



@@ 84,7 88,9 @@ COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
ENV RAILS_ENV="production" \
    NODE_ENV="production" \
    RAILS_SERVE_STATIC_FILES="true" \
    BIND="0.0.0.0"
    BIND="0.0.0.0" \
    MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
    MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"

# Set the run user
USER mastodon

M Gemfile => Gemfile +2 -4
@@ 30,10 30,7 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'
gem 'devise', '~> 4.9'
# The below `v4.x` branch allows attr_encrypted 4.x, which is required for Rails 7.
# Once a new gem version is pushed, we can go back to released gem and off of github branch.
gem 'devise-two-factor', github: 'tinfoil/devise-two-factor', branch: 'v4.x'
gem 'attr_encrypted', '~> 4.0'
gem 'devise-two-factor', '~> 4.1'

group :pam_authentication, optional: true do
  gem 'devise_pam_authenticatable2', '~> 9.2'


@@ 164,3 161,4 @@ gem 'hcaptcha', '~> 7.1'
gem 'cocoon', '~> 1.2'

gem 'net-http', '~> 0.3.2'
gem 'rubyzip', '~> 2.3'

M Gemfile.lock => Gemfile.lock +14 -19
@@ 27,18 27,6 @@ GIT
    rails-settings-cached (0.6.6)
      rails (>= 4.2.0)

GIT
  remote: https://github.com/tinfoil/devise-two-factor.git
  revision: e685f91ce62d036259885fbe31fcb4fa930bcfcb
  branch: v4.x
  specs:
    devise-two-factor (4.0.2)
      activesupport (< 7.1)
      attr_encrypted (>= 1.3, < 5, != 2)
      devise (~> 4.0)
      railties (< 7.1)
      rotp (~> 6.0)

GEM
  remote: https://rubygems.org/
  specs:


@@ 218,6 206,12 @@ GEM
      railties (>= 4.1.0)
      responders
      warden (~> 1.2.3)
    devise-two-factor (4.1.0)
      activesupport (< 7.1)
      attr_encrypted (>= 1.3, < 5, != 2)
      devise (~> 4.0)
      railties (< 7.1)
      rotp (~> 6.0)
    devise_pam_authenticatable2 (9.2.0)
      devise (>= 4.0.0)
      rpam2 (~> 4.0)


@@ 354,15 348,15 @@ GEM
    ipaddress (0.8.3)
    jmespath (1.6.2)
    json (2.6.3)
    json-canonicalization (0.3.1)
    json-canonicalization (0.3.2)
    json-jwt (1.15.3)
      activesupport (>= 4.2)
      aes_key_wrap
      bindata
      httpclient
    json-ld (3.2.4)
    json-ld (3.2.5)
      htmlentities (~> 4.3)
      json-canonicalization (~> 0.3)
      json-canonicalization (~> 0.3, >= 0.3.2)
      link_header (~> 0.0, >= 0.0.8)
      multi_json (~> 1.15)
      rack (>= 2.2, < 4)


@@ 492,7 486,7 @@ GEM
    parslet (2.0.0)
    pastel (0.8.0)
      tty-color (~> 0.5)
    pg (1.5.2)
    pg (1.5.3)
    pghero (3.3.3)
      activerecord (>= 6)
    pkg-config (1.5.1)


@@ 626,7 620,7 @@ GEM
    rubocop-performance (1.17.1)
      rubocop (>= 1.7.0, < 2.0)
      rubocop-ast (>= 0.4.0)
    rubocop-rails (2.18.0)
    rubocop-rails (2.19.1)
      activesupport (>= 4.2.0)
      rack (>= 1.1)
      rubocop (>= 1.33.0, < 2.0)


@@ 638,6 632,7 @@ GEM
      nokogiri (>= 1.10.5)
      rexml
    ruby2_keywords (0.0.5)
    rubyzip (2.3.2)
    rufus-scheduler (3.8.2)
      fugit (~> 1.1, >= 1.1.6)
    safety_net_attestation (0.4.0)


@@ 777,7 772,6 @@ DEPENDENCIES
  active_model_serializers (~> 0.10)
  addressable (~> 2.8)
  annotate (~> 3.2)
  attr_encrypted (~> 4.0)
  aws-sdk-s3 (~> 1.120)
  better_errors (~> 2.9)
  binding_of_caller (~> 1.0)


@@ 799,7 793,7 @@ DEPENDENCIES
  concurrent-ruby
  connection_pool
  devise (~> 4.9)
  devise-two-factor!
  devise-two-factor (~> 4.1)
  devise_pam_authenticatable2 (~> 9.2)
  discard (~> 1.2)
  doorkeeper (~> 5.6)


@@ 879,6 873,7 @@ DEPENDENCIES
  rubocop-rails
  rubocop-rspec
  ruby-progressbar (~> 1.13)
  rubyzip (~> 2.3)
  sanitize (~> 6.0)
  scenic (~> 1.7)
  sidekiq (~> 6.5)

M app/controllers/admin/domain_blocks_controller.rb => app/controllers/admin/domain_blocks_controller.rb +1 -1
@@ 33,7 33,7 @@ module Admin

      if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
        @domain_block.save
        flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
        flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe
        @domain_block.errors.delete(:domain)
        render :new
      else

M app/controllers/api/v1/media_controller.rb => app/controllers/api/v1/media_controller.rb +2 -1
@@ 15,7 15,8 @@ class Api::V1::MediaController < Api::BaseController
    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
    render json: file_type_error, status: 422
  rescue Paperclip::Error
  rescue Paperclip::Error => e
    Rails.logger.error "#{e.class}: #{e.message}"
    render json: processing_error, status: 500
  end


M app/controllers/api/v2/media_controller.rb => app/controllers/api/v2/media_controller.rb +2 -1
@@ 6,7 6,8 @@ class Api::V2::MediaController < Api::V1::MediaController
    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200
  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
    render json: file_type_error, status: 422
  rescue Paperclip::Error
  rescue Paperclip::Error => e
    Rails.logger.error "#{e.class}: #{e.message}"
    render json: processing_error, status: 500
  end
end

M app/controllers/authorize_interactions_controller.rb => app/controllers/authorize_interactions_controller.rb +1 -1
@@ 60,7 60,7 @@ class AuthorizeInteractionsController < ApplicationController
  end

  def uri_param
    params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '')
    params[:uri] || params.fetch(:acct, '').delete_prefix('acct:')
  end

  def set_body_classes

M app/controllers/concerns/signature_verification.rb => app/controllers/concerns/signature_verification.rb +5 -4
@@ 180,14 180,15 @@ module SignatureVerification

  def build_signed_string
    signed_headers.map do |signed_header|
      if signed_header == Request::REQUEST_TARGET
      case signed_header
      when Request::REQUEST_TARGET
        "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
      elsif signed_header == '(created)'
      when '(created)'
        raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
        raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?

        "(created): #{signature_params['created']}"
      elsif signed_header == '(expires)'
      when '(expires)'
        raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
        raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?



@@ 244,7 245,7 @@ module SignatureVerification
    end

    if key_id.start_with?('acct:')
      stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
      stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
    elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
      account   = ActivityPub::TagManager.instance.uri_to_actor(key_id)
      account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }

M app/controllers/intents_controller.rb => app/controllers/intents_controller.rb +1 -1
@@ 9,7 9,7 @@ class IntentsController < ApplicationController
    if uri.scheme == 'web+mastodon'
      case uri.host
      when 'follow'
        return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, ''))
        return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:'))
      when 'share'
        return redirect_to share_path(text: uri.query_values['text'])
      end

M app/controllers/media_proxy_controller.rb => app/controllers/media_proxy_controller.rb +1 -1
@@ 16,7 16,7 @@ class MediaProxyController < ApplicationController
  rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error

  def show
    with_lock("media_download:#{params[:id]}") do
    with_redis_lock("media_download:#{params[:id]}") do
      @media_attachment = MediaAttachment.remote.attached.find(params[:id])
      authorize @media_attachment.status, :show?
      redownload! if @media_attachment.needs_redownload? && !reject_media?

M app/controllers/settings/exports_controller.rb => app/controllers/settings/exports_controller.rb +1 -1
@@ 15,7 15,7 @@ class Settings::ExportsController < Settings::BaseController
  def create
    backup = nil

    with_lock("backup:#{current_user.id}") do
    with_redis_lock("backup:#{current_user.id}") do
      authorize :backup, :create?
      backup = current_user.backups.create!
    end

M app/controllers/settings/imports_controller.rb => app/controllers/settings/imports_controller.rb +78 -12
@@ 1,31 1,97 @@
# frozen_string_literal: true

require 'csv'

class Settings::ImportsController < Settings::BaseController
  before_action :set_account
  before_action :set_bulk_import, only: [:show, :confirm, :destroy]
  before_action :set_recent_imports, only: [:index]

  TYPE_TO_FILENAME_MAP = {
    following: 'following_accounts_failures.csv',
    blocking: 'blocked_accounts_failures.csv',
    muting: 'muted_accounts_failures.csv',
    domain_blocking: 'blocked_domains_failures.csv',
    bookmarks: 'bookmarks_failures.csv',
  }.freeze

  TYPE_TO_HEADERS_MAP = {
    following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'],
    blocking: false,
    muting: ['Account address', 'Hide notifications'],
    domain_blocking: false,
    bookmarks: false,
  }.freeze

  def index
    @import = Form::Import.new(current_account: current_account)
  end

  def show; end

  def failures
    @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id])

    respond_to do |format|
      format.csv do
        filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym]
        headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym]

        export_data = CSV.generate(headers: headers, write_headers: true) do |csv|
          @bulk_import.rows.find_each do |row|
            case @bulk_import.type.to_sym
            when :following
              csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')]
            when :blocking
              csv << [row.data['acct']]
            when :muting
              csv << [row.data['acct'], row.data.fetch('hide_notifications', true)]
            when :domain_blocking
              csv << [row.data['domain']]
            when :bookmarks
              csv << [row.data['uri']]
            end
          end
        end

  def show
    @import = Import.new
        send_data export_data, filename: filename
      end
    end
  end

  def confirm
    @bulk_import.update!(state: :scheduled)
    BulkImportWorker.perform_async(@bulk_import.id)
    redirect_to settings_imports_path, notice: I18n.t('imports.success')
  end

  def create
    @import = Import.new(import_params)
    @import.account = @account
    @import = Form::Import.new(import_params.merge(current_account: current_account))

    if @import.save
      ImportWorker.perform_async(@import.id)
      redirect_to settings_import_path, notice: I18n.t('imports.success')
      redirect_to settings_import_path(@import.bulk_import.id)
    else
      render :show
      # We need to set recent imports as we are displaying the index again
      set_recent_imports
      render :index
    end
  end

  def destroy
    @bulk_import.destroy!
    redirect_to settings_imports_path
  end

  private

  def set_account
    @account = current_user.account
  def import_params
    params.require(:form_import).permit(:data, :type, :mode)
  end

  def import_params
    params.require(:import).permit(:data, :type, :mode)
  def set_bulk_import
    @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id])
  end

  def set_recent_imports
    @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10)
  end
end

M app/controllers/settings/preferences/appearance_controller.rb => app/controllers/settings/preferences/appearance_controller.rb +1 -1
@@ 1,6 1,6 @@
# frozen_string_literal: true

class Settings::Preferences::AppearanceController < Settings::PreferencesController
class Settings::Preferences::AppearanceController < Settings::Preferences::BaseController
  private

  def after_update_redirect_path

R app/controllers/settings/preferences_controller.rb => app/controllers/settings/preferences/base_controller.rb +2 -2
@@ 1,6 1,6 @@
# frozen_string_literal: true

class Settings::PreferencesController < Settings::BaseController
class Settings::Preferences::BaseController < Settings::BaseController
  def show; end

  def update


@@ 15,7 15,7 @@ class Settings::PreferencesController < Settings::BaseController
  private

  def after_update_redirect_path
    settings_preferences_path
    raise 'Override in controller'
  end

  def user_params

M app/controllers/settings/preferences/notifications_controller.rb => app/controllers/settings/preferences/notifications_controller.rb +1 -1
@@ 1,6 1,6 @@
# frozen_string_literal: true

class Settings::Preferences::NotificationsController < Settings::PreferencesController
class Settings::Preferences::NotificationsController < Settings::Preferences::BaseController
  private

  def after_update_redirect_path

M app/controllers/settings/preferences/other_controller.rb => app/controllers/settings/preferences/other_controller.rb +1 -1
@@ 1,6 1,6 @@
# frozen_string_literal: true

class Settings::Preferences::OtherController < Settings::PreferencesController
class Settings::Preferences::OtherController < Settings::Preferences::BaseController
  private

  def after_update_redirect_path

M app/controllers/well_known/webfinger_controller.rb => app/controllers/well_known/webfinger_controller.rb +8 -1
@@ 18,7 18,14 @@ module WellKnown
    private

    def set_account
      @account = Account.find_local!(username_from_resource)
      username = username_from_resource
      @account = begin
        if username == Rails.configuration.x.local_domain
          Account.representative
        else
          Account.find_local!(username)
        end
      end
    end

    def username_from_resource

M app/helpers/application_helper.rb => app/helpers/application_helper.rb +9 -7
@@ 32,10 32,6 @@ module ApplicationHelper
    paths.any? { |path| current_page?(path) } ? 'active' : ''
  end

  def active_link_to(label, path, **options)
    link_to label, path, options.merge(class: active_nav_class(path))
  end

  def show_landing_strip?
    !user_signed_in? && !single_user_mode?
  end


@@ 147,7 143,7 @@ module ApplicationHelper
    if prefers_autoplay?
      image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
    else
      image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static)))
      image_tag(custom_emoji.image.url(:static), :class => 'emojione custom-emoji', :alt => ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static)))
    end
  end



@@ 174,11 170,11 @@ module ApplicationHelper
  end

  def storage_host
    "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}"
    URI::HTTPS.build(host: storage_host_name).to_s
  end

  def storage_host?
    ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
    storage_host_name.present?
  end

  def quote_wrap(text, line_width: 80, break_sequence: "\n")


@@ 236,4 232,10 @@ module ApplicationHelper
  def prerender_custom_emojis(html, custom_emojis, other_options = {})
    EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
  end

  private

  def storage_host_name
    ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
  end
end

D app/javascript/mastodon/actions/app.js => app/javascript/mastodon/actions/app.js +0 -17
@@ 1,17 0,0 @@
export const APP_FOCUS   = 'APP_FOCUS';
export const APP_UNFOCUS = 'APP_UNFOCUS';

export const focusApp = () => ({
  type: APP_FOCUS,
});

export const unfocusApp = () => ({
  type: APP_UNFOCUS,
});

export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';

export const changeLayout = layout => ({
  type: APP_LAYOUT_CHANGE,
  layout,
});

A app/javascript/mastodon/actions/app.ts => app/javascript/mastodon/actions/app.ts +10 -0
@@ 0,0 1,10 @@
import { createAction } from '@reduxjs/toolkit';

export const focusApp = createAction('APP_FOCUS');
export const unfocusApp = createAction('APP_UNFOCUS');

type ChangeLayoutPayload = {
  layout: 'mobile' | 'single-column' | 'multi-column';
};
export const changeLayout =
  createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');

R app/javascript/mastodon/blurhash.js => app/javascript/mastodon/blurhash.ts +3 -3
@@ 84,7 84,7 @@ const DIGIT_CHARACTERS = [
  '~',
];

export const decode83 = (str) => {
export const decode83 = (str: string) => {
  let value = 0;
  let c, digit;



@@ 97,13 97,13 @@ export const decode83 = (str) => {
  return value;
};

export const intToRGB = int => ({
export const intToRGB = (int: number) => ({
  r: Math.max(0, (int >> 16)),
  g: Math.max(0, (int >> 8) & 255),
  b: Math.max(0, (int & 255)),
});

export const getAverageFromBlurhash = blurhash => {
export const getAverageFromBlurhash = (blurhash: string) => {
  if (!blurhash) {
    return null;
  }

R app/javascript/mastodon/compare_id.js => app/javascript/mastodon/compare_id.ts +1 -1
@@ 1,4 1,4 @@
export default function compareId (id1, id2) {
export default function compareId (id1: string, id2: string) {
  if (id1 === id2) {
    return 0;
  }

R app/javascript/mastodon/components/blurhash.jsx => app/javascript/mastodon/components/blurhash.tsx +14 -34
@@ 1,39 1,27 @@
// @ts-check

import { decode } from 'blurhash';
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';

/**
 * @typedef BlurhashPropsBase
 * @property {string?} hash Hash to render
 * @property {number} width
 * Width of the blurred region in pixels. Defaults to 32
 * @property {number} [height]
 * Height of the blurred region in pixels. Defaults to width
 * @property {boolean} [dummy]
 * Whether dummy mode is enabled. If enabled, nothing is rendered
 * and canvas left untouched
 */

/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */

/**
 * Component that is used to render blurred of blurhash string
 * @param {BlurhashProps} param1 Props of the component
 * @returns {JSX.Element} Canvas which will render blurred region element to embed
 */
type Props = {
  hash: string;
  width?: number;
  height?: number;
  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
  children?: never;
  [key: string]: any;
}
function Blurhash({
  hash,
  width = 32,
  height = width,
  dummy = false,
  ...canvasProps
}) {
  const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
}: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const { current: canvas } = canvasRef;
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const canvas = canvasRef.current!;
    // eslint-disable-next-line no-self-assign
    canvas.width = canvas.width; // resets canvas

    if (dummy || !hash) return;


@@ 43,8 31,7 @@ function Blurhash({
      const ctx = canvas.getContext('2d');
      const imageData = new ImageData(pixels, width, height);

      // @ts-expect-error
      ctx.putImageData(imageData, 0, 0);
      ctx?.putImageData(imageData, 0, 0);
    } catch (err) {
      console.error('Blurhash decoding failure', { err, hash });
    }


@@ 55,11 42,4 @@ function Blurhash({
  );
}

Blurhash.propTypes = {
  hash: PropTypes.string.isRequired,
  width: PropTypes.number,
  height: PropTypes.number,
  dummy: PropTypes.bool,
};

export default React.memo(Blurhash);

M app/javascript/mastodon/components/column_back_button.jsx => app/javascript/mastodon/components/column_back_button.jsx +3 -1
@@ 21,7 21,9 @@ export default class ColumnBackButton extends React.PureComponent {

    if (onClick) {
      onClick();
    } else if (window.history && window.history.state) {
    // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
    // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
    } else if (router.route.location.key) {
      router.history.goBack();
    } else {
      router.history.push('/');

R app/javascript/mastodon/components/icon_button.jsx => app/javascript/mastodon/components/icon_button.tsx +38 -36
@@ 1,34 1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';

export default class IconButton extends React.PureComponent {

  static propTypes = {
    className: PropTypes.string,
    title: PropTypes.string.isRequired,
    icon: PropTypes.string.isRequired,
    onClick: PropTypes.func,
    onMouseDown: PropTypes.func,
    onKeyDown: PropTypes.func,
    onKeyPress: PropTypes.func,
    size: PropTypes.number,
    active: PropTypes.bool,
    expanded: PropTypes.bool,
    style: PropTypes.object,
    activeStyle: PropTypes.object,
    disabled: PropTypes.bool,
    inverted: PropTypes.bool,
    animate: PropTypes.bool,
    overlay: PropTypes.bool,
    tabIndex: PropTypes.number,
    counter: PropTypes.number,
    obfuscateCount: PropTypes.bool,
    href: PropTypes.string,
    ariaHidden: PropTypes.bool,
  };
import { Icon } from './icon';
import { AnimatedNumber } from './animated_number';

type Props = {
  className?: string;
  title: string;
  icon: string;
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
  onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
  onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
  size: number;
  active: boolean;
  expanded?: boolean;
  style?: React.CSSProperties;
  activeStyle?: React.CSSProperties;
  disabled: boolean;
  inverted?: boolean;
  animate: boolean;
  overlay: boolean;
  tabIndex: number;
  counter?: number;
  obfuscateCount?: boolean;
  href?: string;
  ariaHidden: boolean;
}
type States = {
  activate: boolean,
  deactivate: boolean,
}
export default class IconButton extends React.PureComponent<Props, States> {

  static defaultProps = {
    size: 18,


@@ 45,7 47,7 @@ export default class IconButton extends React.PureComponent {
    deactivate: false,
  };

  componentWillReceiveProps (nextProps) {
  UNSAFE_componentWillReceiveProps (nextProps: Props) {
    if (!nextProps.animate) return;

    if (this.props.active && !nextProps.active) {


@@ 55,27 57,27 @@ export default class IconButton extends React.PureComponent {
    }
  }

  handleClick = (e) =>  {
  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) =>  {
    e.preventDefault();

    if (!this.props.disabled) {
    if (!this.props.disabled && this.props.onClick != null) {
      this.props.onClick(e);
    }
  };

  handleKeyPress = (e) => {
  handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
    if (this.props.onKeyPress && !this.props.disabled) {
      this.props.onKeyPress(e);
    }
  };

  handleMouseDown = (e) => {
  handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
    if (!this.props.disabled && this.props.onMouseDown) {
      this.props.onMouseDown(e);
    }
  };

  handleKeyDown = (e) => {
  handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
    if (!this.props.disabled && this.props.onKeyDown) {
      this.props.onKeyDown(e);
    }


@@ 132,7 134,7 @@ export default class IconButton extends React.PureComponent {
      </React.Fragment>
    );

    if (href && !this.prop) {
    if (href != null) {
      contents = (
        <a href={href} target='_blank' rel='noopener noreferrer'>
          {contents}

M app/javascript/mastodon/components/media_gallery.jsx => app/javascript/mastodon/components/media_gallery.jsx +18 -49
@@ 81,12 81,10 @@ class Item extends React.PureComponent {
  render () {
    const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;

    let badges = [], thumbnail;

    let width  = 50;
    let height = 100;
    let top    = 'auto';
    let left   = 'auto';
    let bottom = 'auto';
    let right  = 'auto';

    if (size === 1) {
      width = 100;


@@ 96,45 94,13 @@ class Item extends React.PureComponent {
      height = 50;
    }

    if (size === 2) {
      if (index === 0) {
        right = '2px';
      } else {
        left = '2px';
      }
    } else if (size === 3) {
      if (index === 0) {
        right = '2px';
      } else if (index > 0) {
        left = '2px';
      }

      if (index === 1) {
        bottom = '2px';
      } else if (index > 1) {
        top = '2px';
      }
    } else if (size === 4) {
      if (index === 0 || index === 2) {
        right = '2px';
      }

      if (index === 1 || index === 3) {
        left = '2px';
      }

      if (index < 2) {
        bottom = '2px';
      } else {
        top = '2px';
      }
    if (attachment.get('description')?.length > 0) {
      badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
    }

    let thumbnail = '';

    if (attachment.get('type') === 'unknown') {
      return (
        <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
        <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
            <Blurhash
              hash={attachment.get('blurhash')}


@@ 184,6 150,8 @@ class Item extends React.PureComponent {
    } else if (attachment.get('type') === 'gifv') {
      const autoPlay = this.getAutoPlay();

      badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);

      thumbnail = (
        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
          <video


@@ 201,14 169,12 @@ class Item extends React.PureComponent {
            loop
            muted
          />

          <span className='media-gallery__gifv__label'>GIF</span>
        </div>
      );
    }

    return (
      <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
      <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
        <Blurhash
          hash={attachment.get('blurhash')}
          dummy={!useBlurhash}


@@ 216,7 182,14 @@ class Item extends React.PureComponent {
            'media-gallery__preview--hidden': visible && this.state.loaded,
          })}
        />

        {visible && thumbnail}

        {badges && (
          <div className='media-gallery__item__badges'>
            {badges}
          </div>
        )}
      </div>
    );
  }


@@ 313,7 286,7 @@ class MediaGallery extends React.PureComponent {
  }

  render () {
    const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
    const { media, lang, intl, sensitive, defaultWidth, standalone, autoplay } = this.props;
    const { visible } = this.state;
    const width = this.state.width || defaultWidth;



@@ 322,13 295,9 @@ class MediaGallery extends React.PureComponent {
    const style = {};

    if (this.isFullSizeEligible() && (standalone || !cropImages)) {
      if (width) {
        style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
      }
    } else if (width) {
      style.height = width / (16/9);
      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
    } else {
      style.height = height;
      style.aspectRatio = '16 / 9';
    }

    const size     = media.take(4).size;

M app/javascript/mastodon/components/picture_in_picture_placeholder.jsx => app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +1 -41
@@ 3,62 3,22 @@ import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { FormattedMessage } from 'react-intl';

class PictureInPicturePlaceholder extends React.PureComponent {

  static propTypes = {
    width: PropTypes.number,
    dispatch: PropTypes.func.isRequired,
  };

  state = {
    width: this.props.width,
    height: this.props.width && (this.props.width / (16/9)),
  };

  handleClick = () => {
    const { dispatch } = this.props;
    dispatch(removePictureInPicture());
  };

  setRef = c => {
    this.node = c;

    if (this.node) {
      this._setDimensions();
    }
  };

  _setDimensions () {
    const width  = this.node.offsetWidth;
    const height = width / (16/9);

    this.setState({ width, height });
  }

  componentDidMount () {
    window.addEventListener('resize', this.handleResize, { passive: true });
  }

  componentWillUnmount () {
    window.removeEventListener('resize', this.handleResize);
  }

  handleResize = debounce(() => {
    if (this.node) {
      this._setDimensions();
    }
  }, 250, {
    trailing: true,
  });

  render () {
    const { height } = this.state;

    return (
      <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}>
      <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}>
        <Icon id='window-restore' />
        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
      </div>

M app/javascript/mastodon/components/status.jsx => app/javascript/mastodon/components/status.jsx +1 -6
@@ 411,7 411,7 @@ class Status extends ImmutablePureComponent {
    }

    if (pictureInPicture.get('inUse')) {
      media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
      media = <PictureInPicturePlaceholder />;
    } else if (status.get('media_attachments').size > 0) {
      if (this.props.muted) {
        media = (


@@ 460,12 460,9 @@ class Status extends ImmutablePureComponent {
                src={attachment.get('url')}
                alt={attachment.get('description')}
                lang={status.get('language')}
                width={this.props.cachedMediaWidth}
                height={110}
                inline
                sensitive={status.get('sensitive')}
                onOpenVideo={this.handleOpenVideo}
                cacheWidth={this.props.cacheMediaWidth}
                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
                visible={this.state.showMedia}
                onToggleVisibility={this.handleToggleMediaVisibility}


@@ 498,8 495,6 @@ class Status extends ImmutablePureComponent {
          onOpenMedia={this.handleOpenMedia}
          card={status.get('card')}
          compact
          cacheWidth={this.props.cacheMediaWidth}
          defaultWidth={this.props.cachedMediaWidth}
          sensitive={status.get('sensitive')}
        />
      );

M app/javascript/mastodon/containers/compose_container.jsx => app/javascript/mastodon/containers/compose_container.jsx +1 -3
@@ 1,7 1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { store } from '../store/configureStore';
import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';


@@ 12,8 12,6 @@ import { fetchCustomEmojis } from '../actions/custom_emojis';
const { localeData, messages } = getLocale();
addLocaleData(localeData);

const store = configureStore();

if (initialState) {
  store.dispatch(hydrateStore(initialState));
}

M app/javascript/mastodon/containers/mastodon.jsx => app/javascript/mastodon/containers/mastodon.jsx +1 -2
@@ 5,7 5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { Provider as ReduxProvider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import configureStore from 'mastodon/store/configureStore';
import { store } from 'mastodon/store/configureStore';
import UI from 'mastodon/features/ui';
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { hydrateStore } from 'mastodon/actions/store';


@@ 19,7 19,6 @@ addLocaleData(localeData);

const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;

export const store = configureStore();
const hydrateAction = hydrateStore(initialState);

store.dispatch(hydrateAction);

M app/javascript/mastodon/features/audio/index.jsx => app/javascript/mastodon/features/audio/index.jsx +13 -6
@@ 384,7 384,7 @@ class Audio extends React.PureComponent {
  }

  _getRadius () {
    return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
    return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
  }

  _getScaleCoefficient () {


@@ 396,7 396,7 @@ class Audio extends React.PureComponent {
  }

  _getCY() {
    return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
    return Math.floor((this.state.height || this.props.height) / 2);
  }

  _getAccentColor () {


@@ 470,7 470,7 @@ class Audio extends React.PureComponent {
    }

    return (
      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>

        <Blurhash
          hash={blurhash}


@@ 515,9 515,16 @@ class Audio extends React.PureComponent {
        {(revealed || editable) && <img
          src={this.props.poster}
          alt=''
          width={(this._getRadius() - TICK_SIZE) * 2}
          height={(this._getRadius() - TICK_SIZE) * 2}
          style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
          style={{
            position: 'absolute',
            left: '50%',
            top: '50%',
            height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
            aspectRatio: '1',
            transform: 'translate(-50%, -50%)',
            borderRadius: '50%',
            pointerEvents: 'none',
          }}
        />}

        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>

M app/javascript/mastodon/features/status/components/card.jsx => app/javascript/mastodon/features/status/components/card.jsx +12 -35
@@ 8,7 8,6 @@ import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
import { useBlurhash } from 'mastodon/initial_state';
import Blurhash from 'mastodon/components/blurhash';
import { debounce } from 'lodash';

const IDNA_PREFIX = 'xn--';



@@ 54,8 53,6 @@ export default class Card extends React.PureComponent {
    card: ImmutablePropTypes.map,
    onOpenMedia: PropTypes.func.isRequired,
    compact: PropTypes.bool,
    defaultWidth: PropTypes.number,
    cacheWidth: PropTypes.func,
    sensitive: PropTypes.bool,
  };



@@ 64,7 61,6 @@ export default class Card extends React.PureComponent {
  };

  state = {
    width: this.props.defaultWidth || 280,
    previewLoaded: false,
    embedded: false,
    revealed: !this.props.sensitive,


@@ 87,24 83,6 @@ export default class Card extends React.PureComponent {
    window.removeEventListener('resize', this.handleResize);
  }

  _setDimensions () {
    const width = this.node.offsetWidth;

    if (this.props.cacheWidth) {
      this.props.cacheWidth(width);
    }

    this.setState({ width });
  }

  handleResize = debounce(() => {
    if (this.node) {
      this._setDimensions();
    }
  }, 250, {
    trailing: true,
  });

  handlePhotoClick = () => {
    const { card, onOpenMedia } = this.props;



@@ 138,10 116,6 @@ export default class Card extends React.PureComponent {

  setRef = c => {
    this.node = c;

    if (this.node) {
      this._setDimensions();
    }
  };

  handleImageLoad = () => {


@@ 157,36 131,31 @@ export default class Card extends React.PureComponent {
  renderVideo () {
    const { card }  = this.props;
    const content   = { __html: addAutoPlay(card.get('html')) };
    const { width } = this.state;
    const ratio     = card.get('width') / card.get('height');
    const height    = width / ratio;

    return (
      <div
        ref={this.setRef}
        className='status-card__image status-card-video'
        dangerouslySetInnerHTML={content}
        style={{ height }}
        style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
      />
    );
  }

  render () {
    const { card, compact } = this.props;
    const { width, embedded, revealed } = this.state;
    const { embedded, revealed } = this.state;

    if (card === null) {
      return null;
    }

    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
    const horizontal  = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
    const horizontal  = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
    const interactive = card.get('type') !== 'link';
    const className   = classnames('status-card', { horizontal, compact, interactive });
    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
    const language    = card.get('language') || '';
    const ratio       = card.get('width') / card.get('height');
    const height      = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);

    const description = (
      <div className='status-card__content' lang={language}>


@@ 196,6 165,14 @@ export default class Card extends React.PureComponent {
      </div>
    );

    const thumbnailStyle = {
      visibility: revealed? null : 'hidden',
    };

    if (horizontal) {
      thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
    }

    let embed     = '';
    let canvas = (
      <Blurhash


@@ 206,7 183,7 @@ export default class Card extends React.PureComponent {
        dummy={!useBlurhash}
      />
    );
    let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
    let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
    let spoilerButton = (
      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
        <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>

M app/javascript/mastodon/features/ui/index.jsx => app/javascript/mastodon/features/ui/index.jsx +1 -1
@@ 362,7 362,7 @@ class UI extends React.PureComponent {

    if (layout !== this.props.layout) {
      this.handleLayoutChange.cancel();
      this.props.dispatch(changeLayout(layout));
      this.props.dispatch(changeLayout({ layout }));
    } else {
      this.handleLayoutChange();
    }

M app/javascript/mastodon/features/video/index.jsx => app/javascript/mastodon/features/video/index.jsx +5 -41
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { is } from 'immutable';
import { throttle, debounce } from 'lodash';
import { throttle } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state';


@@ 102,8 102,6 @@ class Video extends React.PureComponent {
    src: PropTypes.string.isRequired,
    alt: PropTypes.string,
    lang: PropTypes.string,
    width: PropTypes.number,
    height: PropTypes.number,
    sensitive: PropTypes.bool,
    currentTime: PropTypes.number,
    onOpenVideo: PropTypes.func,


@@ 112,7 110,6 @@ class Video extends React.PureComponent {
    inline: PropTypes.bool,
    editable: PropTypes.bool,
    alwaysVisible: PropTypes.bool,
    cacheWidth: PropTypes.func,
    visible: PropTypes.bool,
    onToggleVisibility: PropTypes.func,
    deployPictureInPicture: PropTypes.func,


@@ 135,7 132,6 @@ class Video extends React.PureComponent {
    volume: 0.5,
    paused: true,
    dragging: false,
    containerWidth: this.props.width,
    fullscreen: false,
    hovered: false,
    muted: false,


@@ 144,24 140,8 @@ class Video extends React.PureComponent {

  setPlayerRef = c => {
    this.player = c;

    if (this.player) {
      this._setDimensions();
    }
  };

  _setDimensions () {
    const width = this.player.offsetWidth;

    if (this.props.cacheWidth) {
      this.props.cacheWidth(width);
    }

    this.setState({
      containerWidth: width,
    });
  }

  setVideoRef = c => {
    this.video = c;



@@ 370,12 350,10 @@ class Video extends React.PureComponent {
    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);

    window.addEventListener('scroll', this.handleScroll);
    window.addEventListener('resize', this.handleResize, { passive: true });
  }

  componentWillUnmount () {
    window.removeEventListener('scroll', this.handleScroll);
    window.removeEventListener('resize', this.handleResize);

    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);


@@ 404,14 382,6 @@ class Video extends React.PureComponent {
    }
  }

  handleResize = debounce(() => {
    if (this.player) {
      this._setDimensions();
    }
  }, 250, {
    trailing: true,
  });

  handleScroll = throttle(() => {
    if (!this.video) {
      return;


@@ 525,17 495,12 @@ class Video extends React.PureComponent {

  render () {
    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
    const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
    const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
    const progress = Math.min((currentTime / duration) * 100, 100);
    const playerStyle = {};

    let { width, height } = this.props;

    if (inline && containerWidth) {
      width  = containerWidth;
      height = containerWidth / (16/9);

      playerStyle.height = height;
    if (inline) {
      playerStyle.aspectRatio = '16 / 9';
    }

    let preload;


@@ 586,8 551,6 @@ class Video extends React.PureComponent {
          aria-label={alt}
          title={alt}
          lang={lang}
          width={width}
          height={height}
          volume={volume}
          onClick={this.togglePlay}
          onKeyDown={this.handleVideoKeyDown}


@@ 596,6 559,7 @@ class Video extends React.PureComponent {
          onLoadedData={this.handleLoadedData}
          onProgress={this.handleProgress}
          onVolumeChange={this.handleVolumeChange}
          style={{ ...playerStyle, width: '100%' }}
        />}

        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>

R app/javascript/mastodon/is_mobile.js => app/javascript/mastodon/is_mobile.ts +6 -14
@@ 1,21 1,12 @@
// @ts-check

import { supportsPassiveEvents } from 'detect-passive-events';
// @ts-expect-error
import { forceSingleColumn } from 'mastodon/initial_state';
import { forceSingleColumn } from './initial_state';

const LAYOUT_BREAKPOINT = 630;

/**
 * @param {number} width
 * @returns {boolean}
 */
export const isMobile = width => width <= LAYOUT_BREAKPOINT;
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;

/**
 * @returns {string}
 */
export const layoutFromWindow = () => {
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
export const layoutFromWindow = (): LayoutType => {
  if (isMobile(window.innerWidth)) {
    return 'mobile';
  } else if (forceSingleColumn) {


@@ 25,8 16,9 @@ export const layoutFromWindow = () => {
  }
};

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null;

const listenerOptions = supportsPassiveEvents ? { passive: true } : false;


M app/javascript/mastodon/main.jsx => app/javascript/mastodon/main.jsx +2 -1
@@ 1,7 1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
import Mastodon, { store } from 'mastodon/containers/mastodon';
import Mastodon from 'mastodon/containers/mastodon';
import { store } from 'mastodon/store/configureStore';
import { me } from 'mastodon/initial_state';
import ready from 'mastodon/ready';


R app/javascript/mastodon/permissions.js => app/javascript/mastodon/permissions.ts +0 -0
M app/javascript/mastodon/reducers/meta.js => app/javascript/mastodon/reducers/meta.js +3 -3
@@ 1,5 1,5 @@
import { STORE_HYDRATE } from 'mastodon/actions/store';
import { APP_LAYOUT_CHANGE } from 'mastodon/actions/app';
import { changeLayout } from 'mastodon/actions/app';
import { Map as ImmutableMap } from 'immutable';
import { layoutFromWindow } from 'mastodon/is_mobile';



@@ 14,8 14,8 @@ export default function meta(state = initialState, action) {
  switch(action.type) {
  case STORE_HYDRATE:
    return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
  case APP_LAYOUT_CHANGE:
    return state.set('layout', action.layout);
  case changeLayout.type:
    return state.set('layout', action.payload.layout);
  default:
    return state;
  }

D app/javascript/mastodon/reducers/missed_updates.js => app/javascript/mastodon/reducers/missed_updates.js +0 -21
@@ 1,21 0,0 @@
import { Map as ImmutableMap } from 'immutable';
import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications';
import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app';

const initialState = ImmutableMap({
  focused: true,
  unread: 0,
});

export default function missed_updates(state = initialState, action) {
  switch(action.type) {
  case APP_FOCUS:
    return state.set('focused', true).set('unread', 0);
  case APP_UNFOCUS:
    return state.set('focused', false);
  case NOTIFICATIONS_UPDATE:
    return state.get('focused') ? state : state.update('unread', x => x + 1);
  default:
    return state;
  }
}

A app/javascript/mastodon/reducers/missed_updates.ts => app/javascript/mastodon/reducers/missed_updates.ts +31 -0
@@ 0,0 1,31 @@
import { Record } from 'immutable';
import type { Action } from 'redux';
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
import { focusApp, unfocusApp } from '../actions/app';

type MissedUpdatesState = {
  focused: boolean;
  unread: number;
};
const initialState = Record<MissedUpdatesState>({
  focused: true,
  unread: 0,
})();

export default function missed_updates(
  state = initialState,
  action: Action<string>,
) {
  switch (action.type) {
  case focusApp.type:
    return state.set('focused', true).set('unread', 0);
  case unfocusApp.type:
    return state.set('focused', false);
  case NOTIFICATIONS_UPDATE:
    return state.get('focused')
      ? state
      : state.update('unread', (x) => x + 1);
  default:
    return state;
  }
}

M app/javascript/mastodon/reducers/notifications.js => app/javascript/mastodon/reducers/notifications.js +4 -4
@@ 23,8 23,8 @@ import {
  MARKERS_FETCH_SUCCESS,
} from '../actions/markers';
import {
  APP_FOCUS,
  APP_UNFOCUS,
  focusApp,
  unfocusApp,
} from '../actions/app';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';


@@ 258,9 258,9 @@ export default function notifications(state = initialState, action) {
    return updateMounted(state);
  case NOTIFICATIONS_UNMOUNT:
    return state.update('mounted', count => count - 1);
  case APP_FOCUS:
  case focusApp.type:
    return updateVisibility(state, true);
  case APP_UNFOCUS:
  case unfocusApp.type:
    return updateVisibility(state, false);
  case NOTIFICATIONS_LOAD_PENDING:
    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);

R app/javascript/mastodon/scroll.js => app/javascript/mastodon/scroll.ts +4 -5
@@ 1,6 1,5 @@
const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;

const scroll = (node, key, target) => {
const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
  const startTime = Date.now();
  const offset    = node[key];
  const gap       = target - offset;


@@ 28,5 27,5 @@ const scroll = (node, key, target) => {

const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;

export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);

M app/javascript/mastodon/store/configureStore.js => app/javascript/mastodon/store/configureStore.js +6 -5
@@ 1,15 1,16 @@
import { createStore, applyMiddleware, compose } from 'redux';
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import appReducer from '../reducers';
import loadingBarMiddleware from '../middleware/loading_bar';
import errorsMiddleware from '../middleware/errors';
import soundsMiddleware from '../middleware/sounds';

export default function configureStore() {
  return createStore(appReducer, compose(applyMiddleware(
export const store = configureStore({
  reducer: appReducer,
  middleware: [
    thunk,
    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
    errorsMiddleware(),
    soundsMiddleware(),
  ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
}
  ],
});

R app/javascript/mastodon/utils/base64.js => app/javascript/mastodon/utils/base64.ts +1 -1
@@ 1,4 1,4 @@
export const decode = base64 => {
export const decode = (base64: string): Uint8Array => {
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);


R app/javascript/mastodon/utils/filters.js => app/javascript/mastodon/utils/filters.ts +1 -1
@@ 1,4 1,4 @@
export const toServerSideType = columnType => {
export const toServerSideType = (columnType: string) => {
  switch (columnType) {
  case 'home':
  case 'notifications':

R app/javascript/mastodon/utils/hashtags.js => app/javascript/mastodon/utils/hashtags.ts +0 -0
R app/javascript/mastodon/utils/numbers.js => app/javascript/mastodon/utils/numbers.ts +12 -19
@@ 1,31 1,28 @@
// @ts-check
import type { ValueOf } from '../../types/util';

export const DECIMAL_UNITS = Object.freeze({
  ONE: 1,
  TEN: 10,
  HUNDRED: Math.pow(10, 2),
  THOUSAND: Math.pow(10, 3),
  MILLION: Math.pow(10, 6),
  BILLION: Math.pow(10, 9),
  TRILLION: Math.pow(10, 12),
  HUNDRED: 100,
  THOUSAND: 1_000,
  MILLION: 1_000_000,
  BILLION: 1_000_000_000,
  TRILLION: 1_000_000_000_000,
});
export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;

const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;

/**
 * @typedef {[number, number, number]} ShortNumber
 * Array of: shorten number, unit of shorten number and maximum fraction digits
 */

/**
 * @param {number} sourceNumber Number to convert to short number
 * @returns {ShortNumber} Calculated short number
 * @example
 * shortNumber(5936);
 * // => [5.936, 1000, 1]
 */
export function toShortNumber(sourceNumber) {
export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
export function toShortNumber(sourceNumber: number): ShortNumber {
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
  } else if (sourceNumber < DECIMAL_UNITS.MILLION) {


@@ 59,20 56,16 @@ export function toShortNumber(sourceNumber) {
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 * // => 1790
 */
export function pluralReady(sourceNumber, division) {
export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
    return sourceNumber;
  }

  let closestScale = division / DECIMAL_UNITS.TEN;
  const closestScale = division / DECIMAL_UNITS.TEN;

  return Math.trunc(sourceNumber / closestScale) * closestScale;
}

/**
 * @param {number} num
 * @returns {number}
 */
export function roundTo10(num) {
export function roundTo10(num: number): number {
  return Math.round(num * 0.1) / 0.1;
}

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +32 -18
@@ 1784,7 1784,6 @@ a.account__display-name {
.status__avatar {
  width: 46px;
  height: 46px;
  box-shadow: 0 0 0 2px $ui-base-color;
}

.muted {


@@ 3110,6 3109,10 @@ $ui-header-height: 55px;
}

.compose-form__highlightable {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  flex: 0 1 auto;
  border-radius: 4px;
  transition: box-shadow 300ms linear;



@@ 3804,6 3807,10 @@ a.status-card {
}

.status-card-video {
  // Firefox has a bug where frameborder=0 iframes add some extra blank space
  // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174
  overflow: hidden;

  iframe {
    width: 100%;
    height: 100%;


@@ 6326,32 6333,27 @@ a.status-card.compact:hover {
  z-index: 9999;
}

.media-gallery__gifv__label {
  display: block;
.media-gallery__item__badges {
  position: absolute;
  color: $primary-text-color;
  background: rgba($base-overlay-background, 0.5);
  bottom: 6px;
  inset-inline-start: 6px;
  display: flex;
  gap: 2px;
}

.media-gallery__gifv__label {
  display: block;
  color: $white;
  background: rgba($black, 0.65);
  padding: 2px 6px;
  border-radius: 2px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
  font-weight: 700;
  z-index: 1;
  pointer-events: none;
  opacity: 0.9;
  transition: opacity 0.1s ease;
  line-height: 18px;
}

.media-gallery__gifv {
  &:hover {
    .media-gallery__gifv__label {
      opacity: 1;
    }
  }
}

.attachment-list {
  display: flex;
  font-size: 14px;


@@ 6424,17 6426,28 @@ a.status-card.compact:hover {
  position: relative;
  width: 100%;
  min-height: 64px;
  display: grid;
  grid-template-columns: 50% 50%;
  grid-template-rows: 50% 50%;
  gap: 2px;
}

.media-gallery__item {
  border: 0;
  box-sizing: border-box;
  display: block;
  float: left;
  position: relative;
  border-radius: 4px;
  overflow: hidden;

  &--tall {
    grid-row: span 2;
  }

  &--wide {
    grid-column: span 2;
  }

  &.standalone {
    .media-gallery__item-gifv-thumbnail {
      transform: none;


@@ 8332,6 8345,7 @@ noscript {
  font-weight: 500;
  cursor: pointer;
  color: $darker-text-color;
  aspect-ratio: 16 / 9;

  i {
    display: block;

M app/javascript/types/resources.ts => app/javascript/types/resources.ts +3 -6
@@ 1,8 1,4 @@
interface MastodonMap<T> {
  get<K extends keyof T>(key: K): T[K];
  has<K extends keyof T>(key: K): boolean;
  set<K extends keyof T>(key: K, value: T[K]): this;
}
import type { Record } from 'immutable';

type AccountValues = {
  id: number;


@@ 10,4 6,5 @@ type AccountValues = {
  avatar_static: string;
  [key: string]: any;
};
export type Account = MastodonMap<AccountValues>;

export type Account = Record<AccountValues>;

A app/javascript/types/util.ts => app/javascript/types/util.ts +1 -0
@@ 0,0 1,1 @@
export type ValueOf<T> = T[keyof T];

M app/lib/activity_tracker.rb => app/lib/activity_tracker.rb +1 -1
@@ 43,7 43,7 @@ class ActivityTracker

    case @type
    when :basic
      redis.mget(*keys).map(&:to_i).sum
      redis.mget(*keys).sum(&:to_i)
    when :unique
      redis.pfcount(*keys)
    end

M app/lib/activitypub/activity/announce.rb => app/lib/activitypub/activity/announce.rb +1 -1
@@ 4,7 4,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
  def perform
    return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?

    with_lock("announce:#{value_or_id(@object)}") do
    with_redis_lock("announce:#{value_or_id(@object)}") do
      original_status = status_from_object

      return reject_payload! if original_status.nil? || !announceable?(original_status)

M app/lib/activitypub/activity/create.rb => app/lib/activitypub/activity/create.rb +2 -2
@@ 47,7 47,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
  def create_status
    return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity?

    with_lock("create:#{object_uri}") do
    with_redis_lock("create:#{object_uri}") do
      return if delete_arrived_first?(object_uri) || poll_vote?

      @status = find_existing_status


@@ 313,7 313,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
    poll = replied_to_status.preloadable_poll
    already_voted = true

    with_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do
    with_redis_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do
      already_voted = poll.votes.where(account: @account).exists?
      poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
    end

M app/lib/activitypub/activity/delete.rb => app/lib/activitypub/activity/delete.rb +3 -3
@@ 12,7 12,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
  private

  def delete_person
    with_lock("delete_in_progress:#{@account.id}", autorelease: 2.hours, raise_on_failure: false) do
    with_redis_lock("delete_in_progress:#{@account.id}", autorelease: 2.hours, raise_on_failure: false) do
      DeleteAccountService.new.call(@account, reserve_username: false, skip_activitypub: true)
    end
  end


@@ 20,14 20,14 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
  def delete_note
    return if object_uri.nil?

    with_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
    with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
      unless non_matching_uri_hosts?(@account.uri, object_uri)
        # This lock ensures a concurrent `ActivityPub::Activity::Create` either
        # does not create a status at all, or has finished saving it to the
        # database before we try to load it.
        # Without the lock, `delete_later!` could be called after `delete_arrived_first?`
        # and `Status.find` before `Status.create!`
        with_lock("create:#{object_uri}") { delete_later!(object_uri) }
        with_redis_lock("create:#{object_uri}") { delete_later!(object_uri) }

        Tombstone.find_or_create_by(uri: object_uri, account: @account)
      end

M app/lib/activitypub/case_transform.rb => app/lib/activitypub/case_transform.rb +1 -1
@@ 13,7 13,7 @@ module ActivityPub::CaseTransform
      when Symbol then camel_lower(value.to_s).to_sym
      when String
        camel_lower_cache[value] ||= if value.start_with?('_:')
                                       "_:#{value.gsub(/\A_:/, '').underscore.camelize(:lower)}"
                                       "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
                                     else
                                       value.underscore.camelize(:lower)
                                     end

M app/lib/feed_manager.rb => app/lib/feed_manager.rb +5 -5
@@ 407,10 407,10 @@ class FeedManager
    return true  if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)

    check_for_blocks = crutches[:active_mentions][status.id] || []
    check_for_blocks.concat([status.account_id])
    check_for_blocks.push(status.account_id)

    if status.reblog?
      check_for_blocks.concat([status.reblog.account_id])
      check_for_blocks.push(status.reblog.account_id)
      check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || [])
    end



@@ 446,7 446,7 @@ class FeedManager
    # the notification has been checked for mute/block. Therefore, it's not
    # necessary to check the author of the toot for mute/block again
    check_for_blocks = status.active_mentions.pluck(:account_id)
    check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
    check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil?

    should_filter   = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions)                                                         # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
    should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them


@@ 593,10 593,10 @@ class FeedManager

    check_for_blocks = statuses.flat_map do |s|
      arr = crutches[:active_mentions][s.id] || []
      arr.concat([s.account_id])
      arr.push(s.account_id)

      if s.reblog?
        arr.concat([s.reblog.account_id])
        arr.push(s.reblog.account_id)
        arr.concat(crutches[:active_mentions][s.reblog_of_id] || [])
      end


M app/lib/importer/accounts_index_importer.rb => app/lib/importer/accounts_index_importer.rb +2 -2
@@ 6,8 6,8 @@ class Importer::AccountsIndexImporter < Importer::BaseImporter
      in_work_unit(tmp) do |accounts|
        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body

        indexed = bulk.select { |entry| entry[:index] }.size
        deleted = bulk.select { |entry| entry[:delete] }.size
        indexed = bulk.count { |entry| entry[:index] }
        deleted = bulk.count { |entry| entry[:delete] }

        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)


M app/lib/importer/tags_index_importer.rb => app/lib/importer/tags_index_importer.rb +2 -2
@@ 6,8 6,8 @@ class Importer::TagsIndexImporter < Importer::BaseImporter
      in_work_unit(tmp) do |tags|
        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body

        indexed = bulk.select { |entry| entry[:index] }.size
        deleted = bulk.select { |entry| entry[:delete] }.size
        indexed = bulk.count { |entry| entry[:index] }
        deleted = bulk.count { |entry| entry[:delete] }

        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)


M app/lib/permalink_redirector.rb => app/lib/permalink_redirector.rb +39 -9
@@ 8,21 8,51 @@ class PermalinkRedirector
  end

  def redirect_path
    if path_segments[0].present? && path_segments[0].start_with?('@') && path_segments[1] =~ /\d/
      find_status_url_by_id(path_segments[1])
    elsif path_segments[0].present? && path_segments[0].start_with?('@')
      find_account_url_by_name(path_segments[0])
    elsif path_segments[0] == 'statuses' && path_segments[1] =~ /\d/
      find_status_url_by_id(path_segments[1])
    elsif path_segments[0] == 'accounts' && path_segments[1] =~ /\d/
      find_account_url_by_id(path_segments[1])
    if at_username_status_request? || statuses_status_request?
      find_status_url_by_id(second_segment)
    elsif at_username_request?
      find_account_url_by_name(first_segment)
    elsif accounts_request? && record_integer_id_request?
      find_account_url_by_id(second_segment)
    end
  end

  private

  def at_username_status_request?
    at_username_request? && record_integer_id_request?
  end

  def statuses_status_request?
    statuses_request? && record_integer_id_request?
  end

  def at_username_request?
    first_segment.present? && first_segment.start_with?('@')
  end

  def statuses_request?
    first_segment == 'statuses'
  end

  def accounts_request?
    first_segment == 'accounts'
  end

  def record_integer_id_request?
    second_segment =~ /\d/
  end

  def first_segment
    path_segments.first
  end

  def second_segment
    path_segments.second
  end

  def path_segments
    @path_segments ||= @path.gsub(/\A\//, '').split('/')
    @path_segments ||= @path.delete_prefix('/').split('/')
  end

  def find_status_url_by_id(id)

A app/lib/vacuum/imports_vacuum.rb => app/lib/vacuum/imports_vacuum.rb +18 -0
@@ 0,0 1,18 @@
# frozen_string_literal: true

class Vacuum::ImportsVacuum
  def perform
    clean_unconfirmed_imports!
    clean_old_imports!
  end

  private

  def clean_unconfirmed_imports!
    BulkImport.where(state: :unconfirmed).where('created_at <= ?', 10.minutes.ago).reorder(nil).in_batches.delete_all
  end

  def clean_old_imports!
    BulkImport.where('created_at <= ?', 1.week.ago).reorder(nil).in_batches.delete_all
  end
end

M app/lib/webfinger_resource.rb => app/lib/webfinger_resource.rb +1 -1
@@ 57,7 57,7 @@ class WebfingerResource
  end

  def resource_without_acct_string
    resource.gsub(/\Aacct:/, '')
    resource.delete_prefix('acct:')
  end

  def local_username

M app/models/account.rb => app/models/account.rb +2 -98
@@ 78,6 78,7 @@ class Account < ApplicationRecord
  include DomainNormalizable
  include DomainMaterializable
  include AccountMerging
  include AccountSearch

  MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
  MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i


@@ 408,14 409,6 @@ class Account < ApplicationRecord
  end

  class << self
    DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/
    TEXTSEARCH = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"

    REPUTATION_SCORE_FUNCTION = '(greatest(0, coalesce(s.followers_count, 0)) / (greatest(0, coalesce(s.following_count, 0)) + 1.0))'
    FOLLOWERS_SCORE_FUNCTION  = 'log(greatest(0, coalesce(s.followers_count, 0)) + 2)'
    TIME_DISTANCE_FUNCTION    = '(case when s.last_status_at is null then 0 else exp(-1.0 * ((greatest(0, abs(extract(DAY FROM age(s.last_status_at))) - 30.0)^2) / (2.0 * ((-1.0 * 30^2) / (2.0 * ln(0.3)))))) end)'
    BOOST                     = "((#{REPUTATION_SCORE_FUNCTION} + #{FOLLOWERS_SCORE_FUNCTION} + #{TIME_DISTANCE_FUNCTION}) / 3.0)"

    def readonly_attributes
      super - %w(statuses_count following_count followers_count)
    end


@@ 425,37 418,6 @@ class Account < ApplicationRecord
      DeliveryFailureTracker.without_unavailable(urls)
    end

    def search_for(terms, limit: 10, offset: 0)
      tsquery = generate_query_for_search(terms)

      sql = <<-SQL.squish
        SELECT
          accounts.*,
          #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
        FROM accounts
        LEFT JOIN users ON accounts.id = users.account_id
        LEFT JOIN account_stats AS s ON accounts.id = s.account_id
        WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
          AND accounts.suspended_at IS NULL
          AND accounts.moved_to_account_id IS NULL
          AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
        ORDER BY rank DESC
        LIMIT :limit OFFSET :offset
      SQL

      records = find_by_sql([sql, limit: limit, offset: offset, tsquery: tsquery])
      ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
      records
    end

    def advanced_search_for(terms, account, limit: 10, following: false, offset: 0)
      tsquery = generate_query_for_search(terms)
      sql = advanced_search_for_sql_template(following)
      records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])
      ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
      records
    end

    def from_text(text)
      return [] if text.blank?



@@ 469,73 431,15 @@ class Account < ApplicationRecord
        EntityCache.instance.mention(username, domain)
      end
    end

    private

    def generate_query_for_search(unsanitized_terms)
      terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')

      # The final ":*" is for prefix search.
      # The trailing space does not seem to fit any purpose, but `to_tsquery`
      # behaves differently with and without a leading space if the terms start
      # with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
      # the same query.
      "' #{terms} ':*"
    end

    def advanced_search_for_sql_template(following)
      if following
        <<-SQL.squish
          WITH first_degree AS (
            SELECT target_account_id
            FROM follows
            WHERE account_id = :id
            UNION ALL
            SELECT :id
          )
          SELECT
            accounts.*,
            (count(f.id) + 1) * #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
          FROM accounts
          LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id)
          LEFT JOIN account_stats AS s ON accounts.id = s.account_id
          WHERE accounts.id IN (SELECT * FROM first_degree)
            AND to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
            AND accounts.suspended_at IS NULL
            AND accounts.moved_to_account_id IS NULL
          GROUP BY accounts.id, s.id
          ORDER BY rank DESC
          LIMIT :limit OFFSET :offset
        SQL
      else
        <<-SQL.squish
          SELECT
            accounts.*,
            #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank,
            count(f.id) AS followed
          FROM accounts
          LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
          LEFT JOIN users ON accounts.id = users.account_id
          LEFT JOIN account_stats AS s ON accounts.id = s.account_id
          WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
            AND accounts.suspended_at IS NULL
            AND accounts.moved_to_account_id IS NULL
            AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
          GROUP BY accounts.id, s.id
          ORDER BY followed DESC, rank DESC
          LIMIT :limit OFFSET :offset
        SQL
      end
    end
  end

  def emojis
    @emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
  end

  before_create :generate_keys
  before_validation :prepare_contents, if: :local?
  before_validation :prepare_username, on: :create
  before_create :generate_keys
  before_destroy :clean_feed_manager

  def ensure_keys!

M app/models/account_conversation.rb => app/models/account_conversation.rb +1 -2
@@ 17,14 17,13 @@
class AccountConversation < ApplicationRecord
  include Redisable

  before_validation :set_last_status
  after_commit :push_to_streaming_api

  belongs_to :account
  belongs_to :conversation
  belongs_to :last_status, class_name: 'Status'

  before_validation :set_last_status

  def participant_account_ids=(arr)
    self[:participant_account_ids] = arr.sort
  end

M app/models/account_migration.rb => app/models/account_migration.rb +1 -1
@@ 42,7 42,7 @@ class AccountMigration < ApplicationRecord

    return false unless errors.empty?

    with_lock("account_migration:#{account.id}") do
    with_redis_lock("account_migration:#{account.id}") do
      save
    end
  end

M app/models/account_statuses_filter.rb => app/models/account_statuses_filter.rb +3 -3
@@ 32,9 32,9 @@ class AccountStatusesFilter
  private

  def initial_scope
    if suspended?
      Status.none
    elsif anonymous?
    return Status.none if suspended?

    if anonymous?
      account.statuses.not_local_only.where(visibility: %i(public unlisted))
    elsif author?
      account.statuses.all # NOTE: #merge! does not work without the #all

M app/models/account_suggestions/source.rb => app/models/account_suggestions/source.rb +1 -1
@@ 18,7 18,7 @@ class AccountSuggestions::Source
  def as_ordered_suggestions(scope, ordered_list)
    return [] if ordered_list.empty?

    map = scope.index_by(&method(:to_ordered_list_key))
    map = scope.index_by { |account| to_ordered_list_key(account) }

    ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account|
      AccountSuggestions::Suggestion.new(

M app/models/admin/appeal_filter.rb => app/models/admin/appeal_filter.rb +3 -1
@@ 5,6 5,8 @@ class Admin::AppealFilter
    status
  ).freeze

  IGNORED_PARAMS = %w(page).freeze

  attr_reader :params

  def initialize(params)


@@ 15,7 17,7 @@ class Admin::AppealFilter
    scope = Appeal.order(id: :desc)

    params.each do |key, value|
      next if %w(page).include?(key.to_s)
      next if IGNORED_PARAMS.include?(key.to_s)

      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
    end

M app/models/admin/status_filter.rb => app/models/admin/status_filter.rb +3 -1
@@ 6,6 6,8 @@ class Admin::StatusFilter
    report_id
  ).freeze

  IGNORED_PARAMS = %w(page report_id).freeze

  attr_reader :params

  def initialize(account, params)


@@ 17,7 19,7 @@ class Admin::StatusFilter
    scope = @account.statuses.where(visibility: [:public, :unlisted])

    params.each do |key, value|
      next if %w(page report_id).include?(key.to_s)
      next if IGNORED_PARAMS.include?(key.to_s)

      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
    end

M app/models/announcement_reaction.rb => app/models/announcement_reaction.rb +1 -2
@@ 14,6 14,7 @@
#

class AnnouncementReaction < ApplicationRecord
  before_validation :set_custom_emoji
  after_commit :queue_publish

  belongs_to :account


@@ 23,8 24,6 @@ class AnnouncementReaction < ApplicationRecord
  validates :name, presence: true
  validates_with ReactionValidator

  before_validation :set_custom_emoji

  private

  def set_custom_emoji

M app/models/block.rb => app/models/block.rb +1 -1
@@ 25,8 25,8 @@ class Block < ApplicationRecord
    false # Force uri_for to use uri attribute
  end

  after_commit :remove_blocking_cache
  before_validation :set_uri, only: :create
  after_commit :remove_blocking_cache

  private


A app/models/bulk_import.rb => app/models/bulk_import.rb +53 -0
@@ 0,0 1,53 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: bulk_imports
#
#  id                :bigint(8)        not null, primary key
#  type              :integer          not null
#  state             :integer          not null
#  total_items       :integer          default(0), not null
#  imported_items    :integer          default(0), not null
#  processed_items   :integer          default(0), not null
#  finished_at       :datetime
#  overwrite         :boolean          default(FALSE), not null
#  likely_mismatched :boolean          default(FALSE), not null
#  original_filename :string           default(""), not null
#  account_id        :bigint(8)        not null
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#
class BulkImport < ApplicationRecord
  self.inheritance_column = false

  belongs_to :account
  has_many :rows, class_name: 'BulkImportRow', inverse_of: :bulk_import, dependent: :delete_all

  enum type: {
    following: 0,
    blocking: 1,
    muting: 2,
    domain_blocking: 3,
    bookmarks: 4,
  }

  enum state: {
    unconfirmed: 0,
    scheduled: 1,
    in_progress: 2,
    finished: 3,
  }

  validates :type, presence: true

  def self.progress!(bulk_import_id, imported: false)
    # Use `increment_counter` so that the incrementation is done atomically in the database
    BulkImport.increment_counter(:processed_items, bulk_import_id) # rubocop:disable Rails/SkipsModelValidations
    BulkImport.increment_counter(:imported_items, bulk_import_id) if imported # rubocop:disable Rails/SkipsModelValidations

    # Since the incrementation has been done atomically, concurrent access to `bulk_import` is now bening
    bulk_import = BulkImport.find(bulk_import_id)
    bulk_import.update!(state: :finished, finished_at: Time.now.utc) if bulk_import.processed_items == bulk_import.total_items
  end
end

A app/models/bulk_import_row.rb => app/models/bulk_import_row.rb +15 -0
@@ 0,0 1,15 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: bulk_import_rows
#
#  id             :bigint(8)        not null, primary key
#  bulk_import_id :bigint(8)        not null
#  data           :jsonb
#  created_at     :datetime         not null
#  updated_at     :datetime         not null
#
class BulkImportRow < ApplicationRecord
  belongs_to :bulk_import
end

M app/models/concerns/account_associations.rb => app/models/concerns/account_associations.rb +3 -0
@@ 68,5 68,8 @@ module AccountAssociations

    # Account statuses cleanup policy
    has_one :statuses_cleanup_policy, class_name: 'AccountStatusesCleanupPolicy', inverse_of: :account, dependent: :destroy

    # Imports
    has_many :bulk_imports, inverse_of: :account, dependent: :delete_all
  end
end

M app/models/concerns/account_interactions.rb => app/models/concerns/account_interactions.rb +2 -1
@@ 271,7 271,8 @@ module AccountInteractions
  end

  def lists_for_local_distribution
    lists.joins(account: :user)
    scope = lists.joins(account: :user)
    scope.where.not(list_accounts: { follow_id: nil }).or(scope.where(account_id: id))
         .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
  end


A app/models/concerns/account_search.rb => app/models/concerns/account_search.rb +140 -0
@@ 0,0 1,140 @@
# frozen_string_literal: true

module AccountSearch
  extend ActiveSupport::Concern

  DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/

  TEXT_SEARCH_RANKS = <<~SQL.squish
    (
        setweight(to_tsvector('simple', accounts.display_name), 'A') ||
        setweight(to_tsvector('simple', accounts.username), 'B') ||
        setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C')
    )
  SQL

  REPUTATION_SCORE_FUNCTION = <<~SQL.squish
    (
        greatest(0, coalesce(s.followers_count, 0)) / (
            greatest(0, coalesce(s.following_count, 0)) + 1.0
        )
    )
  SQL

  FOLLOWERS_SCORE_FUNCTION = <<~SQL.squish
    log(
        greatest(0, coalesce(s.followers_count, 0)) + 2
    )
  SQL

  TIME_DISTANCE_FUNCTION = <<~SQL.squish
    (
        case
            when s.last_status_at is null then 0
            else exp(
                -1.0 * (
                    (
                        greatest(0, abs(extract(DAY FROM age(s.last_status_at))) - 30.0)^2) /#{' '}
                        (2.0 * ((-1.0 * 30^2) / (2.0 * ln(0.3)))
                    )
                )
            )
        end
    )
  SQL

  BOOST = <<~SQL.squish
    (
        (#{REPUTATION_SCORE_FUNCTION} + #{FOLLOWERS_SCORE_FUNCTION} + #{TIME_DISTANCE_FUNCTION}) / 3.0
    )
  SQL

  BASIC_SEARCH_SQL = <<~SQL.squish
    SELECT
      accounts.*,
      #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank
    FROM accounts
    LEFT JOIN users ON accounts.id = users.account_id
    LEFT JOIN account_stats AS s ON accounts.id = s.account_id
    WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
      AND accounts.suspended_at IS NULL
      AND accounts.moved_to_account_id IS NULL
      AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
    ORDER BY rank DESC
    LIMIT :limit OFFSET :offset
  SQL

  ADVANCED_SEARCH_WITH_FOLLOWING = <<~SQL.squish
    WITH first_degree AS (
      SELECT target_account_id
      FROM follows
      WHERE account_id = :id
      UNION ALL
      SELECT :id
    )
    SELECT
      accounts.*,
      (count(f.id) + 1) * #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank
    FROM accounts
    LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id)
    LEFT JOIN account_stats AS s ON accounts.id = s.account_id
    WHERE accounts.id IN (SELECT * FROM first_degree)
      AND to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
      AND accounts.suspended_at IS NULL
      AND accounts.moved_to_account_id IS NULL
    GROUP BY accounts.id, s.id
    ORDER BY rank DESC
    LIMIT :limit OFFSET :offset
  SQL

  ADVANCED_SEARCH_WITHOUT_FOLLOWING = <<~SQL.squish
    SELECT
      accounts.*,
      #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank,
      count(f.id) AS followed
    FROM accounts
    LEFT OUTER JOIN follows AS f ON
      (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
    LEFT JOIN users ON accounts.id = users.account_id
    LEFT JOIN account_stats AS s ON accounts.id = s.account_id
    WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
      AND accounts.suspended_at IS NULL
      AND accounts.moved_to_account_id IS NULL
      AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
    GROUP BY accounts.id, s.id
    ORDER BY followed DESC, rank DESC
    LIMIT :limit OFFSET :offset
  SQL

  class_methods do
    def search_for(terms, limit: 10, offset: 0)
      tsquery = generate_query_for_search(terms)

      find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records|
        ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
      end
    end

    def advanced_search_for(terms, account, limit: 10, following: false, offset: 0)
      tsquery = generate_query_for_search(terms)
      sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING

      find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records|
        ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
      end
    end

    private

    def generate_query_for_search(unsanitized_terms)
      terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')

      # The final ":*" is for prefix search.
      # The trailing space does not seem to fit any purpose, but `to_tsquery`
      # behaves differently with and without a leading space if the terms start
      # with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
      # the same query.
      "' #{terms} ':*"
    end
  end
end

M app/models/concerns/lockable.rb => app/models/concerns/lockable.rb +1 -1
@@ 5,7 5,7 @@ module Lockable
  # @param [ActiveSupport::Duration] autorelease Automatically release the lock after this time
  # @param [Boolean] raise_on_failure Raise an error if a lock cannot be acquired, or fail silently
  # @raise [Mastodon::RaceConditionError]
  def with_lock(lock_name, autorelease: 15.minutes, raise_on_failure: true)
  def with_redis_lock(lock_name, autorelease: 15.minutes, raise_on_failure: true)
    with_redis do |redis|
      RedisLock.acquire(redis: redis, key: "lock:#{lock_name}", autorelease: autorelease.seconds) do |lock|
        if lock.acquired?

A app/models/concerns/status_safe_reblog_insert.rb => app/models/concerns/status_safe_reblog_insert.rb +72 -0
@@ 0,0 1,72 @@
# frozen_string_literal: true

module StatusSafeReblogInsert
  extend ActiveSupport::Concern

  class_methods do
    # This is a hack to ensure that no reblogs of discarded statuses are created,
    # as this cannot be enforced through database constraints the same way we do
    # for reblogs of deleted statuses.
    #
    # To achieve this, we redefine the internal method responsible for issuing
    # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query
    # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL"
    # clause on the reblogged status to ensure consistency at the database level.
    #
    # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence
    # code, and actually calls it if we are not handling a reblog.
    def _insert_record(values)
      return super unless values.is_a?(Hash) && values['reblog_of_id'].present?

      primary_key = self.primary_key
      primary_key_value = nil

      if primary_key
        primary_key_value = values[primary_key]

        if !primary_key_value && prefetch_primary_key?
          primary_key_value = next_sequence_value
          values[primary_key] = primary_key_value
        end
      end

      # The following line is where we differ from stock ActiveRecord implementation
      im = _compile_reblog_insert(values)

      # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible.
      # For our purposes, it's equivalent to a foreign key constraint violation
      result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
      raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil?

      result
    end

    def _compile_reblog_insert(values)
      # This is somewhat equivalent to the following code of ActiveRecord::Persistence:
      # `arel_table.compile_insert(_substitute_values(values))`
      # The main difference is that we use a `SELECT` instead of a `VALUES` clause,
      # which means we have to build the `SELECT` clause ourselves and do a bit more
      # manual work.

      # Instead of using Arel::InsertManager#values, we are going to use Arel::InsertManager#select
      im = Arel::InsertManager.new
      im.into(arel_table)

      binds = []
      reblog_bind = nil
      values.each do |name, value|
        attr = arel_table[name]
        bind = predicate_builder.build_bind_attribute(attr.name, value)

        im.columns << attr
        binds << bind

        reblog_bind = bind if name == 'reblog_of_id'
      end

      im.select(arel_table.where(arel_table[:id].eq(reblog_bind)).where(arel_table[:deleted_at].eq(nil)).project(*binds))

      im
    end
  end
end

M app/models/follow_request.rb => app/models/follow_request.rb +2 -1
@@ 32,7 32,8 @@ class FollowRequest < ApplicationRecord
  validates :languages, language: true

  def authorize!
    account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true)
    follow = account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true)
    ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id) # rubocop:disable Rails/SkipsModelValidations
    MergeWorker.perform_async(target_account.id, account.id) if account.local?
    destroy!
  end

A app/models/form/import.rb => app/models/form/import.rb +151 -0
@@ 0,0 1,151 @@
# frozen_string_literal: true

require 'csv'

# A non-ActiveRecord helper class for CSV uploads.
# Handles saving contents to database.
class Form::Import
  include ActiveModel::Model

  MODES = %i(merge overwrite).freeze

  FILE_SIZE_LIMIT       = 20.megabytes
  ROWS_PROCESSING_LIMIT = 20_000

  EXPECTED_HEADERS_BY_TYPE = {
    following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'],
    blocking: ['Account address'],
    muting: ['Account address', 'Hide notifications'],
    domain_blocking: ['#domain'],
    bookmarks: ['#uri'],
  }.freeze

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

  ATTRIBUTE_BY_HEADER = {
    'Account address' => 'acct',
    'Show boosts' => 'show_reblogs',
    'Notify on new posts' => 'notify',
    'Languages' => 'languages',
    'Hide notifications' => 'hide_notifications',
    '#domain' => 'domain',
    '#uri' => 'uri',
  }.freeze

  class EmptyFileError < StandardError; end

  attr_accessor :current_account, :data, :type, :overwrite, :bulk_import

  validates :type, presence: true
  validates :data, presence: true
  validate :validate_data

  def guessed_type
    return :muting if csv_data.headers.include?('Hide notifications')
    return :following if csv_data.headers.include?('Show boosts') || csv_data.headers.include?('Notify on new posts') || csv_data.headers.include?('Languages')
    return :following if data.original_filename&.start_with?('follows') || data.original_filename&.start_with?('following_accounts')
    return :blocking if data.original_filename&.start_with?('blocks') || data.original_filename&.start_with?('blocked_accounts')
    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')
  end

  # Whether the uploaded CSV file seems to correspond to a different import type than the one selected
  def likely_mismatched?
    guessed_type.present? && guessed_type != type.to_sym
  end

  def save
    return false unless valid?

    ApplicationRecord.transaction do
      now = Time.now.utc
      @bulk_import = current_account.bulk_imports.create(type: type, overwrite: overwrite || false, state: :unconfirmed, original_filename: data.original_filename, likely_mismatched: likely_mismatched?)
      nb_items = BulkImportRow.insert_all(parsed_rows.map { |row| { bulk_import_id: bulk_import.id, data: row, created_at: now, updated_at: now } }).length # rubocop:disable Rails/SkipsModelValidations
      @bulk_import.update(total_items: nb_items)
    end
  end

  def mode
    overwrite ? :overwrite : :merge
  end

  def mode=(str)
    self.overwrite = str.to_sym == :overwrite
  end

  private

  def default_csv_header
    case type.to_sym
    when :following, :blocking, :muting
      'Account address'
    when :domain_blocking
      '#domain'
    when :bookmarks
      '#uri'
    end
  end

  def csv_data
    return @csv_data if defined?(@csv_data)

    csv_converter = lambda do |field, field_info|
      case field_info.header
      when 'Show boosts', 'Notify on new posts', 'Hide notifications'
        ActiveModel::Type::Boolean.new.cast(field)
      when 'Languages'
        field&.split(',')&.map(&:strip)&.presence
      when 'Account address'
        field.strip.gsub(/\A@/, '')
      when '#domain', '#uri'
        field.strip
      else
        field
      end
    end

    @csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: true, converters: csv_converter)
    @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
  end

  def csv_row_count
    return @csv_row_count if defined?(@csv_row_count)

    csv_data.rewind
    @csv_row_count = csv_data.take(ROWS_PROCESSING_LIMIT + 2).count
  end

  def parsed_rows
    csv_data.rewind

    expected_headers = EXPECTED_HEADERS_BY_TYPE[type.to_sym]

    csv_data.take(ROWS_PROCESSING_LIMIT + 1).map do |row|
      row.to_h.slice(*expected_headers).transform_keys { |key| ATTRIBUTE_BY_HEADER[key] }
    end
  end

  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)

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

    if type.to_sym == :following
      base_limit = FollowLimitValidator.limit_for_account(current_account)
      limit = base_limit
      limit -= current_account.following_count unless overwrite
      errors.add(:data, I18n.t('users.follow_limit_reached', limit: base_limit)) if csv_row_count > limit
    end
  rescue CSV::MalformedCSVError => e
    errors.add(:data, I18n.t('imports.errors.invalid_csv_file', error: e.message))
  rescue EmptyFileError
    errors.add(:data, I18n.t('imports.errors.empty'))
  end
end

M app/models/import.rb => app/models/import.rb +3 -1
@@ 17,6 17,9 @@
#  overwrite         :boolean          default(FALSE), not null
#

# NOTE: This is a deprecated model, only kept to not break ongoing imports
# on upgrade. See `BulkImport` and `Form::Import` for its replacements.

class Import < ApplicationRecord
  FILE_TYPES = %w(text/plain text/csv application/csv).freeze
  MODES = %i(merge overwrite).freeze


@@ 28,7 31,6 @@ class Import < ApplicationRecord
  enum type: { following: 0, blocking: 1, muting: 2, domain_blocking: 3, bookmarks: 4 }

  validates :type, presence: true
  validates_with ImportValidator, on: :create

  has_attached_file :data
  validates_attachment_content_type :data, content_type: FILE_TYPES

M app/models/list_account.rb => app/models/list_account.rb +20 -5
@@ 4,24 4,39 @@
#
# Table name: list_accounts
#
#  id         :bigint(8)        not null, primary key
#  list_id    :bigint(8)        not null
#  account_id :bigint(8)        not null
#  follow_id  :bigint(8)
#  id                :bigint(8)        not null, primary key
#  list_id           :bigint(8)        not null
#  account_id        :bigint(8)        not null
#  follow_id         :bigint(8)
#  follow_request_id :bigint(8)
#

class ListAccount < ApplicationRecord
  belongs_to :list
  belongs_to :account
  belongs_to :follow, optional: true
  belongs_to :follow_request, optional: true

  validates :account_id, uniqueness: { scope: :list_id }
  validate :validate_relationship

  before_validation :set_follow

  private

  def set_follow
    self.follow = Follow.find_by!(account_id: list.account_id, target_account_id: account.id) unless list.account_id == account.id
    return if list.account_id == account.id

    self.follow = Follow.find_by!(account_id: list.account_id, target_account_id: account.id)
  rescue ActiveRecord::RecordNotFound
    self.follow_request = FollowRequest.find_by!(account_id: list.account_id, target_account_id: account.id)
  end

  def validate_relationship
    return if list.account_id == account_id

    errors.add(:account_id, 'follow relationship missing') if follow_id.nil? && follow_request_id.nil?
    errors.add(:follow, 'mismatched accounts') if follow_id.present? && follow.target_account_id != account_id
    errors.add(:follow_request, 'mismatched accounts') if follow_request_id.present? && follow_request.target_account_id != account_id
  end
end

M app/models/media_attachment.rb => app/models/media_attachment.rb +8 -6
@@ 34,8 34,8 @@ class MediaAttachment < ApplicationRecord

  include Attachmentable

  enum type: { :image => 0, :gifv => 1, :video => 2, :unknown => 3, :audio => 4 }
  enum processing: { :queued => 0, :in_progress => 1, :complete => 2, :failed => 3 }, _prefix: true
  enum type: { image: 0, gifv: 1, video: 2, unknown: 3, audio: 4 }
  enum processing: { queued: 0, in_progress: 1, complete: 2, failed: 3 }, _prefix: true

  MAX_DESCRIPTION_LENGTH = 1_500



@@ 135,7 135,7 @@ class MediaAttachment < ApplicationRecord
      convert_options: {
        output: {
          'loglevel' => 'fatal',
          vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
          :vf => 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
        }.freeze,
      }.freeze,
      format: 'png',


@@ 169,6 169,8 @@ class MediaAttachment < ApplicationRecord
    original: IMAGE_STYLES[:small].freeze,
  }.freeze

  DEFAULT_STYLES = [:original].freeze

  GLOBAL_CONVERT_OPTIONS = {
    all: '-quality 90 +profile "!icc,*" +set modify-date +set create-date',
  }.freeze


@@ 271,12 273,12 @@ class MediaAttachment < ApplicationRecord
    delay_processing? && attachment_name == :file
  end

  after_commit :enqueue_processing, on: :create
  after_commit :reset_parent_cache, on: :update

  before_create :set_unknown_type
  before_create :set_processing

  after_commit :enqueue_processing, on: :create
  after_commit :reset_parent_cache, on: :update

  after_post_process :set_meta

  class << self

M app/models/relationship_filter.rb => app/models/relationship_filter.rb +3 -1
@@ 10,6 10,8 @@ class RelationshipFilter
    location
  ).freeze

  IGNORED_PARAMS = %w(relationship page).freeze

  attr_reader :params, :account

  def initialize(account, params)


@@ 23,7 25,7 @@ class RelationshipFilter
    scope = scope_for('relationship', params['relationship'].to_s.strip)

    params.each do |key, value|
      next if %w(relationship page).include?(key)
      next if IGNORED_PARAMS.include?(key)

      scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
    end

M app/models/session_activation.rb => app/models/session_activation.rb +1 -1
@@ 36,8 36,8 @@ class SessionActivation < ApplicationRecord
    detection.platform.id
  end

  before_create :assign_access_token
  before_save   :assign_user_agent
  before_create :assign_access_token

  class << self
    def active?(id)

M app/models/status.rb => app/models/status.rb +24 -85
@@ 32,14 32,13 @@
#

class Status < ApplicationRecord
  before_destroy :unlink_from_conversations!

  include Discard::Model
  include Paginable
  include Cacheable
  include StatusThreadingConcern
  include StatusSnapshotConcern
  include RateLimitable
  include StatusSafeReblogInsert

  rate_limit by: :account, family: :statuses



@@ 119,6 118,28 @@ class Status < ApplicationRecord
  after_create_commit :trigger_create_webhooks
  after_update_commit :trigger_update_webhooks

  after_create_commit  :increment_counter_caches
  after_destroy_commit :decrement_counter_caches

  after_create_commit :store_uri, if: :local?
  after_create_commit :update_statistics, if: :local?

  before_validation :prepare_contents, if: :local?
  before_validation :set_reblog
  before_validation :set_visibility
  before_validation :set_conversation
  before_validation :set_local

  before_create :set_local_only

  around_create Mastodon::Snowflake::Callbacks

  after_create :set_poll_id

  # The `prepend: true` option below ensures this runs before
  # the `dependent: destroy` callbacks remove relevant records
  before_destroy :unlink_from_conversations!, prepend: true

  cache_associated :application,
                   :media_attachments,
                   :conversation,


@@ 316,23 337,6 @@ class Status < ApplicationRecord
    attributes['trendable'].nil? && account.requires_review_notification?
  end

  after_create_commit  :increment_counter_caches
  after_destroy_commit :decrement_counter_caches

  after_create_commit :store_uri, if: :local?
  after_create_commit :update_statistics, if: :local?

  before_validation :prepare_contents, if: :local?
  before_validation :set_reblog
  before_validation :set_visibility
  before_validation :set_conversation
  before_validation :set_local
  before_create :set_locality

  around_create Mastodon::Snowflake::Callbacks

  after_create :set_poll_id

  class << self
    def selectable_visibilities
      visibilities.keys - %w(direct limited)


@@ 442,71 446,6 @@ class Status < ApplicationRecord
    super || build_status_stat
  end

  # This is a hack to ensure that no reblogs of discarded statuses are created,
  # as this cannot be enforced through database constraints the same way we do
  # for reblogs of deleted statuses.
  #
  # To achieve this, we redefine the internal method responsible for issuing
  # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query
  # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL"
  # clause on the reblogged status to ensure consistency at the database level.
  #
  # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence
  # code, and actually calls it if we are not handling a reblog.
  def self._insert_record(values)
    return super unless values.is_a?(Hash) && values['reblog_of_id'].present?

    primary_key = self.primary_key
    primary_key_value = nil

    if primary_key
      primary_key_value = values[primary_key]

      if !primary_key_value && prefetch_primary_key?
        primary_key_value = next_sequence_value
        values[primary_key] = primary_key_value
      end
    end

    # The following line is where we differ from stock ActiveRecord implementation
    im = _compile_reblog_insert(values)

    # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible.
    # For our purposes, it's equivalent to a foreign key constraint violation
    result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
    raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil?

    result
  end

  def self._compile_reblog_insert(values)
    # This is somewhat equivalent to the following code of ActiveRecord::Persistence:
    # `arel_table.compile_insert(_substitute_values(values))`
    # The main difference is that we use a `SELECT` instead of a `VALUES` clause,
    # which means we have to build the `SELECT` clause ourselves and do a bit more
    # manual work.

    # Instead of using Arel::InsertManager#values, we are going to use Arel::InsertManager#select
    im = Arel::InsertManager.new
    im.into(arel_table)

    binds = []
    reblog_bind = nil
    values.each do |name, value|
      attr = arel_table[name]
      bind = predicate_builder.build_bind_attribute(attr.name, value)

      im.columns << attr
      binds << bind

      reblog_bind = bind if name == 'reblog_of_id'
    end

    im.select(arel_table.where(arel_table[:id].eq(reblog_bind)).where(arel_table[:deleted_at].eq(nil)).project(*binds))

    im
  end

  def discard_with_reblogs
    discard_time = Time.current
    Status.unscoped.where(reblog_of_id: id, deleted_at: [nil, deleted_at]).in_batches.update_all(deleted_at: discard_time) unless reblog?


@@ 555,7 494,7 @@ class Status < ApplicationRecord
    self.sensitive  = false if sensitive.nil?
  end

  def set_locality
  def set_local_only
    return unless account.domain.nil? && !attribute_changed?(:local_only)

    self.local_only = marked_local_only?

M app/models/trends/history.rb => app/models/trends/history.rb +1 -1
@@ 11,7 11,7 @@ class Trends::History
    end

    def uses
      with_redis { |redis| redis.mget(*@days.map { |day| day.key_for(:uses) }).map(&:to_i).sum }
      with_redis { |redis| redis.mget(*@days.map { |day| day.key_for(:uses) }).sum(&:to_i) }
    end

    def accounts

M app/models/trends/preview_card_filter.rb => app/models/trends/preview_card_filter.rb +3 -1
@@ 6,6 6,8 @@ class Trends::PreviewCardFilter
    locale
  ).freeze

  IGNORED_PARAMS = %w(page).freeze

  attr_reader :params

  def initialize(params)


@@ 16,7 18,7 @@ class Trends::PreviewCardFilter
    scope = initial_scope

    params.each do |key, value|
      next if %w(page).include?(key.to_s)
      next if IGNORED_PARAMS.include?(key.to_s)

      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
    end

M app/models/trends/status_filter.rb => app/models/trends/status_filter.rb +3 -1
@@ 6,6 6,8 @@ class Trends::StatusFilter
    locale
  ).freeze

  IGNORED_PARAMS = %w(page).freeze

  attr_reader :params

  def initialize(params)


@@ 16,7 18,7 @@ class Trends::StatusFilter
    scope = initial_scope

    params.each do |key, value|
      next if %w(page).include?(key.to_s)
      next if IGNORED_PARAMS.include?(key.to_s)

      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
    end

M app/presenters/status_relationships_presenter.rb => app/presenters/status_relationships_presenter.rb +3 -1
@@ 1,6 1,8 @@
# frozen_string_literal: true

class StatusRelationshipsPresenter
  PINNABLE_VISIBILITIES = %w(public unlisted private).freeze

  attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map,
              :bookmarks_map, :filters_map



@@ 16,7 18,7 @@ class StatusRelationshipsPresenter
      statuses            = statuses.compact
      status_ids          = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
      conversation_ids    = statuses.filter_map(&:conversation_id).uniq
      pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) }
      pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) }

      @filters_map     = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {})
      @reblogs_map     = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})

M app/services/activitypub/fetch_remote_actor_service.rb => app/services/activitypub/fetch_remote_actor_service.rb +1 -1
@@ 67,7 67,7 @@ class ActivityPub::FetchRemoteActorService < BaseService
  end

  def split_acct(acct)
    acct.gsub(/\Aacct:/, '').split('@')
    acct.delete_prefix('acct:').split('@')
  end

  def supported_context?

M app/services/activitypub/fetch_remote_status_service.rb => app/services/activitypub/fetch_remote_status_service.rb +3 -0
@@ 2,12 2,15 @@

class ActivityPub::FetchRemoteStatusService < BaseService
  include JsonLdHelper
  include DomainControlHelper
  include Redisable

  DISCOVERIES_PER_REQUEST = 1000

  # Should be called when uri has already been checked for locality
  def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
    return if domain_not_allowed?(uri)

    @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
    @json = if prefetched_body.nil?
              fetch_resource(uri, id, on_behalf_of)

M app/services/activitypub/process_account_service.rb => app/services/activitypub/process_account_service.rb +1 -1
@@ 24,7 24,7 @@ class ActivityPub::ProcessAccountService < BaseService
    # The key does not need to be unguessable, it just needs to be somewhat unique
    @options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}"

    with_lock("process_account:#{@uri}") do
    with_redis_lock("process_account:#{@uri}") do
      @account            = Account.remote.find_by(uri: @uri) if @options[:only_key]
      @account          ||= Account.find_remote(@username, @domain)
      @old_public_key     = @account&.public_key

M app/services/activitypub/process_status_update_service.rb => app/services/activitypub/process_status_update_service.rb +2 -2
@@ 35,7 35,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
    last_edit_date = @status.edited_at.presence || @status.created_at

    # Only allow processing one create/update per status at a time
    with_lock("create:#{@uri}") do
    with_redis_lock("create:#{@uri}") do
      Status.transaction do
        record_previous_edit!
        update_media_attachments!


@@ 58,7 58,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
  end

  def handle_implicit_update!
    with_lock("create:#{@uri}") do
    with_redis_lock("create:#{@uri}") do
      update_poll!(allow_significant_changes: false)
      queue_poll_notifications!
    end

M app/services/backup_service.rb => app/services/backup_service.rb +86 -62
@@ 1,59 1,67 @@
# frozen_string_literal: true

require 'rubygems/package'
require 'zip'

class BackupService < BaseService
  include Payloadable
  include ContextHelper

  attr_reader :account, :backup, :collection
  attr_reader :account, :backup

  def call(backup)
    @backup  = backup
    @account = backup.user.account

    build_json!
    build_archive!
  end

  private

  def build_json!
    @collection = serialize(collection_presenter, ActivityPub::CollectionSerializer)
  def build_outbox_json!(file)
    skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer)
    skeleton[:@context] = full_context
    skeleton[:orderedItems] = ['!PLACEHOLDER!']
    skeleton = Oj.dump(skeleton)
    prepend, append = skeleton.split('"!PLACEHOLDER!"')
    add_comma = false

    file.write(prepend)

    account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
      statuses.each do |status|
        item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer, signer: @account, allow_local_only: true)
        item.delete(:@context)
      file.write(',') if add_comma
      add_comma = true

      file.write(statuses.map do |status|
        item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer, allow_local_only: true)
        item.delete('@context')

        unless item[:type] == 'Announce' || item[:object][:attachment].blank?
          item[:object][:attachment].each do |attachment|
            attachment[:url] = Addressable::URI.parse(attachment[:url]).path.gsub(/\A\/system\//, '')
            attachment[:url] = Addressable::URI.parse(attachment[:url]).path.delete_prefix('/system/')
          end
        end

        @collection[:orderedItems] << item
      end
        Oj.dump(item)
      end.join(','))

      GC.start
    end

    file.write(append)
  end

  def build_archive!
    tmp_file = Tempfile.new(%w(archive .tar.gz))

    File.open(tmp_file, 'wb') do |file|
      Zlib::GzipWriter.wrap(file) do |gz|
        Gem::Package::TarWriter.new(gz) do |tar|
          dump_media_attachments!(tar)
          dump_outbox!(tar)
          dump_likes!(tar)
          dump_bookmarks!(tar)
          dump_actor!(tar)
        end
      end
    tmp_file = Tempfile.new(%w(archive .zip))

    Zip::File.open(tmp_file, create: true) do |zipfile|
      dump_outbox!(zipfile)
      dump_media_attachments!(zipfile)
      dump_likes!(zipfile)
      dump_bookmarks!(zipfile)
      dump_actor!(zipfile)
    end

    archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.tar.gz"
    archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.zip"

    @backup.dump      = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename)
    @backup.processed = true


@@ 63,27 71,28 @@ class BackupService < BaseService
    tmp_file.unlink
  end

  def dump_media_attachments!(tar)
  def dump_media_attachments!(zipfile)
    MediaAttachment.attached.where(account: account).reorder(nil).find_in_batches do |media_attachments|
      media_attachments.each do |m|
        next unless m.file&.path
        path = m.file&.path
        next unless path

        download_to_tar(tar, m.file, m.file.path)
        path = path.gsub(/\A.*\/system\//, '')
        path = path.gsub(/\A\/+/, '')
        download_to_zip(zipfile, m.file, path)
      end

      GC.start
    end
  end

  def dump_outbox!(tar)
    json = Oj.dump(collection)

    tar.add_file_simple('outbox.json', 0o444, json.bytesize) do |io|
      io.write(json)
  def dump_outbox!(zipfile)
    zipfile.get_output_stream('outbox.json') do |io|
      build_outbox_json!(io)
    end
  end

  def dump_actor!(tar)
  def dump_actor!(zipfile)
    actor = serialize(account, ActivityPub::ActorSerializer)

    actor[:icon][:url]  = "avatar#{File.extname(actor[:icon][:url])}"  if actor[:icon]


@@ 92,51 101,66 @@ class BackupService < BaseService
    actor[:likes]       = 'likes.json'
    actor[:bookmarks]   = 'bookmarks.json'

    download_to_tar(tar, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
    download_to_tar(tar, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
    download_to_zip(tar, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
    download_to_zip(tar, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?

    json = Oj.dump(actor)

    tar.add_file_simple('actor.json', 0o444, json.bytesize) do |io|
    zipfile.get_output_stream('actor.json') do |io|
      io.write(json)
    end
  end

  def dump_likes!(tar)
    collection = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
  def dump_likes!(zipfile)
    skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
    skeleton.delete(:totalItems)
    skeleton[:orderedItems] = ['!PLACEHOLDER!']
    skeleton = Oj.dump(skeleton)
    prepend, append = skeleton.split('"!PLACEHOLDER!"')

    Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses|
      statuses.each do |status|
        collection[:totalItems] += 1
        collection[:orderedItems] << ActivityPub::TagManager.instance.uri_for(status)
      end
    zipfile.get_output_stream('likes.json') do |io|
      io.write(prepend)

      GC.start
    end
      add_comma = false

    json = Oj.dump(collection)
      Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses|
        io.write(',') if add_comma
        add_comma = true

    tar.add_file_simple('likes.json', 0o444, json.bytesize) do |io|
      io.write(json)
        io.write(statuses.map do |status|
          Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
        end.join(','))

        GC.start
      end

      io.write(append)
    end
  end

  def dump_bookmarks!(tar)
    collection = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
  def dump_bookmarks!(zipfile)
    skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
    skeleton.delete(:totalItems)
    skeleton[:orderedItems] = ['!PLACEHOLDER!']
    skeleton = Oj.dump(skeleton)
    prepend, append = skeleton.split('"!PLACEHOLDER!"')

    Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses|
      statuses.each do |status|
        collection[:totalItems] += 1
        collection[:orderedItems] << ActivityPub::TagManager.instance.uri_for(status)
      end
    zipfile.get_output_stream('bookmarks.json') do |io|
      io.write(prepend)

      GC.start
    end
      add_comma = false
      Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses|
        io.write(',') if add_comma
        add_comma = true

    json = Oj.dump(collection)
        io.write(statuses.map do |status|
          Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
        end.join(','))

    tar.add_file_simple('bookmarks.json', 0o444, json.bytesize) do |io|
      io.write(json)
        GC.start
      end

      io.write(append)
    end
  end



@@ 160,10 184,10 @@ class BackupService < BaseService

  CHUNK_SIZE = 1.megabyte

  def download_to_tar(tar, attachment, filename)
  def download_to_zip(zipfile, attachment, filename)
    adapter = Paperclip.io_adapters.for(attachment)

    tar.add_file_simple(filename, 0o444, adapter.size) do |io|
    zipfile.get_output_stream(filename) do |io|
      while (buffer = adapter.read(CHUNK_SIZE))
        io.write(buffer)
      end

A app/services/bulk_import_row_service.rb => app/services/bulk_import_row_service.rb +60 -0
@@ 0,0 1,60 @@
# frozen_string_literal: true

class BulkImportRowService
  def call(row)
    @account = row.bulk_import.account
    @data    = row.data
    @type    = row.bulk_import.type.to_sym

    case @type
    when :following, :blocking, :muting
      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 }) }
      return false if @target_account.nil?
    when :bookmarks
      target_uri      = @data['uri']
      target_domain   = Addressable::URI.parse(target_uri).normalized_host
      @target_status = ActivityPub::TagManager.instance.uri_to_resource(target_uri, Status)
      return false if @target_status.nil? && ActivityPub::TagManager.instance.local_uri?(target_uri)

      @target_status ||= stoplight_wrap_request(target_domain) { ActivityPub::FetchRemoteStatusService.new.call(target_uri) }
      return false if @target_status.nil?
    end

    case @type
    when :following
      FollowService.new.call(@account, @target_account, reblogs: @data['show_reblogs'], notify: @data['notify'], languages: @data['languages'])
    when :blocking
      BlockService.new.call(@account, @target_account)
    when :muting
      MuteService.new.call(@account, @target_account, notifications: @data['hide_notifications'])
    when :bookmarks
      return false unless StatusPolicy.new(@account, @target_status).show?

      @account.bookmarks.find_or_create_by!(status: @target_status)
    end

    true
  rescue ActiveRecord::RecordNotFound
    false
  end

  def domain(uri)
    domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1]
    TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
  end

  def stoplight_wrap_request(domain, &block)
    if domain.present?
      Stoplight("source:#{domain}", &block)
        .with_fallback { nil }
        .with_threshold(1)
        .with_cool_off_time(5.minutes.seconds)
        .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
        .run
    else
      yield
    end
  end
end

A app/services/bulk_import_service.rb => app/services/bulk_import_service.rb +160 -0
@@ 0,0 1,160 @@
# frozen_string_literal: true

class BulkImportService < BaseService
  def call(import)
    @import  = import
    @account = @import.account

    case @import.type.to_sym
    when :following
      import_follows!
    when :blocking
      import_blocks!
    when :muting
      import_mutes!
    when :domain_blocking
      import_domain_blocks!
    when :bookmarks
      import_bookmarks!
    end

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

    raise
  end

  private

  def extract_rows_by_acct
    local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
    @import.rows.to_a.index_by { |row| row.data['acct'].delete_suffix(local_domain_suffix) }
  end

  def import_follows!
    rows_by_acct = extract_rows_by_acct

    if @import.overwrite?
      @account.following.find_each do |followee|
        row = rows_by_acct.delete(followee.acct)

        if row.nil?
          UnfollowService.new.call(@account, followee)
        else
          row.destroy
          @import.processed_items += 1
          @import.imported_items += 1

          # Since we're updating the settings of an existing relationship, we can safely call
          # FollowService directly
          FollowService.new.call(@account, followee, reblogs: row.data['show_reblogs'], notify: row.data['notify'], languages: row.data['languages'])
        end
      end

      # Save pending infos due to `overwrite?` handling
      @import.save!
    end

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

  def import_blocks!
    rows_by_acct = extract_rows_by_acct

    if @import.overwrite?
      @account.blocking.find_each do |blocked_account|
        row = rows_by_acct.delete(blocked_account.acct)

        if row.nil?
          UnblockService.new.call(@account, blocked_account)
        else
          row.destroy
          @import.processed_items += 1
          @import.imported_items += 1
          BlockService.new.call(@account, blocked_account)
        end
      end

      # Save pending infos due to `overwrite?` handling
      @import.save!
    end

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

  def import_mutes!
    rows_by_acct = extract_rows_by_acct

    if @import.overwrite?
      @account.muting.find_each do |muted_account|
        row = rows_by_acct.delete(muted_account.acct)

        if row.nil?
          UnmuteService.new.call(@account, muted_account)
        else
          row.destroy
          @import.processed_items += 1
          @import.imported_items += 1
          MuteService.new.call(@account, muted_account, notifications: row.data['hide_notifications'])
        end
      end

      # Save pending infos due to `overwrite?` handling
      @import.save!
    end

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

  def import_domain_blocks!
    domains = @import.rows.map { |row| row.data['domain'] }

    if @import.overwrite?
      @account.domain_blocks.find_each do |domain_block|
        domain = domains.delete(domain_block)

        @account.unblock_domain!(domain_block.domain) if domain.nil?
      end
    end

    @import.rows.delete_all
    domains.each { |domain| @account.block_domain!(domain) }
    @import.update!(processed_items: @import.total_items, imported_items: @import.total_items)

    AfterAccountDomainBlockWorker.push_bulk(domains) do |domain|
      [@account.id, domain]
    end
  end

  def import_bookmarks!
    rows_by_uri = @import.rows.index_by { |row| row.data['uri'] }

    if @import.overwrite?
      @account.bookmarks.includes(:status).find_each do |bookmark|
        row = rows_by_uri.delete(ActivityPub::TagManager.instance.uri_for(bookmark.status))

        if row.nil?
          bookmark.destroy!
        else
          row.destroy
          @import.processed_items += 1
          @import.imported_items += 1
        end
      end

      # Save pending infos due to `overwrite?` handling
      @import.save!
    end

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

M app/services/fetch_link_card_service.rb => app/services/fetch_link_card_service.rb +1 -1
@@ 23,7 23,7 @@ class FetchLinkCardService < BaseService

    @url = @original_url.to_s

    with_lock("fetch:#{@original_url}") do
    with_redis_lock("fetch:#{@original_url}") do
      @card = PreviewCard.find_by(url: @url)
      process_url if @card.nil? || @card.updated_at <= 2.weeks.ago || @card.missing_image?
    end

M app/services/fetch_resource_service.rb => app/services/fetch_resource_service.rb +2 -1
@@ 4,6 4,7 @@ class FetchResourceService < BaseService
  include JsonLdHelper

  ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html;q=0.1'
  ACTIVITY_STREAM_LINK_TYPES = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze

  attr_reader :response_code



@@ 65,7 66,7 @@ class FetchResourceService < BaseService

  def process_html(response)
    page      = Nokogiri::HTML(response.body_with_limit)
    json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) }
    json_link = page.xpath('//link[@rel="alternate"]').find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) }

    process(json_link['href'], terminal: true) unless json_link.nil?
  end

M app/services/follow_migration_service.rb => app/services/follow_migration_service.rb +26 -4
@@ 9,10 9,10 @@ class FollowMigrationService < FollowService
  def call(source_account, target_account, old_target_account, bypass_locked: false)
    @old_target_account = old_target_account

    follow    = source_account.active_relationships.find_by(target_account: old_target_account)
    reblogs   = follow&.show_reblogs?
    notify    = follow&.notify?
    languages = follow&.languages
    @original_follow = source_account.active_relationships.find_by(target_account: old_target_account)
    reblogs          = @original_follow&.show_reblogs?
    notify           = @original_follow&.notify?
    languages        = @original_follow&.languages

    super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
  end


@@ 21,6 21,7 @@ class FollowMigrationService < FollowService

  def request_follow!
    follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
    migrate_list_accounts!

    if @target_account.local?
      LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')


@@ 32,9 33,30 @@ class FollowMigrationService < FollowService
    follow_request
  end

  def change_follow_options!
    migrate_list_accounts!
    super
  end

  def change_follow_request_options!
    migrate_list_accounts!
    super
  end

  def direct_follow!
    follow = super

    migrate_list_accounts!
    UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)

    follow
  end

  def migrate_list_accounts!
    ListAccount.where(follow_id: @original_follow.id).includes(:list).find_each do |list_account|
      list_account.list.accounts << @target_account
    rescue ActiveRecord::RecordInvalid
      nil
    end
  end
end

M app/services/import_service.rb => app/services/import_service.rb +3 -0
@@ 2,6 2,9 @@

require 'csv'

# NOTE: This is a deprecated service, only kept to not break ongoing imports
# on upgrade. See `BulkImportService` for its replacement.

class ImportService < BaseService
  ROWS_PROCESSING_LIMIT = 20_000


M app/services/remove_status_service.rb => app/services/remove_status_service.rb +1 -1
@@ 18,7 18,7 @@ class RemoveStatusService < BaseService
    @account  = status.account
    @options  = options

    with_lock("distribute:#{@status.id}") do
    with_redis_lock("distribute:#{@status.id}") do
      @status.discard_with_reblogs

      StatusPin.find_by(status: @status)&.destroy

M app/services/resolve_account_service.rb => app/services/resolve_account_service.rb +2 -2
@@ 100,13 100,13 @@ class ResolveAccountService < BaseService
  end

  def split_acct(acct)
    acct.gsub(/\Aacct:/, '').split('@')
    acct.delete_prefix('acct:').split('@')
  end

  def fetch_account!
    return unless activitypub_ready?

    with_lock("resolve:#{@username}@#{@domain}") do
    with_redis_lock("resolve:#{@username}@#{@domain}") do
      @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
    end


M app/services/suspend_account_service.rb => app/services/suspend_account_service.rb +1 -1
@@ 68,7 68,7 @@ class SuspendAccountService < BaseService
    @account.media_attachments.find_each do |media_attachment|
      attachment_names.each do |attachment_name|
        attachment = media_attachment.public_send(attachment_name)
        styles     = [:original] | attachment.styles.keys
        styles     = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys

        next if attachment.blank?


M app/services/tag_search_service.rb => app/services/tag_search_service.rb +1 -1
@@ 2,7 2,7 @@

class TagSearchService < BaseService
  def call(query, options = {})
    @query   = query.strip.gsub(/\A#/, '')
    @query   = query.strip.delete_prefix('#')
    @offset  = options.delete(:offset).to_i
    @limit   = options.delete(:limit).to_i
    @options = options

M app/services/unfollow_service.rb => app/services/unfollow_service.rb +1 -1
@@ 15,7 15,7 @@ class UnfollowService < BaseService
    @target_account = target_account
    @options        = options

    with_lock("relationship:#{[source_account.id, target_account.id].sort.join(':')}") do
    with_redis_lock("relationship:#{[source_account.id, target_account.id].sort.join(':')}") do
      unfollow! || undo_follow_request!
    end
  end

M app/services/unsuspend_account_service.rb => app/services/unsuspend_account_service.rb +1 -1
@@ 64,7 64,7 @@ class UnsuspendAccountService < BaseService
    @account.media_attachments.find_each do |media_attachment|
      attachment_names.each do |attachment_name|
        attachment = media_attachment.public_send(attachment_name)
        styles     = [:original] | attachment.styles.keys
        styles     = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys

        next if attachment.blank?


M app/services/vote_service.rb => app/services/vote_service.rb +1 -1
@@ 18,7 18,7 @@ class VoteService < BaseService

    already_voted = true

    with_lock("vote:#{@poll.id}:#{@account.id}") do
    with_redis_lock("vote:#{@poll.id}:#{@account.id}") do
      already_voted = @poll.votes.where(account: @account).exists?

      ApplicationRecord.transaction do

M app/validators/email_mx_validator.rb => app/validators/email_mx_validator.rb +1 -3
@@ 8,9 8,7 @@ class EmailMxValidator < ActiveModel::Validator

    domain = get_domain(user.email)

    if domain.blank?
      user.errors.add(:email, :invalid)
    elsif domain.include?('..')
    if domain.blank? || domain.include?('..')
      user.errors.add(:email, :invalid)
    elsif !on_allowlist?(domain)
      resolved_ips, resolved_domains = resolve_mx(domain)

D app/validators/import_validator.rb => app/validators/import_validator.rb +0 -46
@@ 1,46 0,0 @@
# frozen_string_literal: true

require 'csv'

class ImportValidator < ActiveModel::Validator
  KNOWN_HEADERS = [
    'Account address',
    '#domain',
    '#uri',
  ].freeze

  def validate(import)
    return if import.type.blank? || import.data.blank?

    # We parse because newlines could be part of individual rows. This
    # runs on create so we should be reading the local file here before
    # it is uploaded to object storage or moved anywhere...
    csv_data = CSV.parse(import.data.queued_for_write[:original].read)

    row_count  = csv_data.size
    row_count -= 1 if KNOWN_HEADERS.include?(csv_data.first&.first)

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

    case import.type
    when 'following'
      validate_following_import(import, row_count)
    end
  rescue CSV::MalformedCSVError
    import.errors.add(:data, :malformed)
  end

  private

  def validate_following_import(import, row_count)
    base_limit = FollowLimitValidator.limit_for_account(import.account)

    limit = if import.overwrite?
              base_limit
            else
              base_limit - import.account.following_count
            end

    import.errors.add(:data, I18n.t('users.follow_limit_reached', limit: base_limit)) if row_count > limit
  end
end

M app/validators/vote_validator.rb => app/validators/vote_validator.rb +13 -5
@@ 6,15 6,23 @@ class VoteValidator < ActiveModel::Validator

    vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote)

    if vote.poll_multiple? && already_voted_for_same_choice_on_multiple_poll?(vote)
      vote.errors.add(:base, I18n.t('polls.errors.already_voted'))
    elsif !vote.poll_multiple? && already_voted_on_non_multiple_poll?(vote)
      vote.errors.add(:base, I18n.t('polls.errors.already_voted'))
    end
    vote.errors.add(:base, I18n.t('polls.errors.already_voted')) if additional_voting_not_allowed?(vote)
  end

  private

  def additional_voting_not_allowed?(vote)
    poll_multiple_and_already_voted?(vote) || poll_non_multiple_and_already_voted?(vote)
  end

  def poll_multiple_and_already_voted?(vote)
    vote.poll_multiple? && already_voted_for_same_choice_on_multiple_poll?(vote)
  end

  def poll_non_multiple_and_already_voted?(vote)
    !vote.poll_multiple? && already_voted_on_non_multiple_poll?(vote)
  end

  def invalid_choice?(vote)
    vote.choice.negative? || vote.choice >= vote.poll.options.size
  end

A app/views/settings/imports/index.html.haml => app/views/settings/imports/index.html.haml +49 -0
@@ 0,0 1,49 @@
- content_for :page_title do
  = t('settings.import')

= 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')

  .fields-row
    .fields-group.fields-row__column.fields-row__column-6
      = f.input :data, as: :file, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
    .fields-group.fields-row__column.fields-row__column-6
      = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: ->(mode) { safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'

  .actions
    = f.button :button, t('imports.upload'), type: :submit

- unless @recent_imports.empty?
  %hr.spacer/

  %h3= t('imports.recent_imports')

  .table-wrapper
    %table.table
      %thead
        %tr
          %th= t('imports.type')
          %th= t('imports.status')
          %th= t('imports.imported')
          %th= t('imports.time_started')
          %th= t('imports.failures')
      %tbody
        - @recent_imports.each do |import|
          %tr
            %td= t("imports.types.#{import.type}")
            %td
              - if import.unconfirmed?
                = link_to t("imports.states.#{import.state}"), settings_import_path(import)
              - else
                = t("imports.states.#{import.state}")
            %td
              #{import.imported_items} / #{import.total_items}
            %td= l(import.created_at)
            %td
              - num_failed = import.processed_items - import.imported_items
              - if num_failed.positive?
                - if import.finished?
                  = link_to num_failed, failures_settings_import_path(import, format: 'csv')
                - else
                  = num_failed

M app/views/settings/imports/show.html.haml => app/views/settings/imports/show.html.haml +10 -10
@@ 1,15 1,15 @@
- content_for :page_title do
  = t('settings.import')
  = t("imports.titles.#{@bulk_import.type.to_s}")

= simple_form_for @import, url: settings_import_path do |f|
  .field-group
    = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface')
- if @bulk_import.likely_mismatched?
  .flash-message.warning= t("imports.mismatched_types_warning")

  .fields-row
    .fields-group.fields-row__column.fields-row__column-6
      = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
    .fields-group.fields-row__column.fields-row__column-6
      = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
- if @bulk_import.overwrite?
  %p.hint= t("imports.overwrite_preambles.#{@bulk_import.type.to_s}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items)
- else
  %p.hint= t("imports.preambles.#{@bulk_import.type.to_s}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items)

.simple_form
  .actions
    = f.button :button, t('imports.upload'), type: :submit
    = link_to t('generic.cancel'), settings_import_path(@bulk_import), method: :delete, class: 'button button-tertiary'
    = link_to t('generic.confirm'), confirm_settings_import_path(@bulk_import), method: :post, class: 'button'

A app/workers/bulk_import_worker.rb => app/workers/bulk_import_worker.rb +13 -0
@@ 0,0 1,13 @@
# frozen_string_literal: true

class BulkImportWorker
  include Sidekiq::Worker

  sidekiq_options queue: 'pull', retry: false

  def perform(import_id)
    import = BulkImport.find(import_id)
    import.update!(state: :in_progress)
    BulkImportService.new.call(import)
  end
end

M app/workers/distribution_worker.rb => app/workers/distribution_worker.rb +1 -1
@@ 6,7 6,7 @@ class DistributionWorker
  include Lockable

  def perform(status_id, options = {})
    with_lock("distribute:#{status_id}") do
    with_redis_lock("distribute:#{status_id}") do
      FanOutOnWriteService.new.call(Status.find(status_id), **options.symbolize_keys)
    end
  rescue ActiveRecord::RecordNotFound

M app/workers/import/relationship_worker.rb => app/workers/import/relationship_worker.rb +3 -0
@@ 1,5 1,8 @@
# frozen_string_literal: true

# NOTE: This is a deprecated worker, only kept to not break ongoing imports
# on upgrade. See `Import::RowWorker` for its replacement.

class Import::RelationshipWorker
  include Sidekiq::Worker


A app/workers/import/row_worker.rb => app/workers/import/row_worker.rb +33 -0
@@ 0,0 1,33 @@
# frozen_string_literal: true

class Import::RowWorker
  include Sidekiq::Worker

  sidekiq_options queue: 'pull', retry: 6, dead: false

  sidekiq_retries_exhausted do |msg, _exception|
    ActiveRecord::Base.connection_pool.with_connection do
      # Increment the total number of processed items, and bump the state of the import if needed
      bulk_import_id = BulkImportRow.where(id: msg['args'][0]).pick(:id)
      BulkImport.progress!(bulk_import_id) unless bulk_import_id.nil?
    end
  end

  def perform(row_id)
    row = BulkImportRow.eager_load(bulk_import: :account).find_by(id: row_id)
    return true if row.nil?

    imported = BulkImportRowService.new.call(row)

    mark_as_processed!(row, imported)
  end

  private

  def mark_as_processed!(row, imported)
    bulk_import_id = row.bulk_import_id
    row.destroy! if imported

    BulkImport.progress!(bulk_import_id, imported: imported)
  end
end

M app/workers/import_worker.rb => app/workers/import_worker.rb +3 -0
@@ 1,5 1,8 @@
# frozen_string_literal: true

# NOTE: This is a deprecated worker, only kept to not break ongoing imports
# on upgrade. See `ImportWorker` for its replacement.

class ImportWorker
  include Sidekiq::Worker


M app/workers/move_worker.rb => app/workers/move_worker.rb +37 -5
@@ 8,9 8,9 @@ class MoveWorker
    @target_account = Account.find(target_account_id)

    if @target_account.local? && @source_account.local?
      nb_moved = rewrite_follows!
      @source_account.update_count!(:followers_count, -nb_moved)
      @target_account.update_count!(:followers_count, nb_moved)
      num_moved = rewrite_follows!
      @source_account.update_count!(:followers_count, -num_moved)
      @target_account.update_count!(:followers_count, num_moved)
    else
      queue_follow_unfollows!
    end


@@ 29,12 29,44 @@ class MoveWorker
  private

  def rewrite_follows!
    num_moved = 0

    # First, approve pending follow requests for the new account,
    # this allows correctly processing list memberships with pending
    # follow requests
    FollowRequest.where(account: @source_account.followers, target_account_id: @target_account.id).find_each do |follow_request|
      ListAccount.where(follow_id: follow_request.id).includes(:list).find_each do |list_account|
        list_account.list.accounts << @target_account
      rescue ActiveRecord::RecordInvalid
        nil
      end

      follow_request.authorize!
    end

    # Then handle accounts that follow both the old and new account
    @source_account.passive_relationships
                   .where(account: Account.local)
                   .where(account: @target_account.followers.local)
                   .in_batches do |follows|
      ListAccount.where(follow: follows).includes(:list).find_each do |list_account|
        list_account.list.accounts << @target_account
      rescue ActiveRecord::RecordInvalid
        nil
      end
    end

    # Finally, handle the common case of accounts not following the new account
    @source_account.passive_relationships
                   .where(account: Account.local)
                   .where.not(account: @target_account.followers.local)
                   .where.not(account_id: @target_account.id)
                   .in_batches
                   .update_all(target_account_id: @target_account.id)
                   .in_batches do |follows|
      ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id)
      num_moved += follows.update_all(target_account_id: @target_account.id)
    end

    num_moved
  end

  def queue_follow_unfollows!

M app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb => app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb +44 -7
@@ 38,17 38,37 @@ class Scheduler::AccountsStatusesCleanupScheduler
    return if under_load?

    budget = compute_budget
    first_policy_id = last_processed_id

    # If the budget allows it, we want to consider all accounts with enabled
    # auto cleanup at least once.
    #
    # We start from `first_policy_id` (the last processed id in the previous
    # run) and process each policy until we loop to `first_policy_id`,
    # recording into `affected_policies` any policy that caused posts to be
    # deleted.
    #
    # After that, we set `full_iteration` to `false` and continue looping on
    # policies from `affected_policies`.
    first_policy_id   = last_processed_id || 0
    first_iteration   = true
    full_iteration    = true
    affected_policies = []

    loop do
      num_processed_accounts = 0

      scope = AccountStatusesCleanupPolicy.where(enabled: true)
      scope = scope.where(id: first_policy_id...) if first_policy_id.present?
      scope = cleanup_policies(first_policy_id, affected_policies, first_iteration, full_iteration)
      scope.find_each(order: :asc) do |policy|
        num_deleted = AccountStatusesCleanupService.new.call(policy, [budget, PER_ACCOUNT_BUDGET].min)
        num_processed_accounts += 1 unless num_deleted.zero?
        budget -= num_deleted

        unless num_deleted.zero?
          num_processed_accounts += 1
          affected_policies << policy.id if full_iteration
        end

        full_iteration = false if !first_iteration && policy.id >= first_policy_id

        if budget.zero?
          save_last_processed_id(policy.id)
          break


@@ 57,9 77,10 @@ class Scheduler::AccountsStatusesCleanupScheduler

      # The idea here is to loop through all policies at least once until the budget is exhausted
      # and start back after the last processed account otherwise
      break if budget.zero? || (num_processed_accounts.zero? && first_policy_id.nil?)
      break if budget.zero? || (num_processed_accounts.zero? && !full_iteration)

      first_policy_id = nil
      full_iteration  = false unless first_iteration
      first_iteration = false
    end
  end



@@ 76,12 97,28 @@ class Scheduler::AccountsStatusesCleanupScheduler

  private

  def cleanup_policies(first_policy_id, affected_policies, first_iteration, full_iteration)
    scope = AccountStatusesCleanupPolicy.where(enabled: true)

    if full_iteration
      # If we are doing a full iteration, examine all policies we have not examined yet
      if first_iteration
        scope.where(id: first_policy_id...)
      else
        scope.where(id: ..first_policy_id).or(scope.where(id: affected_policies))
      end
    else
      # Otherwise, examine only policies that previously yielded posts to delete
      scope.where(id: affected_policies)
    end
  end

  def queue_under_load?(name, max_latency)
    Sidekiq::Queue.new(name).latency > max_latency
  end

  def last_processed_id
    redis.get('account_statuses_cleanup_scheduler:last_policy_id')
    redis.get('account_statuses_cleanup_scheduler:last_policy_id')&.to_i
  end

  def save_last_processed_id(id)

M app/workers/scheduler/vacuum_scheduler.rb => app/workers/scheduler/vacuum_scheduler.rb +5 -0
@@ 23,6 23,7 @@ class Scheduler::VacuumScheduler
      backups_vacuum,
      access_tokens_vacuum,
      feeds_vacuum,
      imports_vacuum,
    ]
  end



@@ 50,6 51,10 @@ class Scheduler::VacuumScheduler
    Vacuum::FeedsVacuum.new
  end

  def imports_vacuum
    Vacuum::ImportsVacuum.new
  end

  def content_retention_policy
    ContentRetentionPolicy.current
  end

M babel.config.js => babel.config.js +3 -0
@@ 9,6 9,9 @@ module.exports = (api) => {
    loose: true,
    modules: false,
    debug: false,
    include: [
      'proposal-numeric-separator',
    ],
  };

  const config = {

M config/deploy.rb => config/deploy.rb +6 -3
@@ 1,6 1,6 @@
# frozen_string_literal: true

lock '3.17.1'
lock '3.17.2'

set :repo_url, ENV.fetch('REPO', 'https://github.com/mastodon/mastodon.git')
set :branch, ENV.fetch('BRANCH', 'main')


@@ 13,9 13,12 @@ set :migration_role, :app
append :linked_files, '.env.production', 'public/robots.txt'
append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system'

SYSTEMD_SERVICES = %i[sidekiq streaming web].freeze
SERVICE_ACTIONS = %i[reload restart status].freeze

namespace :systemd do
  %i[sidekiq streaming web].each do |service|
    %i[reload restart status].each do |action|
  SYSTEMD_SERVICES.each do |service|
    SERVICE_ACTIONS.each do |action|
      desc "Perform a #{action} on #{service} service"
      task "#{service}:#{action}".to_sym do
        on roles(:app) do

M config/environments/development.rb => config/environments/development.rb +1 -1
@@ 14,7 14,7 @@ Rails.application.configure do

  # Enable/disable caching. By default caching is disabled.
  # Run rails dev:cache to toggle caching.
  if Rails.root.join('tmp/caching-dev.txt').exist?
  if Rails.root.join('tmp', 'caching-dev.txt').exist?
    config.action_controller.perform_caching = true
    config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS
  else

M config/environments/test.rb => config/environments/test.rb +1 -1
@@ 59,7 59,7 @@ Rails.application.configure do
  end
end

Paperclip::Attachment.default_options[:path] = "#{Rails.root}/spec/test_files/:class/:id_partition/:style.:extension"
Paperclip::Attachment.default_options[:path] = Rails.root.join('spec', 'test_files', ':class', ':id_partition', ':style.:extension')

# set fake_data for pam, don't do real calls, just use fake data
if ENV['PAM_ENABLED'] == 'true'

M config/i18n-tasks.yml => config/i18n-tasks.yml +2 -0
@@ 72,6 72,8 @@ ignore_unused:
  - 'themes.*'
  - 'move_handler.carry_{mutes,blocks}_over_text'
  - 'notification_mailer.*'
  - 'imports.overwrite_preambles.{following,blocking,muting,domain_blocking,bookmarks}_html'
  - 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks}_html'

ignore_inconsistent_interpolations:
  - '*.one'

M config/initializers/paperclip.rb => config/initializers/paperclip.rb +2 -10
@@ 68,11 68,7 @@ if ENV['S3_ENABLED'] == 'true'
    }
  )
  
  if ENV['S3_PERMISSION'] == ''
    Paperclip::Attachment.default_options.merge!(
      s3_permissions: ->(*) { nil }
    )
  end
  Paperclip::Attachment.default_options[:s3_permissions] = ->(*) { nil } if ENV['S3_PERMISSION'] == ''

  if ENV.has_key?('S3_ENDPOINT')
    Paperclip::Attachment.default_options[:s3_options].merge!(


@@ 90,11 86,7 @@ if ENV['S3_ENABLED'] == 'true'
    )
  end

  if ENV.has_key?('S3_STORAGE_CLASS')
    Paperclip::Attachment.default_options[:s3_headers].merge!(
      'X-Amz-Storage-Class' => ENV['S3_STORAGE_CLASS']
    )
  end
  Paperclip::Attachment.default_options[:s3_headers]['X-Amz-Storage-Class'] = ENV['S3_STORAGE_CLASS'] if ENV.has_key?('S3_STORAGE_CLASS')

  # Some S3-compatible providers might not actually be compatible with some APIs
  # used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822

M config/locales/en.yml => config/locales/en.yml +38 -0
@@ 1218,7 1218,9 @@ en:
    all_matching_items_selected_html:
      one: "<strong>%{count}</strong> item matching your search is selected."
      other: All <strong>%{count}</strong> items matching your search are selected.
    cancel: Cancel
    changes_saved_msg: Changes successfully saved!
    confirm: Confirm
    copy: Copy
    delete: Delete
    deselect: Deselect all


@@ 1234,15 1236,51 @@ en:
      other: Something isn't quite right yet! Please review %{count} errors below
  imports:
    errors:
      empty: Empty CSV file
      incompatible_type: Incompatible with the selected import type
      invalid_csv_file: 'Invalid CSV file. Error: %{error}'
      over_rows_processing_limit: contains more than %{count} rows
      too_large: File is too large
    failures: Failures
    imported: Imported
    mismatched_types_warning: It appears you may have selected the wrong type for this import, please double-check.
    modes:
      merge: Merge
      merge_long: Keep existing records and add new ones
      overwrite: Overwrite
      overwrite_long: Replace current records with the new ones
    overwrite_preambles:
      blocking_html: You are about to <strong>replace your block list</strong> with up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>.
      bookmarks_html: You are about to <strong>replace your bookmarks</strong> with up to <strong>%{total_items} posts</strong> from <strong>%{filename}</strong>.
      domain_blocking_html: You are about to <strong>replace your domain block list</strong> with up to <strong>%{total_items} domains</strong> from <strong>%{filename}</strong>.
      following_html: You are about to <strong>follow</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong> and <strong>stop following anyone else</strong>.
      muting_html: You are about to <strong>replace your list of muted accounts</strong> with up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>.
    preambles:
      blocking_html: You are about to <strong>block</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>.
      bookmarks_html: You are about to add up to <strong>%{total_items} posts</strong> from <strong>%{filename}</strong> to your <strong>bookmarks</strong>.
      domain_blocking_html: You are about to <strong>block</strong> up to <strong>%{total_items} domains</strong> from <strong>%{filename}</strong>.
      following_html: You are about to <strong>follow</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>.
      muting_html: You are about to <strong>mute</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>.
    preface: You can import data that you have exported from another server, such as a list of the people you are following or blocking.
    recent_imports: Recent imports
    states:
      finished: Finished
      in_progress: In progress
      scheduled: Scheduled
      unconfirmed: Unconfirmed
    status: Status
    success: Your data was successfully uploaded and will be processed in due time
    time_started: Started at
    titles:
      blocking: Importing blocked accounts
      bookmarks: Importing bookmarks
      domain_blocking: Importing blocked domains
      following: Importing followed accounts
      muting: Importing muted accounts
    type: Import type
    type_groups:
      constructive: Follows & Bookmarks
      destructive: Blocks & mutes
    types:
      blocking: Blocking list
      bookmarks: Bookmarks

M config/navigation.rb => config/navigation.rb +1 -1
@@ 32,7 32,7 @@ SimpleNavigation::Configuration.run do |navigation|
    end

    n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_path do |s|
      s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_path, if: -> { current_user.functional? }
      s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_imports_path, if: -> { current_user.functional? }
      s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_path
    end


M config/routes.rb => config/routes.rb +3 -574
@@ 133,73 133,7 @@ Rails.application.routes.draw do
  get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false
  get '/settings', to: redirect('/settings/profile')

  namespace :settings do
    resource :profile, only: [:show, :update] do
      resources :pictures, only: :destroy
    end

    get :preferences, to: redirect('/settings/preferences/appearance')

    namespace :preferences do
      resource :appearance, only: [:show, :update], controller: :appearance
      resource :notifications, only: [:show, :update]
      resource :other, only: [:show, :update], controller: :other
    end

    resource :import, only: [:show, :create]
    resource :export, only: [:show, :create]

    namespace :exports, constraints: { format: :csv } do
      resources :follows, only: :index, controller: :following_accounts
      resources :blocks, only: :index, controller: :blocked_accounts
      resources :mutes, only: :index, controller: :muted_accounts
      resources :lists, only: :index, controller: :lists
      resources :domain_blocks, only: :index, controller: :blocked_domains
      resources :bookmarks, only: :index, controller: :bookmarks
    end

    resources :two_factor_authentication_methods, only: [:index] do
      collection do
        post :disable
      end
    end

    resource :otp_authentication, only: [:show, :create], controller: 'two_factor_authentication/otp_authentication'

    resources :webauthn_credentials, only: [:index, :new, :create, :destroy],
              path: 'security_keys',
              controller: 'two_factor_authentication/webauthn_credentials' do

      collection do
        get :options
      end
    end

    namespace :two_factor_authentication do
      resources :recovery_codes, only: [:create]
      resource :confirmation, only: [:new, :create]
    end

    resources :applications, except: [:edit] do
      member do
        post :regenerate
      end
    end

    resources :flavours, only: [:index, :show, :update], param: :flavour

    resource :delete, only: [:show, :destroy]
    resource :migration, only: [:show, :create]

    namespace :migration do
      resource :redirect, only: [:new, :create, :destroy]
    end

    resources :aliases, only: [:index, :create, :destroy]
    resources :sessions, only: [:destroy]
    resources :featured_tags, only: [:index, :create, :destroy]
    resources :login_activities, only: [:index]
  end
  draw(:settings)

  namespace :disputes do
    resources :strikes, only: [:show, :index] do


@@ 231,516 165,11 @@ Rails.application.routes.draw do
  resource :authorize_interaction, only: [:show, :create]
  resource :share, only: [:show, :create]

  namespace :admin do
    get '/dashboard', to: 'dashboard#index'

    resources :domain_allows, only: [:new, :create, :show, :destroy]
    resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do
      collection do
        post :batch
      end
    end

    resources :export_domain_allows, only: [:new] do
      collection do
        get :export, constraints: { format: :csv }
        post :import
      end
    end

    resources :export_domain_blocks, only: [:new] do
      collection do
        get :export, constraints: { format: :csv }
        post :import
      end
    end

    resources :email_domain_blocks, only: [:index, :new, :create] do
      collection do
        post :batch
      end
    end

    resources :action_logs, only: [:index]
    resources :warning_presets, except: [:new]

    resources :announcements, except: [:show] do
      member do
        post :publish
        post :unpublish
      end
    end

    get '/settings', to: redirect('/admin/settings/branding')
    get '/settings/edit', to: redirect('/admin/settings/branding')

    namespace :settings do
      resource :branding, only: [:show, :update], controller: 'branding'
      resource :registrations, only: [:show, :update], controller: 'registrations'
      resource :content_retention, only: [:show, :update], controller: 'content_retention'
      resource :about, only: [:show, :update], controller: 'about'
      resource :appearance, only: [:show, :update], controller: 'appearance'
      resource :discovery, only: [:show, :update], controller: 'discovery'
      resource :other, only: [:show, :update], controller: 'other'
    end

    resources :site_uploads, only: [:destroy]

    resources :invites, only: [:index, :create, :destroy] do
      collection do
        post :deactivate_all
      end
    end

    resources :relays, only: [:index, :new, :create, :destroy] do
      member do
        post :enable
        post :disable
      end
    end

    resources :instances, only: [:index, :show, :destroy], constraints: { id: /[^\/]+/ } do
      member do
        post :clear_delivery_errors
        post :restart_delivery
        post :stop_delivery
      end
    end

    resources :rules

    resources :webhooks do
      member do
        post :enable
        post :disable
      end

      resource :secret, only: [], controller: 'webhooks/secrets' do
        post :rotate
      end
    end

    resources :reports, only: [:index, :show] do
      resources :actions, only: [:create], controller: 'reports/actions' do
        collection do
          post :preview
        end
      end

      member do
        post :assign_to_self
        post :unassign
        post :reopen
        post :resolve
      end
    end

    resources :report_notes, only: [:create, :destroy]

    resources :accounts, only: [:index, :show, :destroy] do
      member do
        post :enable
        post :unsensitive
        post :unsilence
        post :unsuspend
        post :redownload
        post :remove_avatar
        post :remove_header
        post :memorialize
        post :approve
        post :reject
        post :unblock_email
      end

      collection do
        post :batch
      end

      resource :change_email, only: [:show, :update]
      resource :reset, only: [:create]
      resource :action, only: [:new, :create], controller: 'account_actions'

      resources :statuses, only: [:index, :show] do
        collection do
          post :batch
        end
      end

      resources :relationships, only: [:index]

      resource :confirmation, only: [:create] do
        collection do
          post :resend
        end
      end
    end

    resources :users, only: [] do
      resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications'
      resource :role, only: [:show, :update], controller: 'users/roles'
    end

    resources :custom_emojis, only: [:index, :new, :create] do
      collection do
        post :batch
      end
    end

    resources :ip_blocks, only: [:index, :new, :create] do
      collection do
        post :batch
      end
    end

    resources :roles, except: [:show]
    resources :account_moderation_notes, only: [:create, :destroy]
    resource :follow_recommendations, only: [:show, :update]
    resources :tags, only: [:show, :update]

    namespace :trends do
      resources :links, only: [:index] do
        collection do
          post :batch
        end
      end

      resources :tags, only: [:index] do
        collection do
          post :batch
        end
      end

      resources :statuses, only: [:index] do
        collection do
          post :batch
        end
      end

      namespace :links do
        resources :preview_card_providers, only: [:index], path: :publishers do
          collection do
            post :batch
          end
        end
      end
    end

    namespace :disputes do
      resources :appeals, only: [:index] do
        member do
          post :approve
          post :reject
        end
      end
    end
  end
  draw(:admin)

  get '/admin', to: redirect('/admin/dashboard', status: 302)

  namespace :api, format: false do
    # OEmbed
    get '/oembed', to: 'oembed#show', as: :oembed

    # JSON / REST API
    namespace :v1 do
      resources :statuses, only: [:create, :show, :update, :destroy] do
        scope module: :statuses do
          resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
          resources :favourited_by, controller: :favourited_by_accounts, only: :index
          resource :reblog, only: :create
          post :unreblog, to: 'reblogs#destroy'

          resource :favourite, only: :create
          post :unfavourite, to: 'favourites#destroy'

          resource :bookmark, only: :create
          post :unbookmark, to: 'bookmarks#destroy'

          resource :mute, only: :create
          post :unmute, to: 'mutes#destroy'

          resource :pin, only: :create
          post :unpin, to: 'pins#destroy'

          resource :history, only: :show
          resource :source, only: :show

          post :translate, to: 'translations#create'
        end

        member do
          get :context
        end
      end

      namespace :timelines do
        resource :direct, only: :show, controller: :direct
        resource :home, only: :show, controller: :home
        resource :public, only: :show, controller: :public
        resources :tag, only: :show
        resources :list, only: :show
      end

      get '/streaming', to: 'streaming#index'
      get '/streaming/(*any)', to: 'streaming#index'

      resources :custom_emojis, only: [:index]
      resources :suggestions, only: [:index, :destroy]
      resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
      resources :preferences, only: [:index]

      resources :announcements, only: [:index] do
        scope module: :announcements do
          resources :reactions, only: [:update, :destroy]
        end

        member do
          post :dismiss
        end
      end

      # namespace :crypto do
      #   resources :deliveries, only: :create

      #   namespace :keys do
      #     resource :upload, only: [:create]
      #     resource :query,  only: [:create]
      #     resource :claim,  only: [:create]
      #     resource :count,  only: [:show]
      #   end

      #   resources :encrypted_messages, only: [:index] do
      #     collection do
      #       post :clear
      #     end
      #   end
      # end

      resources :conversations, only: [:index, :destroy] do
        member do
          post :read
        end
      end

      resources :media,        only: [:create, :update, :show]
      resources :blocks,       only: [:index]
      resources :mutes,        only: [:index]
      resources :favourites,   only: [:index]
      resources :bookmarks,    only: [:index]
      resources :reports,      only: [:create]
      resources :trends,       only: [:index], controller: 'trends/tags'
      resources :filters,      only: [:index, :create, :show, :update, :destroy]
      resources :endorsements, only: [:index]
      resources :markers,      only: [:index, :create]

      namespace :apps do
        get :verify_credentials, to: 'credentials#show'
      end

      resources :apps, only: [:create]

      namespace :trends do
        resources :links, only: [:index]
        resources :tags, only: [:index]
        resources :statuses, only: [:index]
      end

      namespace :emails do
        resources :confirmations, only: [:create]
      end

      resource :instance, only: [:show] do
        resources :peers, only: [:index], controller: 'instances/peers'
        resources :rules, only: [:index], controller: 'instances/rules'
        resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks'
        resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies'
        resource :extended_description, only: [:show], controller: 'instances/extended_descriptions'
        resource :translation_languages, only: [:show], controller: 'instances/translation_languages'
        resource :activity, only: [:show], controller: 'instances/activity'
      end

      resource :domain_blocks, only: [:show, :create, :destroy]

      resource :directory, only: [:show]

      resources :follow_requests, only: [:index] do
        member do
          post :authorize
          post :reject
        end
      end

      resources :notifications, only: [:index, :show, :destroy] do
        collection do
          post :clear
          delete :destroy_multiple
        end

        member do
          post :dismiss
        end
      end

      namespace :accounts do
        get :verify_credentials, to: 'credentials#show'
        patch :update_credentials, to: 'credentials#update'
        resource :search, only: :show, controller: :search
        resource :lookup, only: :show, controller: :lookup
        resources :relationships, only: :index
        resources :familiar_followers, only: :index
      end

      resources :accounts, only: [:create, :show] do
        resources :statuses, only: :index, controller: 'accounts/statuses'
        resources :followers, only: :index, controller: 'accounts/follower_accounts'
        resources :following, only: :index, controller: 'accounts/following_accounts'
        resources :lists, only: :index, controller: 'accounts/lists'
        resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
        resources :featured_tags, only: :index, controller: 'accounts/featured_tags'

        member do
          post :follow
          post :unfollow
          post :remove_from_followers
          post :block
          post :unblock
          post :mute
          post :unmute
        end

        resource :pin, only: :create, controller: 'accounts/pins'
        post :unpin, to: 'accounts/pins#destroy'
        resource :note, only: :create, controller: 'accounts/notes'
      end

      resources :tags, only: [:show] do
        member do
          post :follow
          post :unfollow
        end
      end

      resources :followed_tags, only: [:index]

      resources :lists, only: [:index, :create, :show, :update, :destroy] do
        resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts'
      end

      namespace :featured_tags do
        get :suggestions, to: 'suggestions#index'
      end

      resources :featured_tags, only: [:index, :create, :destroy]

      resources :polls, only: [:create, :show] do
        resources :votes, only: :create, controller: 'polls/votes'
      end

      namespace :push do
        resource :subscription, only: [:create, :show, :update, :destroy]
      end

      namespace :admin do
        resources :accounts, only: [:index, :show, :destroy] do
          member do
            post :enable
            post :unsensitive
            post :unsilence
            post :unsuspend
            post :approve
            post :reject
          end

          resource :action, only: [:create], controller: 'account_actions'
        end

        resources :reports, only: [:index, :update, :show] do
          member do
            post :assign_to_self
            post :unassign
            post :reopen
            post :resolve
          end
        end

        resources :domain_allows, only: [:index, :show, :create, :destroy]
        resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
        resources :email_domain_blocks, only: [:index, :show, :create, :destroy]
        resources :ip_blocks, only: [:index, :show, :update, :create, :destroy]

        namespace :trends do
          resources :tags, only: [:index] do
            member do
              post :approve
              post :reject
            end
          end
          resources :links, only: [:index] do
            member do
              post :approve
              post :reject
            end
          end
          resources :statuses, only: [:index] do
            member do
              post :approve
              post :reject
            end
          end

          namespace :links do
            resources :preview_card_providers, only: [:index], path: :publishers do
              member do
                post :approve
                post :reject
              end
            end
          end
        end

        post :measures, to: 'measures#create'
        post :dimensions, to: 'dimensions#create'
        post :retention, to: 'retention#create'

        resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do
          collection do
            post :test
          end
        end
      end
    end

    namespace :v2 do
      get '/search', to: 'search#index', as: :search

      resources :media,       only: [:create]
      resources :suggestions, only: [:index]
      resource  :instance,    only: [:show]
      resources :filters,     only: [:index, :create, :show, :update, :destroy] do
        resources :keywords, only: [:index, :create], controller: 'filters/keywords'
        resources :statuses, only: [:index, :create], controller: 'filters/statuses'
      end

      namespace :filters do
        resources :keywords, only: [:show, :update, :destroy]
        resources :statuses, only: [:show, :destroy]
      end

      namespace :admin do
        resources :accounts, only: [:index]
      end
    end

    namespace :web do
      resource :settings, only: [:update]
      resource :embed, only: [:create]
      resources :push_subscriptions, only: [:create] do
        member do
          put :update
        end
      end
    end
  end
  draw(:api)

  web_app_paths.each do |path|
    get path, to: 'home#index'

A config/routes/admin.rb => config/routes/admin.rb +205 -0
@@ 0,0 1,205 @@
# frozen_string_literal: true

namespace :admin do
  get '/dashboard', to: 'dashboard#index'

  resources :domain_allows, only: [:new, :create, :show, :destroy]
  resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do
    collection do
      post :batch
    end
  end

  resources :export_domain_allows, only: [:new] do
    collection do
      get :export, constraints: { format: :csv }
      post :import
    end
  end

  resources :export_domain_blocks, only: [:new] do
    collection do
      get :export, constraints: { format: :csv }
      post :import
    end
  end

  resources :email_domain_blocks, only: [:index, :new, :create] do
    collection do
      post :batch
    end
  end

  resources :action_logs, only: [:index]
  resources :warning_presets, except: [:new]

  resources :announcements, except: [:show] do
    member do
      post :publish
      post :unpublish
    end
  end

  get '/settings', to: redirect('/admin/settings/branding')
  get '/settings/edit', to: redirect('/admin/settings/branding')

  namespace :settings do
    resource :branding, only: [:show, :update], controller: 'branding'
    resource :registrations, only: [:show, :update], controller: 'registrations'
    resource :content_retention, only: [:show, :update], controller: 'content_retention'
    resource :about, only: [:show, :update], controller: 'about'
    resource :appearance, only: [:show, :update], controller: 'appearance'
    resource :discovery, only: [:show, :update], controller: 'discovery'
    resource :other, only: [:show, :update], controller: 'other'
  end

  resources :site_uploads, only: [:destroy]

  resources :invites, only: [:index, :create, :destroy] do
    collection do
      post :deactivate_all
    end
  end

  resources :relays, only: [:index, :new, :create, :destroy] do
    member do
      post :enable
      post :disable
    end
  end

  resources :instances, only: [:index, :show, :destroy], constraints: { id: %r{[^/]+} } do
    member do
      post :clear_delivery_errors
      post :restart_delivery
      post :stop_delivery
    end
  end

  resources :rules

  resources :webhooks do
    member do
      post :enable
      post :disable
    end

    resource :secret, only: [], controller: 'webhooks/secrets' do
      post :rotate
    end
  end

  resources :reports, only: [:index, :show] do
    resources :actions, only: [:create], controller: 'reports/actions' do
      collection do
        post :preview
      end
    end

    member do
      post :assign_to_self
      post :unassign
      post :reopen
      post :resolve
    end
  end

  resources :report_notes, only: [:create, :destroy]

  resources :accounts, only: [:index, :show, :destroy] do
    member do
      post :enable
      post :unsensitive
      post :unsilence
      post :unsuspend
      post :redownload
      post :remove_avatar
      post :remove_header
      post :memorialize
      post :approve
      post :reject
      post :unblock_email
    end

    collection do
      post :batch
    end

    resource :change_email, only: [:show, :update]
    resource :reset, only: [:create]
    resource :action, only: [:new, :create], controller: 'account_actions'

    resources :statuses, only: [:index, :show] do
      collection do
        post :batch
      end
    end

    resources :relationships, only: [:index]

    resource :confirmation, only: [:create] do
      collection do
        post :resend
      end
    end
  end

  resources :users, only: [] do
    resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications'
    resource :role, only: [:show, :update], controller: 'users/roles'
  end

  resources :custom_emojis, only: [:index, :new, :create] do
    collection do
      post :batch
    end
  end

  resources :ip_blocks, only: [:index, :new, :create] do
    collection do
      post :batch
    end
  end

  resources :roles, except: [:show]
  resources :account_moderation_notes, only: [:create, :destroy]
  resource :follow_recommendations, only: [:show, :update]
  resources :tags, only: [:show, :update]

  namespace :trends do
    resources :links, only: [:index] do
      collection do
        post :batch
      end
    end

    resources :tags, only: [:index] do
      collection do
        post :batch
      end
    end

    resources :statuses, only: [:index] do
      collection do
        post :batch
      end
    end

    namespace :links do
      resources :preview_card_providers, only: [:index], path: :publishers do
        collection do
          post :batch
        end
      end
    end
  end

  namespace :disputes do
    resources :appeals, only: [:index] do
      member do
        post :approve
        post :reject
      end
    end
  end
end

A config/routes/api.rb => config/routes/api.rb +306 -0
@@ 0,0 1,306 @@
# frozen_string_literal: true

namespace :api, format: false do
  # OEmbed
  get '/oembed', to: 'oembed#show', as: :oembed

  # JSON / REST API
  namespace :v1 do
    resources :statuses, only: [:create, :show, :update, :destroy] do
      scope module: :statuses do
        resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
        resources :favourited_by, controller: :favourited_by_accounts, only: :index
        resource :reblog, only: :create
        post :unreblog, to: 'reblogs#destroy'

        resource :favourite, only: :create
        post :unfavourite, to: 'favourites#destroy'

        resource :bookmark, only: :create
        post :unbookmark, to: 'bookmarks#destroy'

        resource :mute, only: :create
        post :unmute, to: 'mutes#destroy'

        resource :pin, only: :create
        post :unpin, to: 'pins#destroy'

        resource :history, only: :show
        resource :source, only: :show

        post :translate, to: 'translations#create'
      end

      member do
        get :context
      end
    end

    namespace :timelines do
      resource :direct, only: :show, controller: :direct
      resource :home, only: :show, controller: :home
      resource :public, only: :show, controller: :public
      resources :tag, only: :show
      resources :list, only: :show
    end

    get '/streaming', to: 'streaming#index'
    get '/streaming/(*any)', to: 'streaming#index'

    resources :custom_emojis, only: [:index]
    resources :suggestions, only: [:index, :destroy]
    resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
    resources :preferences, only: [:index]

    resources :announcements, only: [:index] do
      scope module: :announcements do
        resources :reactions, only: [:update, :destroy]
      end

      member do
        post :dismiss
      end
    end

    # namespace :crypto do
    #   resources :deliveries, only: :create

    #   namespace :keys do
    #     resource :upload, only: [:create]
    #     resource :query,  only: [:create]
    #     resource :claim,  only: [:create]
    #     resource :count,  only: [:show]
    #   end

    #   resources :encrypted_messages, only: [:index] do
    #     collection do
    #       post :clear
    #     end
    #   end
    # end

    resources :conversations, only: [:index, :destroy] do
      member do
        post :read
      end
    end

    resources :media, only: [:create, :update, :show]
    resources :blocks, only: [:index]
    resources :mutes, only: [:index]
    resources :favourites, only: [:index]
    resources :bookmarks, only: [:index]
    resources :reports, only: [:create]
    resources :trends, only: [:index], controller: 'trends/tags'
    resources :filters, only: [:index, :create, :show, :update, :destroy]
    resources :endorsements, only: [:index]
    resources :markers, only: [:index, :create]

    namespace :apps do
      get :verify_credentials, to: 'credentials#show'
    end

    resources :apps, only: [:create]

    namespace :trends do
      resources :tags, only: [:index]
      resources :links, only: [:index]
      resources :statuses, only: [:index]
    end

    namespace :emails do
      resources :confirmations, only: [:create]
    end

    resource :instance, only: [:show] do
      resources :peers, only: [:index], controller: 'instances/peers'
      resources :rules, only: [:index], controller: 'instances/rules'
      resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks'
      resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies'
      resource :extended_description, only: [:show], controller: 'instances/extended_descriptions'
      resource :translation_languages, only: [:show], controller: 'instances/translation_languages'
      resource :activity, only: [:show], controller: 'instances/activity'
    end

    resource :domain_blocks, only: [:show, :create, :destroy]

    resource :directory, only: [:show]

    resources :follow_requests, only: [:index] do
      member do
        post :authorize
        post :reject
      end
    end

    resources :notifications, only: [:index, :show, :destroy] do
      collection do
        post :clear
        delete :destroy_multiple
      end

      member do
        post :dismiss
      end
    end

    namespace :accounts do
      get :verify_credentials, to: 'credentials#show'
      patch :update_credentials, to: 'credentials#update'
      resource :search, only: :show, controller: :search
      resource :lookup, only: :show, controller: :lookup
      resources :relationships, only: :index
      resources :familiar_followers, only: :index
    end

    resources :accounts, only: [:create, :show] do
      resources :statuses, only: :index, controller: 'accounts/statuses'
      resources :followers, only: :index, controller: 'accounts/follower_accounts'
      resources :following, only: :index, controller: 'accounts/following_accounts'
      resources :lists, only: :index, controller: 'accounts/lists'
      resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
      resources :featured_tags, only: :index, controller: 'accounts/featured_tags'

      member do
        post :follow
        post :unfollow
        post :remove_from_followers
        post :block
        post :unblock
        post :mute
        post :unmute
      end

      resource :pin, only: :create, controller: 'accounts/pins'
      post :unpin, to: 'accounts/pins#destroy'
      resource :note, only: :create, controller: 'accounts/notes'
    end

    resources :tags, only: [:show] do
      member do
        post :follow
        post :unfollow
      end
    end

    resources :followed_tags, only: [:index]

    resources :lists, only: [:index, :create, :show, :update, :destroy] do
      resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts'
    end

    namespace :featured_tags do
      get :suggestions, to: 'suggestions#index'
    end

    resources :featured_tags, only: [:index, :create, :destroy]

    resources :polls, only: [:create, :show] do
      resources :votes, only: :create, controller: 'polls/votes'
    end

    namespace :push do
      resource :subscription, only: [:create, :show, :update, :destroy]
    end

    namespace :admin do
      resources :accounts, only: [:index, :show, :destroy] do
        member do
          post :enable
          post :unsensitive
          post :unsilence
          post :unsuspend
          post :approve
          post :reject
        end

        resource :action, only: [:create], controller: 'account_actions'
      end

      resources :reports, only: [:index, :update, :show] do
        member do
          post :assign_to_self
          post :unassign
          post :reopen
          post :resolve
        end
      end

      resources :domain_allows, only: [:index, :show, :create, :destroy]
      resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
      resources :email_domain_blocks, only: [:index, :show, :create, :destroy]
      resources :ip_blocks, only: [:index, :show, :update, :create, :destroy]

      namespace :trends do
        resources :tags, only: [:index] do
          member do
            post :approve
            post :reject
          end
        end
        resources :links, only: [:index] do
          member do
            post :approve
            post :reject
          end
        end
        resources :statuses, only: [:index] do
          member do
            post :approve
            post :reject
          end
        end

        namespace :links do
          resources :preview_card_providers, only: [:index], path: :publishers do
            member do
              post :approve
              post :reject
            end
          end
        end
      end

      post :measures, to: 'measures#create'
      post :dimensions, to: 'dimensions#create'
      post :retention, to: 'retention#create'

      resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do
        collection do
          post :test
        end
      end
    end
  end

  namespace :v2 do
    get '/search', to: 'search#index', as: :search

    resources :media, only: [:create]
    resources :suggestions, only: [:index]
    resource :instance, only: [:show]
    resources :filters, only: [:index, :create, :show, :update, :destroy] do
      resources :keywords, only: [:index, :create], controller: 'filters/keywords'
      resources :statuses, only: [:index, :create], controller: 'filters/statuses'
    end

    namespace :filters do
      resources :keywords, only: [:show, :update, :destroy]
      resources :statuses, only: [:show, :destroy]
    end

    namespace :admin do
      resources :accounts, only: [:index]
    end
  end

  namespace :web do
    resource :settings, only: [:update]
    resource :embed, only: [:create]
    resources :push_subscriptions, only: [:create] do
      member do
        put :update
      end
    end
  end
end

A config/routes/settings.rb => config/routes/settings.rb +74 -0
@@ 0,0 1,74 @@
# frozen_string_literal: true

namespace :settings do
  resource :profile, only: [:show, :update] do
    resources :pictures, only: :destroy
  end

  get :preferences, to: redirect('/settings/preferences/appearance')

  namespace :preferences do
    resource :appearance, only: [:show, :update], controller: :appearance
    resource :notifications, only: [:show, :update]
    resource :other, only: [:show, :update], controller: :other
  end

  resources :imports, only: [:index, :show, :destroy, :create] do
    member do
      post :confirm
      get :failures
    end
  end

  resource :export, only: [:show, :create]

  namespace :exports, constraints: { format: :csv } do
    resources :follows, only: :index, controller: :following_accounts
    resources :blocks, only: :index, controller: :blocked_accounts
    resources :mutes, only: :index, controller: :muted_accounts
    resources :lists, only: :index, controller: :lists
    resources :domain_blocks, only: :index, controller: :blocked_domains
    resources :bookmarks, only: :index, controller: :bookmarks
  end

  resources :two_factor_authentication_methods, only: [:index] do
    collection do
      post :disable
    end
  end

  resource :otp_authentication, only: [:show, :create], controller: 'two_factor_authentication/otp_authentication'

  resources :webauthn_credentials, only: [:index, :new, :create, :destroy],
                                   path: 'security_keys',
                                   controller: 'two_factor_authentication/webauthn_credentials' do
    collection do
      get :options
    end
  end

  namespace :two_factor_authentication do
    resources :recovery_codes, only: [:create]
    resource :confirmation, only: [:new, :create]
  end

  resources :applications, except: [:edit] do
    member do
      post :regenerate
    end
  end

  resources :flavours, only: [:index, :show, :update], param: :flavour

  resource :delete, only: [:show, :destroy]
  resource :migration, only: [:show, :create]

  namespace :migration do
    resource :redirect, only: [:new, :create, :destroy]
  end

  resources :aliases, only: [:index, :create, :destroy]
  resources :sessions, only: [:destroy]
  resources :featured_tags, only: [:index, :create, :destroy]
  resources :login_activities, only: [:index]
end

A db/migrate/20230330135507_create_bulk_imports.rb => db/migrate/20230330135507_create_bulk_imports.rb +22 -0
@@ 0,0 1,22 @@
# frozen_string_literal: true

class CreateBulkImports < ActiveRecord::Migration[6.1]
  def change
    create_table :bulk_imports do |t|
      t.integer :type, null: false
      t.integer :state, null: false
      t.integer :total_items, null: false, default: 0
      t.integer :imported_items, null: false, default: 0
      t.integer :processed_items, null: false, default: 0
      t.datetime :finished_at
      t.boolean :overwrite, null: false, default: false
      t.boolean :likely_mismatched, null: false, default: false
      t.string :original_filename, null: false, default: ''
      t.references :account, null: false, foreign_key: { on_delete: :cascade }

      t.timestamps
    end

    add_index :bulk_imports, [:id], name: :index_bulk_imports_unconfirmed, where: 'state = 0'
  end
end

A db/migrate/20230330140036_create_bulk_import_rows.rb => db/migrate/20230330140036_create_bulk_import_rows.rb +12 -0
@@ 0,0 1,12 @@
# frozen_string_literal: true

class CreateBulkImportRows < ActiveRecord::Migration[6.1]
  def change
    create_table :bulk_import_rows do |t|
      t.references :bulk_import, null: false, foreign_key: { on_delete: :cascade }
      t.jsonb :data

      t.timestamps
    end
  end
end

A db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb => db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb +10 -0
@@ 0,0 1,10 @@
# frozen_string_literal: true

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

  def change
    safety_assured { add_reference :list_accounts, :follow_request, foreign_key: { on_delete: :cascade }, index: false }
    add_index :list_accounts, :follow_request_id, algorithm: :concurrently, where: 'follow_request_id IS NOT NULL'
  end
end

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

ActiveRecord::Schema.define(version: 2023_02_15_074424) do
ActiveRecord::Schema.define(version: 2023_03_30_155710) do

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


@@ 294,6 294,31 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
    t.index ["status_id"], name: "index_bookmarks_on_status_id"
  end

  create_table "bulk_import_rows", force: :cascade do |t|
    t.bigint "bulk_import_id", null: false
    t.jsonb "data"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["bulk_import_id"], name: "index_bulk_import_rows_on_bulk_import_id"
  end

  create_table "bulk_imports", force: :cascade do |t|
    t.integer "type", null: false
    t.integer "state", null: false
    t.integer "total_items", default: 0, null: false
    t.integer "imported_items", default: 0, null: false
    t.integer "processed_items", default: 0, null: false
    t.datetime "finished_at"
    t.boolean "overwrite", default: false, null: false
    t.boolean "likely_mismatched", default: false, null: false
    t.string "original_filename", default: "", null: false
    t.bigint "account_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["account_id"], name: "index_bulk_imports_on_account_id"
    t.index ["id"], name: "index_bulk_imports_unconfirmed", where: "(state = 0)"
  end

  create_table "canonical_email_blocks", force: :cascade do |t|
    t.string "canonical_email_hash", default: "", null: false
    t.bigint "reference_account_id"


@@ 529,8 554,10 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
    t.bigint "list_id", null: false
    t.bigint "account_id", null: false
    t.bigint "follow_id"
    t.bigint "follow_request_id"
    t.index ["account_id", "list_id"], name: "index_list_accounts_on_account_id_and_list_id", unique: true
    t.index ["follow_id"], name: "index_list_accounts_on_follow_id", where: "(follow_id IS NOT NULL)"
    t.index ["follow_request_id"], name: "index_list_accounts_on_follow_request_id", where: "(follow_request_id IS NOT NULL)"
    t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id"
  end



@@ 1149,6 1176,8 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
  add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
  add_foreign_key "bookmarks", "accounts", on_delete: :cascade
  add_foreign_key "bookmarks", "statuses", on_delete: :cascade
  add_foreign_key "bulk_import_rows", "bulk_imports", on_delete: :cascade
  add_foreign_key "bulk_imports", "accounts", on_delete: :cascade
  add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade
  add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
  add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade


@@ 1174,6 1203,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do
  add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
  add_foreign_key "invites", "users", on_delete: :cascade
  add_foreign_key "list_accounts", "accounts", on_delete: :cascade
  add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
  add_foreign_key "list_accounts", "follows", on_delete: :cascade
  add_foreign_key "list_accounts", "lists", on_delete: :cascade
  add_foreign_key "lists", "accounts", on_delete: :cascade

M lib/mastodon/accounts_cli.rb => lib/mastodon/accounts_cli.rb +7 -7
@@ 121,10 121,10 @@ module Mastodon
        say('OK', :green)
        say("New password: #{password}")
      else
        user.errors.to_h.each do |key, error|
        user.errors.each do |error|
          say('Failure/Error: ', :red)
          say(key)
          say("    #{error}", :red)
          say(error.attribute)
          say("    #{error.type}", :red)
        end

        exit(1)


@@ 197,10 197,10 @@ module Mastodon
        say('OK', :green)
        say("New password: #{password}") if options[:reset_password]
      else
        user.errors.to_h.each do |key, error|
        user.errors.each do |error|
          say('Failure/Error: ', :red)
          say(key)
          say("    #{error}", :red)
          say(error.attribute)
          say("    #{error.type}", :red)
        end

        exit(1)


@@ 353,7 353,7 @@ module Mastodon

        begin
          code = Request.new(:head, account.uri).perform(&:code)
        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Mastodon::PrivateNetworkAddressError
          skip_domains << account.domain
        end


M lib/mastodon/emoji_cli.rb => lib/mastodon/emoji_cli.rb +0 -1
@@ 73,7 73,6 @@ module Mastodon
        end
      end

      puts
      say("Imported #{imported}, skipped #{skipped}, failed to import #{failed}", color(imported, skipped, failed))
    end


M lib/mastodon/maintenance_cli.rb => lib/mastodon/maintenance_cli.rb +1 -3
@@ 664,9 664,7 @@ module Mastodon

    def remove_index_if_exists!(table, name)
      ActiveRecord::Base.connection.remove_index(table, name: name)
    rescue ArgumentError
      nil
    rescue ActiveRecord::StatementInvalid
    rescue ArgumentError, ActiveRecord::StatementInvalid
      nil
    end
  end

M lib/mastodon/media_cli.rb => lib/mastodon/media_cli.rb +6 -4
@@ 9,6 9,8 @@ module Mastodon
    include ActionView::Helpers::NumberHelper
    include CLIHelper

    VALID_PATH_SEGMENTS_SIZE = [7, 10].freeze

    def self.exit_on_failure?
      true
    end


@@ 133,7 135,7 @@ module Mastodon
            path_segments = object.key.split('/')
            path_segments.delete('cache')

            unless [7, 10].include?(path_segments.size)
            unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size)
              progress.log(pastel.yellow("Unrecognized file found: #{object.key}"))
              next
            end


@@ 177,7 179,7 @@ module Mastodon
          path_segments = key.split(File::SEPARATOR)
          path_segments.delete('cache')

          unless [7, 10].include?(path_segments.size)
          unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size)
            progress.log(pastel.yellow("Unrecognized file found: #{key}"))
            next
          end


@@ 310,7 312,7 @@ module Mastodon
      path_segments = path.split('/')[2..]
      path_segments.delete('cache')

      unless [7, 10].include?(path_segments.size)
      unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size)
        say('Not a media URL', :red)
        exit(1)
      end


@@ 363,7 365,7 @@ module Mastodon
        segments = object.key.split('/')
        segments.delete('cache')

        next unless [7, 10].include?(segments.size)
        next unless VALID_PATH_SEGMENTS_SIZE.include?(segments.size)

        model_name = segments.first.classify
        record_id  = segments[2..-2].join.to_i

M lib/mastodon/version.rb => lib/mastodon/version.rb +2 -2
@@ 17,11 17,11 @@ module Mastodon
    end

    def flags
      ''
      ENV.fetch('MASTODON_VERSION_FLAGS', '')
    end

    def suffix
      '+glitch'
      "+glitch#{ENV.fetch('MASTODON_VERSION_SUFFIX', '')}"
    end

    def to_a

M lib/paperclip/color_extractor.rb => lib/paperclip/color_extractor.rb +1 -1
@@ 173,7 173,7 @@ module Paperclip
    def palette_from_histogram(result, quantity)
      frequencies       = result.scan(/([0-9]+)\:/).flatten.map(&:to_f)
      hex_values        = result.scan(/\#([0-9A-Fa-f]{6,8})/).flatten
      total_frequencies = frequencies.reduce(&:+).to_f
      total_frequencies = frequencies.sum.to_f

      frequencies.map.with_index { |f, i| [f / total_frequencies, hex_values[i]] }
                 .sort_by { |r| -r[0] }

M lib/terrapin/multi_pipe_extensions.rb => lib/terrapin/multi_pipe_extensions.rb +1 -1
@@ 13,7 13,7 @@ module Terrapin

    def pipe_options
      # Add some flags to explicitly close the other end of the pipes
      { out: @stdout_out, err: @stderr_out, @stdout_in => :close, @stderr_in => :close }
      { :out => @stdout_out, :err => @stderr_out, @stdout_in => :close, @stderr_in => :close }
    end

    def read

M package.json => package.json +14 -13
@@ 26,22 26,23 @@
  },
  "private": true,
  "dependencies": {
    "@babel/core": "^7.21.4",
    "@babel/core": "^7.21.8",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
    "@babel/plugin-transform-react-inline-elements": "^7.21.0",
    "@babel/plugin-transform-runtime": "^7.21.4",
    "@babel/preset-env": "^7.21.4",
    "@babel/preset-env": "^7.21.5",
    "@babel/preset-react": "^7.18.6",
    "@babel/preset-typescript": "^7.21.4",
    "@babel/runtime": "^7.21.0",
    "@babel/preset-typescript": "^7.21.5",
    "@babel/runtime": "^7.21.5",
    "@gamestdio/websocket": "^0.3.2",
    "@github/webauthn-json": "^2.1.1",
    "@rails/ujs": "^6.1.7",
    "@reduxjs/toolkit": "^1.9.5",
    "abortcontroller-polyfill": "^1.7.5",
    "atrament": "0.2.4",
    "arrow-key-navigation": "^1.2.0",
    "autoprefixer": "^10.4.14",
    "axios": "^1.3.6",
    "axios": "^1.4.0",
    "babel-loader": "^8.3.0",
    "babel-plugin-lodash": "^3.3.4",
    "babel-plugin-preval": "^5.1.0",


@@ 54,7 55,7 @@
    "compression-webpack-plugin": "^6.1.1",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.7",
    "cssnano": "^6.0.0",
    "cssnano": "^6.0.1",
    "detect-passive-events": "^2.0.3",
    "dotenv": "^16.0.3",
    "emoji-mart": "npm:emoji-mart-lazyload@latest",


@@ 75,7 76,7 @@
    "intl-messageformat": "^2.2.0",
    "intl-relativeformat": "^6.4.3",
    "js-yaml": "^4.1.0",
    "jsdom": "^21.1.1",
    "jsdom": "^21.1.2",
    "lodash": "^4.17.21",
    "mark-loader": "^0.1.6",
    "mini-css-extract-plugin": "^1.6.2",


@@ 105,7 106,7 @@
    "react-redux-loading-bar": "^5.0.4",
    "react-router-dom": "^4.1.1",
    "react-router-scroll-4": "^1.0.0-beta.1",
    "react-select": "^5.7.2",
    "react-select": "^5.7.3",
    "react-sparklines": "^1.7.0",
    "react-swipeable-views": "^0.14.0",
    "react-textarea-autosize": "^8.4.1",


@@ 180,11 181,11 @@
    "@types/uuid": "^8.3.4",
    "@types/webpack": "^4.41.33",
    "@types/yargs": "^17.0.24",
    "@typescript-eslint/eslint-plugin": "^5.59.1",
    "@typescript-eslint/parser": "^5.59.1",
    "@typescript-eslint/eslint-plugin": "^5.59.2",
    "@typescript-eslint/parser": "^5.59.2",
    "babel-jest": "^29.5.0",
    "eslint": "^8.39.0",
    "eslint-plugin-formatjs": "^4.9.0",
    "eslint-plugin-formatjs": "^4.10.1",
    "eslint-plugin-import": "~2.27.5",
    "eslint-plugin-jsdoc": "^43.1.1",
    "eslint-plugin-jsx-a11y": "~6.7.1",


@@ 199,11 200,11 @@
    "raf": "^3.4.1",
    "react-intl-translations-manager": "^5.0.3",
    "react-test-renderer": "^16.14.0",
    "stylelint": "^15.6.0",
    "stylelint": "^15.6.1",
    "stylelint-config-standard-scss": "^9.0.0",
    "typescript": "^5.0.4",
    "webpack-dev-server": "^3.11.3",
    "yargs": "^17.7.1"
    "yargs": "^17.7.2"
  },
  "resolutions": {
    "kind-of": "^6.0.3",

M spec/config/initializers/rack_attack_spec.rb => spec/config/initializers/rack_attack_spec.rb +8 -8
@@ 46,36 46,36 @@ describe Rack::Attack, type: :request do
  let(:remote_ip) { '1.2.3.5' }

  describe 'throttle excessive sign-up requests by IP address' do
    context 'through the website' do
    context 'when accessed through the website' do
      let(:limit)  { 25 }
      let(:period) { 5.minutes }
      let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }

      context 'for exact path' do
      context 'with exact path' do
        let(:path) { '/auth' }

        it_behaves_like 'throttled endpoint'
      end

      context 'for path with format' do
      context 'with path with format' do
        let(:path) { '/auth.html' }

        it_behaves_like 'throttled endpoint'
      end
    end

    context 'through the API' do
    context 'when accessed through the API' do
      let(:limit)  { 5 }
      let(:period) { 30.minutes }
      let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }

      context 'for exact path' do
      context 'with exact path' do
        let(:path) { '/api/v1/accounts' }

        it_behaves_like 'throttled endpoint'
      end

      context 'for path with format' do
      context 'with path with format' do
        let(:path)  { '/api/v1/accounts.json' }

        it 'returns http not found' do


@@ 91,13 91,13 @@ describe Rack::Attack, type: :request do
    let(:period) { 5.minutes }
    let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }

    context 'for exact path' do
    context 'with exact path' do
      let(:path) { '/auth/sign_in' }

      it_behaves_like 'throttled endpoint'
    end

    context 'for path with format' do
    context 'with path with format' do
      let(:path) { '/auth/sign_in.html' }

      it_behaves_like 'throttled endpoint'

M spec/controllers/about_controller_spec.rb => spec/controllers/about_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AboutController, type: :controller do
RSpec.describe AboutController do
  render_views

  describe 'GET #show' do

M spec/controllers/accounts_controller_spec.rb => spec/controllers/accounts_controller_spec.rb +6 -6
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AccountsController, type: :controller do
RSpec.describe AccountsController do
  render_views

  let(:account) { Fabricate(:account) }


@@ 57,7 57,7 @@ RSpec.describe AccountsController, type: :controller do
      end
    end

    context 'as HTML' do
    context 'with HTML' do
      let(:format) { 'html' }

      it_behaves_like 'preliminary checks'


@@ 140,7 140,7 @@ RSpec.describe AccountsController, type: :controller do
      end
    end

    context 'as JSON' do
    context 'with JSON' do
      let(:authorized_fetch_mode) { false }
      let(:format) { 'json' }



@@ 193,7 193,7 @@ RSpec.describe AccountsController, type: :controller do
          expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
        end

        context 'in authorized fetch mode' do
        context 'with authorized fetch mode' do
          let(:authorized_fetch_mode) { true }

          it 'returns http unauthorized' do


@@ 251,7 251,7 @@ RSpec.describe AccountsController, type: :controller do
          expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
        end

        context 'in authorized fetch mode' do
        context 'with authorized fetch mode' do
          let(:authorized_fetch_mode) { true }

          it 'returns http success' do


@@ 278,7 278,7 @@ RSpec.describe AccountsController, type: :controller do
      end
    end

    context 'as RSS' do
    context 'with RSS' do
      let(:format) { 'rss' }

      it_behaves_like 'preliminary checks'

M spec/controllers/activitypub/collections_controller_spec.rb => spec/controllers/activitypub/collections_controller_spec.rb +3 -4
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe ActivityPub::CollectionsController, type: :controller do
RSpec.describe ActivityPub::CollectionsController do
  let!(:account) { Fabricate(:account) }
  let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
  let(:remote_account) { nil }


@@ 35,10 35,9 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
  describe 'GET #show' do
    context 'when id is "featured"' do
      context 'without signature' do
        subject(:body) { body_as_json }

        subject(:response) { get :show, params: { id: 'featured', account_username: account.username } }

        let(:body) { body_as_json }
        let(:remote_account) { nil }

        it 'returns http success' do


@@ 120,7 119,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
          end
        end

        context 'in authorized fetch mode' do
        context 'with authorized fetch mode' do
          before do
            allow(controller).to receive(:authorized_fetch_mode?).and_return(true)
          end

M spec/controllers/activitypub/followers_synchronizations_controller_spec.rb => spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +2 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controller do
RSpec.describe ActivityPub::FollowersSynchronizationsController do
  let!(:account)    { Fabricate(:account) }
  let!(:follower_1) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') }
  let!(:follower_2) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') }


@@ 34,10 34,9 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
    end

    context 'with signature from example.com' do
      subject(:body) { body_as_json }

      subject(:response) { get :show, params: { account_username: account.username } }

      let(:body) { body_as_json }
      let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }

      it 'returns http success' do

M spec/controllers/activitypub/inboxes_controller_spec.rb => spec/controllers/activitypub/inboxes_controller_spec.rb +2 -2
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe ActivityPub::InboxesController, type: :controller do
RSpec.describe ActivityPub::InboxesController do
  let(:remote_account) { nil }

  before do


@@ 21,7 21,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
        expect(response).to have_http_status(202)
      end

      context 'for a specific account' do
      context 'with a specific account' do
        subject(:response) { post :create, params: { account_username: account.username }, body: '{}' }

        let(:account) { Fabricate(:account) }

M spec/controllers/activitypub/outboxes_controller_spec.rb => spec/controllers/activitypub/outboxes_controller_spec.rb +2 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe ActivityPub::OutboxesController, type: :controller do
RSpec.describe ActivityPub::OutboxesController do
  let!(:account) { Fabricate(:account) }

  shared_examples 'cacheable response' do


@@ 35,10 35,9 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do

  describe 'GET #show' do
    context 'without signature' do
      subject(:body) { body_as_json }

      subject(:response) { get :show, params: { account_username: account.username, page: page } }

      let(:body) { body_as_json }
      let(:remote_account) { nil }

      context 'with page not requested' do

M spec/controllers/activitypub/replies_controller_spec.rb => spec/controllers/activitypub/replies_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe ActivityPub::RepliesController, type: :controller do
RSpec.describe ActivityPub::RepliesController do
  let(:status) { Fabricate(:status, visibility: parent_visibility) }
  let(:remote_account)  { Fabricate(:account, domain: 'foobar.com') }
  let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }

M spec/controllers/admin/account_moderation_notes_controller_spec.rb => spec/controllers/admin/account_moderation_notes_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::AccountModerationNotesController, type: :controller do
RSpec.describe Admin::AccountModerationNotesController do
  render_views

  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }

M spec/controllers/admin/accounts_controller_spec.rb => spec/controllers/admin/accounts_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::AccountsController, type: :controller do
RSpec.describe Admin::AccountsController do
  render_views

  before { sign_in current_user, scope: :user }

M spec/controllers/admin/action_logs_controller_spec.rb => spec/controllers/admin/action_logs_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe Admin::ActionLogsController, type: :controller do
describe Admin::ActionLogsController do
  render_views

  # Action logs typically cause issues when their targets are not in the database

M spec/controllers/admin/base_controller_spec.rb => spec/controllers/admin/base_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe Admin::BaseController, type: :controller do
describe Admin::BaseController do
  controller do
    def success
      authorize :dashboard, :index?

M spec/controllers/admin/change_emails_controller_spec.rb => spec/controllers/admin/change_emails_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::ChangeEmailsController, type: :controller do
RSpec.describe Admin::ChangeEmailsController do
  render_views

  let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }

M spec/controllers/admin/confirmations_controller_spec.rb => spec/controllers/admin/confirmations_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::ConfirmationsController, type: :controller do
RSpec.describe Admin::ConfirmationsController do
  render_views

  before do

M spec/controllers/admin/dashboard_controller_spec.rb => spec/controllers/admin/dashboard_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe Admin::DashboardController, type: :controller do
describe Admin::DashboardController do
  render_views

  describe 'GET #index' do

M spec/controllers/admin/disputes/appeals_controller_spec.rb => spec/controllers/admin/disputes/appeals_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::Disputes::AppealsController, type: :controller do
RSpec.describe Admin::Disputes::AppealsController do
  render_views

  before { sign_in current_user, scope: :user }

M spec/controllers/admin/domain_allows_controller_spec.rb => spec/controllers/admin/domain_allows_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::DomainAllowsController, type: :controller do
RSpec.describe Admin::DomainAllowsController do
  render_views

  before do

M spec/controllers/admin/domain_blocks_controller_spec.rb => spec/controllers/admin/domain_blocks_controller_spec.rb +3 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::DomainBlocksController, type: :controller do
RSpec.describe Admin::DomainBlocksController do
  render_views

  before do


@@ 83,7 83,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
      BlockDomainService.new.call(domain_block)
    end

    context 'downgrading a domain suspension to silence' do
    context 'when downgrading a domain suspension to silence' do
      let(:original_severity) { 'suspend' }
      let(:new_severity)      { 'silence' }



@@ 100,7 100,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
      end
    end

    context 'upgrading a domain silence to suspend' do
    context 'when upgrading a domain silence to suspend' do
      let(:original_severity) { 'silence' }
      let(:new_severity)      { 'suspend' }


M spec/controllers/admin/email_domain_blocks_controller_spec.rb => spec/controllers/admin/email_domain_blocks_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::EmailDomainBlocksController, type: :controller do
RSpec.describe Admin::EmailDomainBlocksController do
  render_views

  before do

M spec/controllers/admin/export_domain_allows_controller_spec.rb => spec/controllers/admin/export_domain_allows_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::ExportDomainAllowsController, type: :controller do
RSpec.describe Admin::ExportDomainAllowsController do
  render_views

  before do

M spec/controllers/admin/export_domain_blocks_controller_spec.rb => spec/controllers/admin/export_domain_blocks_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::ExportDomainBlocksController, type: :controller do
RSpec.describe Admin::ExportDomainBlocksController do
  render_views

  before do

M spec/controllers/admin/instances_controller_spec.rb => spec/controllers/admin/instances_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::InstancesController, type: :controller do
RSpec.describe Admin::InstancesController do
  render_views

  let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }

M spec/controllers/admin/reports/actions_controller_spec.rb => spec/controllers/admin/reports/actions_controller_spec.rb +3 -3
@@ 15,7 15,7 @@ describe Admin::Reports::ActionsController do
    let(:report) { Fabricate(:report) }

    before do
      post :preview, params: { report_id: report.id, action => '' }
      post :preview, params: { :report_id => report.id, action => '' }
    end

    context 'when the action is "suspend"' do


@@ 146,13 146,13 @@ describe Admin::Reports::ActionsController do
      end
    end

    context 'action as submit button' do
    context 'with Action as submit button' do
      subject { post :create, params: common_params.merge({ action => '' }) }

      it_behaves_like 'all action types'
    end

    context 'action as submit button' do
    context 'with Action as submit button' do
      subject { post :create, params: common_params.merge({ moderation_action: action }) }

      it_behaves_like 'all action types'

M spec/controllers/admin/settings/branding_controller_spec.rb => spec/controllers/admin/settings/branding_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::Settings::BrandingController, type: :controller do
RSpec.describe Admin::Settings::BrandingController do
  render_views

  describe 'When signed in as an admin' do

M spec/controllers/admin/statuses_controller_spec.rb => spec/controllers/admin/statuses_controller_spec.rb +2 -2
@@ 30,7 30,7 @@ describe Admin::StatusesController do
      end
    end

    context 'filtering by media' do
    context 'when filtering by media' do
      before do
        get :index, params: { account_id: account.id, media: '1' }
      end


@@ 43,7 43,7 @@ describe Admin::StatusesController do

  describe 'POST #batch' do
    before do
      post :batch, params: { account_id: account.id, action => '', admin_status_batch_action: { status_ids: status_ids } }
      post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } }
    end

    let(:status_ids) { [media_attached_status.id] }

M spec/controllers/admin/tags_controller_spec.rb => spec/controllers/admin/tags_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::TagsController, type: :controller do
RSpec.describe Admin::TagsController do
  render_views

  before do

M spec/controllers/api/base_controller_spec.rb => spec/controllers/api/base_controller_spec.rb +10 -10
@@ 2,9 2,11 @@

require 'rails_helper'

class FakeService; end

describe Api::BaseController do
  before do
    stub_const('FakeService', Class.new)
  end

  controller do
    def success
      head 200


@@ 72,7 74,11 @@ describe Api::BaseController do
  end

  describe 'error handling' do
    ERRORS_WITH_CODES = {
    before do
      routes.draw { get 'error' => 'api/base#error' }
    end

    {
      ActiveRecord::RecordInvalid => 422,
      Mastodon::ValidationError => 422,
      ActiveRecord::RecordNotFound => 404,


@@ 80,13 86,7 @@ describe Api::BaseController do
      HTTP::Error => 503,
      OpenSSL::SSL::SSLError => 503,
      Mastodon::NotPermittedError => 403,
    }

    before do
      routes.draw { get 'error' => 'api/base#error' }
    end

    ERRORS_WITH_CODES.each do |error, code|
    }.each do |error, code|
      it "Handles error class of #{error}" do
        expect(FakeService).to receive(:new).and_raise(error)


M spec/controllers/api/oembed_controller_spec.rb => spec/controllers/api/oembed_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::OEmbedController, type: :controller do
RSpec.describe Api::OEmbedController do
  render_views

  let(:alice)  { Fabricate(:account, username: 'alice') }

M spec/controllers/api/v1/accounts/pins_controller_spec.rb => spec/controllers/api/v1/accounts/pins_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Accounts::PinsController, type: :controller do
RSpec.describe Api::V1::Accounts::PinsController do
  let(:john)  { Fabricate(:user) }
  let(:kevin) { Fabricate(:user) }
  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: john.id, scopes: 'write:accounts') }

M spec/controllers/api/v1/accounts/relationships_controller_spec.rb => spec/controllers/api/v1/accounts/relationships_controller_spec.rb +2 -2
@@ 21,7 21,7 @@ describe Api::V1::Accounts::RelationshipsController do
      lewis.follow!(user.account)
    end

    context 'provided only one ID' do
    context 'when provided only one ID' do
      before do
        get :index, params: { id: simon.id }
      end


@@ 39,7 39,7 @@ describe Api::V1::Accounts::RelationshipsController do
      end
    end

    context 'provided multiple IDs' do
    context 'when provided multiple IDs' do
      before do
        get :index, params: { id: [simon.id, lewis.id] }
      end

M spec/controllers/api/v1/accounts/search_controller_spec.rb => spec/controllers/api/v1/accounts/search_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
RSpec.describe Api::V1::Accounts::SearchController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/accounts_controller_spec.rb => spec/controllers/api/v1/accounts_controller_spec.rb +4 -4
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::AccountsController, type: :controller do
RSpec.describe Api::V1::AccountsController do
  render_views

  let(:user)   { Fabricate(:user) }


@@ 30,7 30,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
      post :create, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement }
    end

    context 'given truthy agreement' do
    context 'when given truthy agreement' do
      let(:agreement) { 'true' }

      it 'returns http success' do


@@ 48,7 48,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
      end
    end

    context 'given no agreement' do
    context 'when given no agreement' do
      it 'returns http unprocessable entity' do
        expect(response).to have_http_status(422)
      end


@@ 121,7 121,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
      end
    end

    context 'modifying follow options' do
    context 'when modifying follow options' do
      let(:locked) { false }

      before do

M spec/controllers/api/v1/admin/account_actions_controller_spec.rb => spec/controllers/api/v1/admin/account_actions_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do
RSpec.describe Api::V1::Admin::AccountActionsController do
  render_views

  let(:role)   { UserRole.find_by(name: 'Moderator') }

M spec/controllers/api/v1/admin/accounts_controller_spec.rb => spec/controllers/api/v1/admin/accounts_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
RSpec.describe Api::V1::Admin::AccountsController do
  render_views

  let(:role)   { UserRole.find_by(name: 'Moderator') }

M spec/controllers/api/v1/admin/domain_allows_controller_spec.rb => spec/controllers/api/v1/admin/domain_allows_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
RSpec.describe Api::V1::Admin::DomainAllowsController do
  render_views

  let(:role)   { UserRole.find_by(name: 'Admin') }

M spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb => spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb +3 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
RSpec.describe Api::V1::Admin::DomainBlocksController do
  render_views

  let(:role)   { UserRole.find_by(name: 'Admin') }


@@ 84,7 84,7 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
      BlockDomainService.new.call(domain_block)
    end

    context 'downgrading a domain suspension to silence' do
    context 'when downgrading a domain suspension to silence' do
      let(:original_severity) { 'suspend' }
      let(:new_severity)      { 'silence' }



@@ 101,7 101,7 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
      end
    end

    context 'upgrading a domain silence to suspend' do
    context 'when upgrading a domain silence to suspend' do
      let(:original_severity) { 'silence' }
      let(:new_severity)      { 'suspend' }


M spec/controllers/api/v1/admin/reports_controller_spec.rb => spec/controllers/api/v1/admin/reports_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
RSpec.describe Api::V1::Admin::ReportsController do
  render_views

  let(:role)   { UserRole.find_by(name: 'Moderator') }

M spec/controllers/api/v1/announcements/reactions_controller_spec.rb => spec/controllers/api/v1/announcements/reactions_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Announcements::ReactionsController, type: :controller do
RSpec.describe Api::V1::Announcements::ReactionsController do
  render_views

  let(:user)   { Fabricate(:user) }

M spec/controllers/api/v1/announcements_controller_spec.rb => spec/controllers/api/v1/announcements_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::AnnouncementsController, type: :controller do
RSpec.describe Api::V1::AnnouncementsController do
  render_views

  let(:user)   { Fabricate(:user) }

M spec/controllers/api/v1/apps_controller_spec.rb => spec/controllers/api/v1/apps_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::AppsController, type: :controller do
RSpec.describe Api::V1::AppsController do
  render_views

  describe 'POST #create' do

M spec/controllers/api/v1/blocks_controller_spec.rb => spec/controllers/api/v1/blocks_controller_spec.rb +5 -5
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::BlocksController, type: :controller do
RSpec.describe Api::V1::BlocksController do
  render_views

  let(:user)   { Fabricate(:user) }


@@ 13,13 13,13 @@ RSpec.describe Api::V1::BlocksController, type: :controller do

  describe 'GET #index' do
    it 'limits according to limit parameter' do
      2.times.map { Fabricate(:block, account: user.account) }
      Array.new(2) { Fabricate(:block, account: user.account) }
      get :index, params: { limit: 1 }
      expect(body_as_json.size).to eq 1
    end

    it 'queries blocks in range according to max_id' do
      blocks = 2.times.map { Fabricate(:block, account: user.account) }
      blocks = Array.new(2) { Fabricate(:block, account: user.account) }

      get :index, params: { max_id: blocks[1] }



@@ 28,7 28,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
    end

    it 'queries blocks in range according to since_id' do
      blocks = 2.times.map { Fabricate(:block, account: user.account) }
      blocks = Array.new(2) { Fabricate(:block, account: user.account) }

      get :index, params: { since_id: blocks[0] }



@@ 37,7 37,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
    end

    it 'sets pagination header for next path' do
      blocks = 2.times.map { Fabricate(:block, account: user.account) }
      blocks = Array.new(2) { Fabricate(:block, account: user.account) }
      get :index, params: { limit: 1, since_id: blocks[0] }
      expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v1_blocks_url(limit: 1, max_id: blocks[1])
    end

M spec/controllers/api/v1/bookmarks_controller_spec.rb => spec/controllers/api/v1/bookmarks_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::BookmarksController, type: :controller do
RSpec.describe Api::V1::BookmarksController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/conversations_controller_spec.rb => spec/controllers/api/v1/conversations_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::ConversationsController, type: :controller do
RSpec.describe Api::V1::ConversationsController do
  render_views

  let!(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) }

M spec/controllers/api/v1/custom_emojis_controller_spec.rb => spec/controllers/api/v1/custom_emojis_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::CustomEmojisController, type: :controller do
RSpec.describe Api::V1::CustomEmojisController do
  render_views

  describe 'GET #index' do

M spec/controllers/api/v1/domain_blocks_controller_spec.rb => spec/controllers/api/v1/domain_blocks_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::DomainBlocksController, type: :controller do
RSpec.describe Api::V1::DomainBlocksController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/emails/confirmations_controller_spec.rb => spec/controllers/api/v1/emails/confirmations_controller_spec.rb +4 -4
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Emails::ConfirmationsController, type: :controller do
RSpec.describe Api::V1::Emails::ConfirmationsController do
  let(:confirmed_at) { nil }
  let(:user)         { Fabricate(:user, confirmed_at: confirmed_at) }
  let(:app)          { Fabricate(:application) }


@@ 15,14 15,14 @@ RSpec.describe Api::V1::Emails::ConfirmationsController, type: :controller do
        allow(controller).to receive(:doorkeeper_token) { token }
      end

      context 'from a random app' do
      context 'when from a random app' do
        it 'returns http forbidden' do
          post :create
          expect(response).to have_http_status(403)
        end
      end

      context 'from an app that created the account' do
      context 'when from an app that created the account' do
        before do
          user.update(created_by_application: token.application)
        end


@@ 35,7 35,7 @@ RSpec.describe Api::V1::Emails::ConfirmationsController, type: :controller do
            expect(response).to have_http_status(403)
          end

          context 'but user changed e-mail and has not confirmed it' do
          context 'with user changed e-mail and has not confirmed it' do
            before do
              user.update(email: 'foo@bar.com')
            end

M spec/controllers/api/v1/endorsements_controller_spec.rb => spec/controllers/api/v1/endorsements_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::EndorsementsController, type: :controller do
RSpec.describe Api::V1::EndorsementsController do
  let(:user)  { Fabricate(:user) }
  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }


M spec/controllers/api/v1/favourites_controller_spec.rb => spec/controllers/api/v1/favourites_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::FavouritesController, type: :controller do
RSpec.describe Api::V1::FavouritesController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/filters_controller_spec.rb => spec/controllers/api/v1/filters_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::FiltersController, type: :controller do
RSpec.describe Api::V1::FiltersController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/follow_requests_controller_spec.rb => spec/controllers/api/v1/follow_requests_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::FollowRequestsController, type: :controller do
RSpec.describe Api::V1::FollowRequestsController do
  render_views

  let(:user)     { Fabricate(:user, account_attributes: { locked: true }) }

M spec/controllers/api/v1/followed_tags_controller_spec.rb => spec/controllers/api/v1/followed_tags_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::FollowedTagsController, type: :controller do
RSpec.describe Api::V1::FollowedTagsController do
  render_views

  let(:user)   { Fabricate(:user) }

M spec/controllers/api/v1/instances/activity_controller_spec.rb => spec/controllers/api/v1/instances/activity_controller_spec.rb +2 -2
@@ 2,14 2,14 @@

require 'rails_helper'

RSpec.describe Api::V1::Instances::ActivityController, type: :controller do
RSpec.describe Api::V1::Instances::ActivityController do
  describe 'GET #show' do
    it 'returns 200' do
      get :show
      expect(response).to have_http_status(200)
    end

    context '!Setting.activity_api_enabled' do
    context 'with !Setting.activity_api_enabled' do
      it 'returns 404' do
        Setting.activity_api_enabled = false


M spec/controllers/api/v1/instances/peers_controller_spec.rb => spec/controllers/api/v1/instances/peers_controller_spec.rb +2 -2
@@ 2,14 2,14 @@

require 'rails_helper'

RSpec.describe Api::V1::Instances::PeersController, type: :controller do
RSpec.describe Api::V1::Instances::PeersController do
  describe 'GET #index' do
    it 'returns 200' do
      get :index
      expect(response).to have_http_status(200)
    end

    context '!Setting.peers_api_enabled' do
    context 'with !Setting.peers_api_enabled' do
      it 'returns 404' do
        Setting.peers_api_enabled = false


M spec/controllers/api/v1/instances_controller_spec.rb => spec/controllers/api/v1/instances_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::InstancesController, type: :controller do
RSpec.describe Api::V1::InstancesController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/lists/accounts_controller_spec.rb => spec/controllers/api/v1/lists/accounts_controller_spec.rb +38 -7
@@ 29,17 29,48 @@ describe Api::V1::Lists::AccountsController do
    let(:scopes) { 'write:lists' }
    let(:bob) { Fabricate(:account, username: 'bob') }

    before do
      user.account.follow!(bob)
      post :create, params: { list_id: list.id, account_ids: [bob.id] }
    context 'when the added account is followed' do
      before do
        user.account.follow!(bob)
        post :create, params: { list_id: list.id, account_ids: [bob.id] }
      end

      it 'returns http success' do
        expect(response).to have_http_status(200)
      end

      it 'adds account to the list' do
        expect(list.accounts.include?(bob)).to be true
      end
    end

    it 'returns http success' do
      expect(response).to have_http_status(200)
    context 'when the added account has been sent a follow request' do
      before do
        user.account.follow_requests.create!(target_account: bob)
        post :create, params: { list_id: list.id, account_ids: [bob.id] }
      end

      it 'returns http success' do
        expect(response).to have_http_status(200)
      end

      it 'adds account to the list' do
        expect(list.accounts.include?(bob)).to be true
      end
    end

    it 'adds account to the list' do
      expect(list.accounts.include?(bob)).to be true
    context 'when the added account is not followed' do
      before do
        post :create, params: { list_id: list.id, account_ids: [bob.id] }
      end

      it 'returns http not found' do
        expect(response).to have_http_status(404)
      end

      it 'does not add the account to the list' do
        expect(list.accounts.include?(bob)).to be false
      end
    end
  end


M spec/controllers/api/v1/lists_controller_spec.rb => spec/controllers/api/v1/lists_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::ListsController, type: :controller do
RSpec.describe Api::V1::ListsController do
  render_views

  let!(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/markers_controller_spec.rb => spec/controllers/api/v1/markers_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::MarkersController, type: :controller do
RSpec.describe Api::V1::MarkersController do
  render_views

  let!(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/media_controller_spec.rb => spec/controllers/api/v1/media_controller_spec.rb +4 -4
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::MediaController, type: :controller do
RSpec.describe Api::V1::MediaController do
  render_views

  let(:user)  { Fabricate(:user) }


@@ 37,7 37,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
      end
    end

    context 'image/jpeg' do
    context 'with image/jpeg' do
      before do
        post :create, params: { file: fixture_file_upload('attachment.jpg', 'image/jpeg') }
      end


@@ 59,7 59,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
      end
    end

    context 'image/gif' do
    context 'with image/gif' do
      before do
        post :create, params: { file: fixture_file_upload('attachment.gif', 'image/gif') }
      end


@@ 81,7 81,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
      end
    end

    context 'video/webm' do
    context 'with video/webm' do
      before do
        post :create, params: { file: fixture_file_upload('attachment.webm', 'video/webm') }
      end

M spec/controllers/api/v1/mutes_controller_spec.rb => spec/controllers/api/v1/mutes_controller_spec.rb +5 -5
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::MutesController, type: :controller do
RSpec.describe Api::V1::MutesController do
  render_views

  let(:user)   { Fabricate(:user) }


@@ 13,13 13,13 @@ RSpec.describe Api::V1::MutesController, type: :controller do

  describe 'GET #index' do
    it 'limits according to limit parameter' do
      2.times.map { Fabricate(:mute, account: user.account) }
      Array.new(2) { Fabricate(:mute, account: user.account) }
      get :index, params: { limit: 1 }
      expect(body_as_json.size).to eq 1
    end

    it 'queries mutes in range according to max_id' do
      mutes = 2.times.map { Fabricate(:mute, account: user.account) }
      mutes = Array.new(2) { Fabricate(:mute, account: user.account) }

      get :index, params: { max_id: mutes[1] }



@@ 28,7 28,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do
    end

    it 'queries mutes in range according to since_id' do
      mutes = 2.times.map { Fabricate(:mute, account: user.account) }
      mutes = Array.new(2) { Fabricate(:mute, account: user.account) }

      get :index, params: { since_id: mutes[0] }



@@ 37,7 37,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do
    end

    it 'sets pagination header for next path' do
      mutes = 2.times.map { Fabricate(:mute, account: user.account) }
      mutes = Array.new(2) { Fabricate(:mute, account: user.account) }
      get :index, params: { limit: 1, since_id: mutes[0] }
      expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v1_mutes_url(limit: 1, max_id: mutes[1])
    end

M spec/controllers/api/v1/notifications_controller_spec.rb => spec/controllers/api/v1/notifications_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::NotificationsController, type: :controller do
RSpec.describe Api::V1::NotificationsController do
  render_views

  let(:user)  { Fabricate(:user, account_attributes: { username: 'alice' }) }

M spec/controllers/api/v1/polls/votes_controller_spec.rb => spec/controllers/api/v1/polls/votes_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Polls::VotesController, type: :controller do
RSpec.describe Api::V1::Polls::VotesController do
  render_views

  let(:user)   { Fabricate(:user) }

M spec/controllers/api/v1/polls_controller_spec.rb => spec/controllers/api/v1/polls_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::PollsController, type: :controller do
RSpec.describe Api::V1::PollsController do
  render_views

  let(:user)   { Fabricate(:user) }

M spec/controllers/api/v1/reports_controller_spec.rb => spec/controllers/api/v1/reports_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::ReportsController, type: :controller do
RSpec.describe Api::V1::ReportsController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb => spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :controller do
RSpec.describe Api::V1::Statuses::FavouritedByAccountsController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb => spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controller do
RSpec.describe Api::V1::Statuses::RebloggedByAccountsController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/statuses_controller_spec.rb => spec/controllers/api/v1/statuses_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::StatusesController, type: :controller do
RSpec.describe Api::V1::StatusesController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/suggestions_controller_spec.rb => spec/controllers/api/v1/suggestions_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::SuggestionsController, type: :controller do
RSpec.describe Api::V1::SuggestionsController do
  render_views

  let(:user)  { Fabricate(:user) }

M spec/controllers/api/v1/tags_controller_spec.rb => spec/controllers/api/v1/tags_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::TagsController, type: :controller do
RSpec.describe Api::V1::TagsController do
  render_views

  let(:user)   { Fabricate(:user) }

M spec/controllers/api/v1/trends/tags_controller_spec.rb => spec/controllers/api/v1/trends/tags_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V1::Trends::TagsController, type: :controller do
RSpec.describe Api::V1::Trends::TagsController do
  render_views

  describe 'GET #index' do

M spec/controllers/api/v2/admin/accounts_controller_spec.rb => spec/controllers/api/v2/admin/accounts_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V2::Admin::AccountsController, type: :controller do
RSpec.describe Api::V2::Admin::AccountsController do
  render_views

  let(:role)   { UserRole.find_by(name: 'Moderator') }

M spec/controllers/api/v2/filters/keywords_controller_spec.rb => spec/controllers/api/v2/filters/keywords_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V2::Filters::KeywordsController, type: :controller do
RSpec.describe Api::V2::Filters::KeywordsController do
  render_views

  let(:user)         { Fabricate(:user) }

M spec/controllers/api/v2/filters/statuses_controller_spec.rb => spec/controllers/api/v2/filters/statuses_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V2::Filters::StatusesController, type: :controller do
RSpec.describe Api::V2::Filters::StatusesController do
  render_views

  let(:user)         { Fabricate(:user) }

M spec/controllers/api/v2/filters_controller_spec.rb => spec/controllers/api/v2/filters_controller_spec.rb +3 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V2::FiltersController, type: :controller do
RSpec.describe Api::V2::FiltersController do
  render_views

  let(:user)  { Fabricate(:user) }


@@ 66,7 66,7 @@ RSpec.describe Api::V2::FiltersController, type: :controller do
    let!(:filter)  { Fabricate(:custom_filter, account: user.account) }
    let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }

    context 'updating filter parameters' do
    context 'when updating filter parameters' do
      before do
        put :update, params: { id: filter.id, title: 'updated', context: %w(home public) }
      end


@@ 84,7 84,7 @@ RSpec.describe Api::V2::FiltersController, type: :controller do
      end
    end

    context 'updating keywords in bulk' do
    context 'when updating keywords in bulk' do
      before do
        allow(redis).to receive_messages(publish: nil)
        put :update, params: { id: filter.id, keywords_attributes: [{ id: keyword.id, keyword: 'updated' }] }

M spec/controllers/api/v2/search_controller_spec.rb => spec/controllers/api/v2/search_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Api::V2::SearchController, type: :controller do
RSpec.describe Api::V2::SearchController do
  render_views

  context 'with token' do

M spec/controllers/api/web/embeds_controller_spec.rb => spec/controllers/api/web/embeds_controller_spec.rb +2 -2
@@ 10,10 10,10 @@ describe Api::Web::EmbedsController do
  before { sign_in user }

  describe 'POST #create' do
    subject(:response) { post :create, params: { url: url } }

    subject(:body) { JSON.parse(response.body, symbolize_names: true) }

    let(:response) { post :create, params: { url: url } }

    context 'when successfully finds status' do
      let(:status) { Fabricate(:status) }
      let(:url) { "http://#{Rails.configuration.x.web_domain}/@#{status.account.username}/#{status.id}" }

M spec/controllers/application_controller_spec.rb => spec/controllers/application_controller_spec.rb +13 -11
@@ 2,7 2,7 @@

require 'rails_helper'

describe ApplicationController, type: :controller do
describe ApplicationController do
  controller do
    def success
      head 200


@@ 32,7 32,7 @@ describe ApplicationController, type: :controller do
    end
  end

  context 'forgery' do
  context 'with a forgery' do
    subject do
      ActionController::Base.allow_forgery_protection = true
      routes.draw { post 'success' => 'anonymous#success' }


@@ 112,7 112,7 @@ describe ApplicationController, type: :controller do
    end
  end

  context 'ActionController::RoutingError' do
  context 'with ActionController::RoutingError' do
    subject do
      routes.draw { get 'routing_error' => 'anonymous#routing_error' }
      get 'routing_error'


@@ 121,7 121,7 @@ describe ApplicationController, type: :controller do
    include_examples 'respond_with_error', 404
  end

  context 'ActiveRecord::RecordNotFound' do
  context 'with ActiveRecord::RecordNotFound' do
    subject do
      routes.draw { get 'record_not_found' => 'anonymous#record_not_found' }
      get 'record_not_found'


@@ 130,7 130,7 @@ describe ApplicationController, type: :controller do
    include_examples 'respond_with_error', 404
  end

  context 'ActionController::InvalidAuthenticityToken' do
  context 'with ActionController::InvalidAuthenticityToken' do
    subject do
      routes.draw { get 'invalid_authenticity_token' => 'anonymous#invalid_authenticity_token' }
      get 'invalid_authenticity_token'


@@ 230,14 230,16 @@ describe ApplicationController, type: :controller do
  end

  describe 'cache_collection' do
    class C < ApplicationController
      public :cache_collection
    subject do
      Class.new(ApplicationController) do
        public :cache_collection
      end
    end

    shared_examples 'receives :with_includes' do |fabricator, klass|
      it 'uses raw if it is not an ActiveRecord::Relation' do
        record = Fabricate(fabricator)
        expect(C.new.cache_collection([record], klass)).to eq [record]
        expect(subject.new.cache_collection([record], klass)).to eq [record]
      end
    end



@@ 248,16 250,16 @@ describe ApplicationController, type: :controller do
        record = Fabricate(fabricator)
        relation = klass.none
        allow(relation).to receive(:cache_ids).and_return([record])
        expect(C.new.cache_collection(relation, klass)).to eq [record]
        expect(subject.new.cache_collection(relation, klass)).to eq [record]
      end
    end

    it 'returns raw unless class responds to :with_includes' do
      raw = Object.new
      expect(C.new.cache_collection(raw, Object)).to eq raw
      expect(subject.new.cache_collection(raw, Object)).to eq raw
    end

    context 'Status' do
    context 'with a Status' do
      include_examples 'cacheable', :status, Status
    end
  end

M spec/controllers/auth/challenges_controller_spec.rb => spec/controllers/auth/challenges_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe Auth::ChallengesController, type: :controller do
describe Auth::ChallengesController do
  render_views

  let(:password) { 'foobar12345' }

M spec/controllers/auth/confirmations_controller_spec.rb => spec/controllers/auth/confirmations_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe Auth::ConfirmationsController, type: :controller do
describe Auth::ConfirmationsController do
  render_views

  describe 'GET #new' do

M spec/controllers/auth/passwords_controller_spec.rb => spec/controllers/auth/passwords_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe Auth::PasswordsController, type: :controller do
describe Auth::PasswordsController do
  include Devise::Test::ControllerHelpers

  describe 'GET #new' do

M spec/controllers/auth/registrations_controller_spec.rb => spec/controllers/auth/registrations_controller_spec.rb +4 -4
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Auth::RegistrationsController, type: :controller do
RSpec.describe Auth::RegistrationsController do
  render_views

  shared_examples 'checks for enabled registrations' do |path|


@@ 157,7 157,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
      end
    end

    context 'approval-based registrations without invite' do
    context 'with Approval-based registrations without invite' do
      subject do
        Setting.registrations_mode = 'approved'
        request.headers['Accept-Language'] = accept_language


@@ 184,7 184,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
      end
    end

    context 'approval-based registrations with expired invite' do
    context 'with Approval-based registrations with expired invite' do
      subject do
        Setting.registrations_mode = 'approved'
        request.headers['Accept-Language'] = accept_language


@@ 212,7 212,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
      end
    end

    context 'approval-based registrations with valid invite and required invite text' do
    context 'with Approval-based registrations with valid invite and required invite text' do
      subject do
        inviter = Fabricate(:user, confirmed_at: 2.days.ago)
        Setting.registrations_mode = 'approved'

M spec/controllers/auth/sessions_controller_spec.rb => spec/controllers/auth/sessions_controller_spec.rb +24 -24
@@ 3,7 3,7 @@
require 'rails_helper'
require 'webauthn/fake_client'

RSpec.describe Auth::SessionsController, type: :controller do
RSpec.describe Auth::SessionsController do
  render_views

  before do


@@ 51,8 51,8 @@ RSpec.describe Auth::SessionsController, type: :controller do
  end

  describe 'POST #create' do
    context 'using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do
      context 'using a valid password' do
    context 'when using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do
      context 'when using a valid password' do
        before do
          post :create, params: { user: { email: 'pam_user1', password: '123456' } }
        end


@@ 66,7 66,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context 'using an invalid password' do
      context 'when using an invalid password' do
        before do
          post :create, params: { user: { email: 'pam_user1', password: 'WRONGPW' } }
        end


@@ 80,7 80,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context 'using a valid email and existing user' do
      context 'when using a valid email and existing user' do
        let!(:user) do
          account = Fabricate.build(:account, username: 'pam_user1', user: nil)
          account.save!(validate: false)


@@ 102,10 102,10 @@ RSpec.describe Auth::SessionsController, type: :controller do
      end
    end

    context 'using password authentication' do
    context 'when using password authentication' do
      let(:user) { Fabricate(:user, email: 'foo@bar.com', password: 'abcdefgh') }

      context 'using a valid password' do
      context 'when using a valid password' do
        before do
          post :create, params: { user: { email: user.email, password: user.password } }
        end


@@ 119,7 119,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context 'using a valid password on a previously-used account with a new IP address' do
      context 'when using a valid password on a previously-used account with a new IP address' do
        let(:previous_ip) { '1.2.3.4' }
        let(:current_ip)  { '4.3.2.1' }



@@ 145,7 145,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context 'using email with uppercase letters' do
      context 'when using email with uppercase letters' do
        before do
          post :create, params: { user: { email: user.email.upcase, password: user.password } }
        end


@@ 159,7 159,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context 'using an invalid password' do
      context 'when using an invalid password' do
        before do
          post :create, params: { user: { email: user.email, password: 'wrongpw' } }
        end


@@ 173,7 173,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context 'using an unconfirmed password' do
      context 'when using an unconfirmed password' do
        before do
          request.headers['Accept-Language'] = accept_language
          post :create, params: { user: { email: unconfirmed_user.email, password: unconfirmed_user.password } }


@@ 187,14 187,14 @@ RSpec.describe Auth::SessionsController, type: :controller do
        end
      end

      context "logging in from the user's page" do
      context "when logging in from the user's page" do
        before do
          allow(controller).to receive(:single_user_mode?).and_return(single_user_mode)
          allow(controller).to receive(:stored_location_for).with(:user).and_return("/@#{user.account.username}")
          post :create, params: { user: { email: user.email, password: user.password } }
        end

        context 'in single user mode' do
        context 'with single user mode' do
          let(:single_user_mode) { true }

          it 'redirects to home' do


@@ 202,7 202,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'in non-single user mode' do
        context 'with non-single user mode' do
          let(:single_user_mode) { false }

          it "redirects back to the user's page" do


@@ 212,7 212,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
      end
    end

    context 'using two-factor authentication' do
    context 'when using two-factor authentication' do
      context 'with OTP enabled as second factor' do
        let!(:user) do
          Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))


@@ 224,7 224,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          return codes
        end

        context 'using email and password' do
        context 'when using email and password' do
          before do
            post :create, params: { user: { email: user.email, password: user.password } }
          end


@@ 235,7 235,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do
        context 'when using email and password after an unfinished log-in attempt to a 2FA-protected account' do
          let!(:other_user) do
            Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
          end


@@ 251,7 251,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using upcase email and password' do
        context 'when using upcase email and password' do
          before do
            post :create, params: { user: { email: user.email.upcase, password: user.password } }
          end


@@ 262,7 262,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using a valid OTP' do
        context 'when using a valid OTP' do
          before do
            post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
          end


@@ 291,7 291,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using a valid recovery code' do
        context 'when using a valid recovery code' do
          before do
            post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
          end


@@ 305,7 305,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using an invalid OTP' do
        context 'when using an invalid OTP' do
          before do
            post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
          end


@@ 353,7 353,7 @@ RSpec.describe Auth::SessionsController, type: :controller do

        let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }

        context 'using email and password' do
        context 'when using email and password' do
          before do
            post :create, params: { user: { email: user.email, password: user.password } }
          end


@@ 364,7 364,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using upcase email and password' do
        context 'when using upcase email and password' do
          before do
            post :create, params: { user: { email: user.email.upcase, password: user.password } }
          end


@@ 375,7 375,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
          end
        end

        context 'using a valid webauthn credential' do
        context 'when using a valid webauthn credential' do
          before do
            @controller.session[:webauthn_challenge] = challenge


M spec/controllers/concerns/account_controller_concern_spec.rb => spec/controllers/concerns/account_controller_concern_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe ApplicationController, type: :controller do
describe ApplicationController do
  controller do
    include AccountControllerConcern


M spec/controllers/concerns/accountable_concern_spec.rb => spec/controllers/concerns/accountable_concern_spec.rb +8 -6
@@ 3,18 3,20 @@
require 'rails_helper'

RSpec.describe AccountableConcern do
  class Hoge
    include AccountableConcern
    attr_reader :current_account
  let(:hoge_class) do
    Class.new do
      include AccountableConcern
      attr_reader :current_account

    def initialize(current_account)
      @current_account = current_account
      def initialize(current_account)
        @current_account = current_account
      end
    end
  end

  let(:user)   { Fabricate(:account) }
  let(:target) { Fabricate(:account) }
  let(:hoge)   { Hoge.new(user) }
  let(:hoge)   { hoge_class.new(user) }

  describe '#log_action' do
    it 'creates Admin::ActionLog' do

M spec/controllers/concerns/cache_concern_spec.rb => spec/controllers/concerns/cache_concern_spec.rb +3 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe CacheConcern, type: :controller do
RSpec.describe CacheConcern do
  controller(ApplicationController) do
    include CacheConcern



@@ 23,14 23,14 @@ RSpec.describe CacheConcern, type: :controller do
  end

  describe '#cache_collection' do
    context 'given an empty array' do
    context 'when given an empty array' do
      it 'returns an empty array' do
        get :empty_array
        expect(response.body).to eq '0'
      end
    end

    context 'given an empty relation' do
    context 'when given an empty relation' do
      it 'returns an empty array' do
        get :empty_relation
        expect(response.body).to eq '0'

M spec/controllers/concerns/challengable_concern_spec.rb => spec/controllers/concerns/challengable_concern_spec.rb +7 -7
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe ChallengableConcern, type: :controller do
RSpec.describe ChallengableConcern do
  controller(ApplicationController) do
    include ChallengableConcern



@@ 31,7 31,7 @@ RSpec.describe ChallengableConcern, type: :controller do
      sign_in user
    end

    context 'for GET requests' do
    context 'with GET requests' do
      before { get :foo }

      it 'does not ask for password' do


@@ 39,7 39,7 @@ RSpec.describe ChallengableConcern, type: :controller do
      end
    end

    context 'for POST requests' do
    context 'with POST requests' do
      before { post :bar }

      it 'does not ask for password' do


@@ 56,7 56,7 @@ RSpec.describe ChallengableConcern, type: :controller do
      sign_in user
    end

    context 'for GET requests' do
    context 'with GET requests' do
      before { get :foo, session: { challenge_passed_at: Time.now.utc } }

      it 'does not ask for password' do


@@ 64,7 64,7 @@ RSpec.describe ChallengableConcern, type: :controller do
      end
    end

    context 'for POST requests' do
    context 'with POST requests' do
      before { post :bar, session: { challenge_passed_at: Time.now.utc } }

      it 'does not ask for password' do


@@ 81,7 81,7 @@ RSpec.describe ChallengableConcern, type: :controller do
      sign_in user
    end

    context 'for GET requests' do
    context 'with GET requests' do
      before { get :foo }

      it 'renders challenge' do


@@ 91,7 91,7 @@ RSpec.describe ChallengableConcern, type: :controller do
      # See Auth::ChallengesControllerSpec
    end

    context 'for POST requests' do
    context 'with POST requests' do
      before { post :bar }

      it 'renders challenge' do

M spec/controllers/concerns/export_controller_concern_spec.rb => spec/controllers/concerns/export_controller_concern_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe ApplicationController, type: :controller do
describe ApplicationController do
  controller do
    include ExportControllerConcern


M spec/controllers/concerns/localized_spec.rb => spec/controllers/concerns/localized_spec.rb +4 -4
@@ 2,7 2,7 @@

require 'rails_helper'

describe ApplicationController, type: :controller do
describe ApplicationController do
  controller do
    include Localized



@@ 41,7 41,7 @@ describe ApplicationController, type: :controller do
    end
  end

  context 'user with valid locale has signed in' do
  context 'with a user with valid locale has signed in' do
    it "sets user's locale" do
      user = Fabricate(:user, locale: :ca)



@@ 52,7 52,7 @@ describe ApplicationController, type: :controller do
    end
  end

  context 'user with invalid locale has signed in' do
  context 'with a user with invalid locale has signed in' do
    before do
      user = Fabricate.build(:user, locale: :invalid)
      user.save!(validate: false)


@@ 62,7 62,7 @@ describe ApplicationController, type: :controller do
    include_examples 'default locale'
  end

  context 'user has not signed in' do
  context 'with a user who has not signed in' do
    include_examples 'default locale'
  end
end

M spec/controllers/concerns/rate_limit_headers_spec.rb => spec/controllers/concerns/rate_limit_headers_spec.rb +2 -2
@@ 16,7 16,7 @@ describe ApplicationController do
  end

  describe 'rate limiting' do
    context 'throttling is off' do
    context 'when throttling is off' do
      before do
        request.env['rack.attack.throttle_data'] = nil
      end


@@ 30,7 30,7 @@ describe ApplicationController do
      end
    end

    context 'throttling is on' do
    context 'when throttling is on' do
      let(:start_time) { DateTime.new(2017, 1, 1, 12, 0, 0).utc }

      before do

M spec/controllers/concerns/signature_verification_spec.rb => spec/controllers/concerns/signature_verification_spec.rb +12 -10
@@ 2,15 2,17 @@

require 'rails_helper'

describe ApplicationController, type: :controller do
  class WrappedActor
    attr_reader :wrapped_account
describe ApplicationController do
  let(:wrapped_actor_class) do
    Class.new do
      attr_reader :wrapped_account

    def initialize(wrapped_account)
      @wrapped_account = wrapped_account
    end
      def initialize(wrapped_account)
        @wrapped_account = wrapped_account
      end

    delegate :uri, :keypair, to: :wrapped_account
      delegate :uri, :keypair, to: :wrapped_account
    end
  end

  controller do


@@ 33,8 35,8 @@ describe ApplicationController, type: :controller do

  before do
    routes.draw do
      match via: [:get, :post], 'success' => 'anonymous#success'
      match via: [:get, :post], 'signature_required' => 'anonymous#signature_required'
      match :via => [:get, :post], 'success' => 'anonymous#success'
      match :via => [:get, :post], 'signature_required' => 'anonymous#signature_required'
    end
  end



@@ 93,7 95,7 @@ describe ApplicationController, type: :controller do
    end

    context 'with a valid actor that is not an Account' do
      let(:actor) { WrappedActor.new(author) }
      let(:actor) { wrapped_actor_class.new(author) }

      before do
        get :success

M spec/controllers/concerns/user_tracking_concern_spec.rb => spec/controllers/concerns/user_tracking_concern_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe ApplicationController, type: :controller do
describe ApplicationController do
  controller do
    include UserTrackingConcern


M spec/controllers/disputes/appeals_controller_spec.rb => spec/controllers/disputes/appeals_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Disputes::AppealsController, type: :controller do
RSpec.describe Disputes::AppealsController do
  render_views

  before { sign_in current_user, scope: :user }

M spec/controllers/disputes/strikes_controller_spec.rb => spec/controllers/disputes/strikes_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Disputes::StrikesController, type: :controller do
RSpec.describe Disputes::StrikesController do
  render_views

  before { sign_in current_user, scope: :user }

M spec/controllers/emojis_controller_spec.rb => spec/controllers/emojis_controller_spec.rb +2 -2
@@ 8,10 8,10 @@ describe EmojisController do
  let(:emoji) { Fabricate(:custom_emoji) }

  describe 'GET #show' do
    subject(:response) { get :show, params: { id: emoji.id, format: :json } }

    subject(:body) { JSON.parse(response.body, symbolize_names: true) }

    let(:response) { get :show, params: { id: emoji.id, format: :json } }

    it 'returns the right response' do
      expect(response).to have_http_status 200
      expect(body[:name]).to eq ':coolcat:'

M spec/controllers/follower_accounts_controller_spec.rb => spec/controllers/follower_accounts_controller_spec.rb +2 -2
@@ 39,10 39,10 @@ describe FollowerAccountsController do
    end

    context 'when format is json' do
      subject(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } }

      subject(:body) { response.parsed_body }

      let(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } }

      context 'with page' do
        let(:page) { 1 }


M spec/controllers/following_accounts_controller_spec.rb => spec/controllers/following_accounts_controller_spec.rb +2 -2
@@ 39,10 39,10 @@ describe FollowingAccountsController do
    end

    context 'when format is json' do
      subject(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } }

      subject(:body) { response.parsed_body }

      let(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } }

      context 'with page' do
        let(:page) { 1 }


M spec/controllers/home_controller_spec.rb => spec/controllers/home_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe HomeController, type: :controller do
RSpec.describe HomeController do
  render_views

  describe 'GET #index' do

M spec/controllers/instance_actors_controller_spec.rb => spec/controllers/instance_actors_controller_spec.rb +2 -2
@@ 2,9 2,9 @@

require 'rails_helper'

RSpec.describe InstanceActorsController, type: :controller do
RSpec.describe InstanceActorsController do
  describe 'GET #show' do
    context 'as JSON' do
    context 'with JSON' do
      let(:format) { 'json' }

      shared_examples 'shared behavior' do

M spec/controllers/intents_controller_spec.rb => spec/controllers/intents_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe IntentsController, type: :controller do
RSpec.describe IntentsController do
  render_views

  let(:user) { Fabricate(:user) }

M spec/controllers/oauth/authorizations_controller_spec.rb => spec/controllers/oauth/authorizations_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Oauth::AuthorizationsController, type: :controller do
RSpec.describe Oauth::AuthorizationsController do
  render_views

  let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }

M spec/controllers/oauth/tokens_controller_spec.rb => spec/controllers/oauth/tokens_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Oauth::TokensController, type: :controller do
RSpec.describe Oauth::TokensController do
  describe 'POST #revoke' do
    let!(:user) { Fabricate(:user) }
    let!(:application) { Fabricate(:application, confidential: false) }

M spec/controllers/settings/applications_controller_spec.rb => spec/controllers/settings/applications_controller_spec.rb +5 -5
@@ 50,7 50,7 @@ describe Settings::ApplicationsController do
  end

  describe 'POST #create' do
    context 'success (passed scopes as a String)' do
    context 'when success (passed scopes as a String)' do
      def call_create
        post :create, params: {
          doorkeeper_application: {


@@ 72,7 72,7 @@ describe Settings::ApplicationsController do
      end
    end

    context 'success (passed scopes as an Array)' do
    context 'when success (passed scopes as an Array)' do
      def call_create
        post :create, params: {
          doorkeeper_application: {


@@ 94,7 94,7 @@ describe Settings::ApplicationsController do
      end
    end

    context 'failure' do
    context 'with failure request' do
      before do
        post :create, params: {
          doorkeeper_application: {


@@ 117,7 117,7 @@ describe Settings::ApplicationsController do
  end

  describe 'PATCH #update' do
    context 'success' do
    context 'when success' do
      let(:opts) do
        {
          website: 'https://foo.bar/',


@@ 142,7 142,7 @@ describe Settings::ApplicationsController do
      end
    end

    context 'failure' do
    context 'with failure request' do
      before do
        patch :update, params: {
          id: app.id,

M spec/controllers/settings/imports_controller_spec.rb => spec/controllers/settings/imports_controller_spec.rb +287 -21
@@ 2,16 2,25 @@

require 'rails_helper'

RSpec.describe Settings::ImportsController, type: :controller do
RSpec.describe Settings::ImportsController do
  render_views

  let(:user) { Fabricate(:user) }

  before do
    sign_in Fabricate(:user), scope: :user
    sign_in user, scope: :user
  end

  describe 'GET #show' do
  describe 'GET #index' do
    let!(:import)       { Fabricate(:bulk_import, account: user.account) }
    let!(:other_import) { Fabricate(:bulk_import) }

    before do
      get :show
      get :index
    end

    it 'assigns the expected imports' do
      expect(assigns(:recent_imports)).to eq [import]
    end

    it 'returns http success' do


@@ 23,31 32,288 @@ RSpec.describe Settings::ImportsController, type: :controller do
    end
  end

  describe 'GET #show' do
    before do
      get :show, params: { id: bulk_import.id }
    end

    context 'with someone else\'s import' do
      let(:bulk_import) { Fabricate(:bulk_import, state: :unconfirmed) }

      it 'returns http not found' do
        expect(response).to have_http_status(404)
      end
    end

    context 'with an already-confirmed import' do
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :in_progress) }

      it 'returns http not found' do
        expect(response).to have_http_status(404)
      end
    end

    context 'with an unconfirmed import' do
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :unconfirmed) }

      it 'returns http success' do
        expect(response).to have_http_status(200)
      end
    end
  end

  describe 'POST #confirm' do
    subject { post :confirm, params: { id: bulk_import.id } }

    before do
      allow(BulkImportWorker).to receive(:perform_async)
    end

    context 'with someone else\'s import' do
      let(:bulk_import) { Fabricate(:bulk_import, state: :unconfirmed) }

      it 'does not change the import\'s state' do
        expect { subject }.to_not(change { bulk_import.reload.state })
      end

      it 'does not fire the import worker' do
        subject
        expect(BulkImportWorker).to_not have_received(:perform_async)
      end

      it 'returns http not found' do
        subject
        expect(response).to have_http_status(404)
      end
    end

    context 'with an already-confirmed import' do
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :in_progress) }

      it 'does not change the import\'s state' do
        expect { subject }.to_not(change { bulk_import.reload.state })
      end

      it 'does not fire the import worker' do
        subject
        expect(BulkImportWorker).to_not have_received(:perform_async)
      end

      it 'returns http not found' do
        subject
        expect(response).to have_http_status(404)
      end
    end

    context 'with an unconfirmed import' do
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :unconfirmed) }

      it 'changes the import\'s state to scheduled' do
        expect { subject }.to change { bulk_import.reload.state.to_sym }.from(:unconfirmed).to(:scheduled)
      end

      it 'fires the import worker on the expected import' do
        subject
        expect(BulkImportWorker).to have_received(:perform_async).with(bulk_import.id)
      end

      it 'redirects to imports path' do
        subject
        expect(response).to redirect_to(settings_imports_path)
      end
    end
  end

  describe 'DELETE #destroy' do
    subject { delete :destroy, params: { id: bulk_import.id } }

    context 'with someone else\'s import' do
      let(:bulk_import) { Fabricate(:bulk_import, state: :unconfirmed) }

      it 'does not delete the import' do
        expect { subject }.to_not(change { BulkImport.exists?(bulk_import.id) })
      end

      it 'returns http not found' do
        subject
        expect(response).to have_http_status(404)
      end
    end

    context 'with an already-confirmed import' do
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :in_progress) }

      it 'does not delete the import' do
        expect { subject }.to_not(change { BulkImport.exists?(bulk_import.id) })
      end

      it 'returns http not found' do
        subject
        expect(response).to have_http_status(404)
      end
    end

    context 'with an unconfirmed import' do
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :unconfirmed) }

      it 'deletes the import' do
        expect { subject }.to change { BulkImport.exists?(bulk_import.id) }.from(true).to(false)
      end

      it 'redirects to imports path' do
        subject
        expect(response).to redirect_to(settings_imports_path)
      end
    end
  end

  describe 'GET #failures' do
    subject { get :failures, params: { id: bulk_import.id }, format: :csv }

    shared_examples 'export failed rows' do |expected_contents|
      let(:bulk_import) { Fabricate(:bulk_import, account: user.account, type: import_type, state: :finished) }

      before do
        bulk_import.update(total_items: bulk_import.rows.count, processed_items: bulk_import.rows.count, imported_items: 0)
      end

      it 'returns http success' do
        subject
        expect(response).to have_http_status(200)
      end

      it 'returns expected contents' do
        subject
        expect(response.body).to eq expected_contents
      end
    end

    context 'with follows' do
      let(:import_type) { 'following' }

      let!(:rows) do
        [
          { 'acct' => 'foo@bar' },
          { 'acct' => 'user@bar', 'show_reblogs' => false, 'notify' => true, 'languages' => ['fr', 'de'] },
        ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
      end

      include_examples 'export failed rows', "Account address,Show boosts,Notify on new posts,Languages\nfoo@bar,true,false,\nuser@bar,false,true,\"fr, de\"\n"
    end

    context 'with blocks' do
      let(:import_type) { 'blocking' }

      let!(:rows) do
        [
          { 'acct' => 'foo@bar' },
          { 'acct' => 'user@bar' },
        ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
      end

      include_examples 'export failed rows', "foo@bar\nuser@bar\n"
    end

    context 'with mutes' do
      let(:import_type) { 'muting' }

      let!(:rows) do
        [
          { 'acct' => 'foo@bar' },
          { 'acct' => 'user@bar', 'hide_notifications' => false },
        ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
      end

      include_examples 'export failed rows', "Account address,Hide notifications\nfoo@bar,true\nuser@bar,false\n"
    end

    context 'with domain blocks' do
      let(:import_type) { 'domain_blocking' }

      let!(:rows) do
        [
          { 'domain' => 'bad.domain' },
          { 'domain' => 'evil.domain' },
        ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
      end

      include_examples 'export failed rows', "bad.domain\nevil.domain\n"
    end

    context 'with bookmarks' do
      let(:import_type) { 'bookmarks' }

      let!(:rows) do
        [
          { 'uri' => 'https://foo.com/1' },
          { 'uri' => 'https://foo.com/2' },
        ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
      end

      include_examples 'export failed rows', "https://foo.com/1\nhttps://foo.com/2\n"
    end
  end

  describe 'POST #create' do
    it 'redirects to settings path with successful following import' do
      service = double(call: nil)
      allow(ResolveAccountService).to receive(:new).and_return(service)
    subject do
      post :create, params: {
        import: {
          type: 'following',
          data: fixture_file_upload('imports.txt'),
        form_import: {
          type: import_type,
          mode: import_mode,
          data: fixture_file_upload(import_file),
        },
      }
    end

    shared_examples 'successful import' do |type, file, mode|
      let(:import_type) { type }
      let(:import_file) { file }
      let(:import_mode) { mode }

      expect(response).to redirect_to(settings_import_path)
      it 'creates an unconfirmed bulk_import with expected type' do
        expect { subject }.to change { user.account.bulk_imports.pluck(:state, :type) }.from([]).to([['unconfirmed', import_type]])
      end

      it 'redirects to confirmation page for the import' do
        subject
        expect(response).to redirect_to(settings_import_path(user.account.bulk_imports.first))
      end
    end

    it 'redirects to settings path with successful blocking import' do
      service = double(call: nil)
      allow(ResolveAccountService).to receive(:new).and_return(service)
      post :create, params: {
        import: {
          type: 'blocking',
          data: fixture_file_upload('imports.txt'),
        },
      }
    shared_examples 'unsuccessful import' do |type, file, mode|
      let(:import_type) { type }
      let(:import_file) { file }
      let(:import_mode) { mode }

      it 'does not creates an unconfirmed bulk_import' do
        expect { subject }.to_not(change { user.account.bulk_imports.count })
      end

      expect(response).to redirect_to(settings_import_path)
      it 'sets error to the import' do
        subject
        expect(assigns(:import).errors).to_not be_empty
      end
    end

    it_behaves_like 'successful import', 'following', 'imports.txt', 'merge'
    it_behaves_like 'successful import', 'following', 'imports.txt', 'overwrite'
    it_behaves_like 'successful import', 'blocking', 'imports.txt', 'merge'
    it_behaves_like 'successful import', 'blocking', 'imports.txt', 'overwrite'
    it_behaves_like 'successful import', 'muting', 'imports.txt', 'merge'
    it_behaves_like 'successful import', 'muting', 'imports.txt', 'overwrite'
    it_behaves_like 'successful import', 'domain_blocking', 'domain_blocks.csv', 'merge'
    it_behaves_like 'successful import', 'domain_blocking', 'domain_blocks.csv', 'overwrite'
    it_behaves_like 'successful import', 'bookmarks', 'bookmark-imports.txt', 'merge'
    it_behaves_like 'successful import', 'bookmarks', 'bookmark-imports.txt', 'overwrite'

    it_behaves_like 'unsuccessful import', 'following', 'domain_blocks.csv', 'merge'
    it_behaves_like 'unsuccessful import', 'following', 'domain_blocks.csv', 'overwrite'
    it_behaves_like 'unsuccessful import', 'blocking', 'domain_blocks.csv', 'merge'
    it_behaves_like 'unsuccessful import', 'blocking', 'domain_blocks.csv', 'overwrite'
    it_behaves_like 'unsuccessful import', 'muting', 'domain_blocks.csv', 'merge'
    it_behaves_like 'unsuccessful import', 'muting', 'domain_blocks.csv', 'overwrite'

    it_behaves_like 'unsuccessful import', 'following', 'empty.csv', 'merge'
    it_behaves_like 'unsuccessful import', 'following', 'empty.csv', 'overwrite'
  end
end

M spec/controllers/settings/preferences/appearance_controller_spec.rb => spec/controllers/settings/preferences/appearance_controller_spec.rb +6 -0
@@ 31,5 31,11 @@ describe Settings::Preferences::AppearanceController do

      expect(response).to redirect_to(settings_preferences_appearance_path)
    end

    it 'renders show on failure' do
      put :update, params: { user: { locale: 'fake option' } }

      expect(response).to render_template('preferences/appearance/show')
    end
  end
end

A spec/controllers/settings/preferences/base_controller_spec.rb => spec/controllers/settings/preferences/base_controller_spec.rb +11 -0
@@ 0,0 1,11 @@
# frozen_string_literal: true

require 'rails_helper'

describe Settings::Preferences::BaseController do
  describe 'after_update_redirect_path' do
    it 'raises error when called' do
      expect { described_class.new.send(:after_update_redirect_path) }.to raise_error(/Override/)
    end
  end
end

M spec/controllers/settings/profiles_controller_spec.rb => spec/controllers/settings/profiles_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Settings::ProfilesController, type: :controller do
RSpec.describe Settings::ProfilesController do
  render_views

  let!(:user) { Fabricate(:user) }

M spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb => spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb +1 -1
@@ 275,7 275,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
        end

        context 'when user have not enabled webauthn' do
          context 'creation succeeds' do
          context 'when creation succeeds' do
            it 'creates a webauthn credential' do
              @controller.session[:webauthn_challenge] = challenge


M spec/controllers/statuses_cleanup_controller_spec.rb => spec/controllers/statuses_cleanup_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe StatusesCleanupController, type: :controller do
RSpec.describe StatusesCleanupController do
  render_views

  before do

M spec/controllers/statuses_controller_spec.rb => spec/controllers/statuses_controller_spec.rb +26 -26
@@ 72,7 72,7 @@ describe StatusesController do
        get :show, params: { account_username: status.account.username, id: status.id, format: format }
      end

      context 'as HTML' do
      context 'with HTML' do
        let(:format) { 'html' }

        it 'returns http success' do


@@ 97,7 97,7 @@ describe StatusesController do
        end
      end

      context 'as JSON' do
      context 'with JSON' do
        let(:format) { 'json' }

        it 'returns http success' do


@@ 132,7 132,7 @@ describe StatusesController do
        get :show, params: { account_username: status.account.username, id: status.id, format: format }
      end

      context 'as JSON' do
      context 'with JSON' do
        let(:format) { 'json' }

        it 'returns http not found' do


@@ 140,7 140,7 @@ describe StatusesController do
        end
      end

      context 'as HTML' do
      context 'with HTML' do
        let(:format) { 'html' }

        it 'returns http not found' do


@@ 156,7 156,7 @@ describe StatusesController do
        get :show, params: { account_username: status.account.username, id: status.id, format: format }
      end

      context 'as JSON' do
      context 'with JSON' do
        let(:format) { 'json' }

        it 'returns http not found' do


@@ 164,7 164,7 @@ describe StatusesController do
        end
      end

      context 'as HTML' do
      context 'with HTML' do
        let(:format) { 'html' }

        it 'returns http not found' do


@@ 196,7 196,7 @@ describe StatusesController do
          get :show, params: { account_username: status.account.username, id: status.id, format: format }
        end

        context 'as HTML' do
        context 'with HTML' do
          let(:format) { 'html' }

          it 'returns http success' do


@@ 221,7 221,7 @@ describe StatusesController do
          end
        end

        context 'as JSON' do
        context 'with JSON' do
          let(:format) { 'json' }

          it 'returns http success' do


@@ 260,7 260,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http success' do


@@ 285,7 285,7 @@ describe StatusesController do
            end
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http success' do


@@ 320,7 320,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http not found' do


@@ 328,7 328,7 @@ describe StatusesController do
            end
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http not found' do


@@ 347,7 347,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http success' do


@@ 372,7 372,7 @@ describe StatusesController do
            end
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http success' do


@@ 407,7 407,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http not found' do


@@ 415,7 415,7 @@ describe StatusesController do
            end
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http not found' do


@@ 460,7 460,7 @@ describe StatusesController do
          get :show, params: { account_username: status.account.username, id: status.id, format: format }
        end

        context 'as HTML' do
        context 'with HTML' do
          let(:format) { 'html' }

          it 'returns http success' do


@@ 485,7 485,7 @@ describe StatusesController do
          end
        end

        context 'as JSON' do
        context 'with JSON' do
          let(:format) { 'json' }

          it 'returns http success' do


@@ 522,7 522,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http success' do


@@ 547,7 547,7 @@ describe StatusesController do
            end
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http success' do


@@ 582,7 582,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http not found' do


@@ 590,7 590,7 @@ describe StatusesController do
            end
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http not found' do


@@ 609,7 609,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http success' do


@@ 634,7 634,7 @@ describe StatusesController do
            end
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http success' do


@@ 669,7 669,7 @@ describe StatusesController do
            get :show, params: { account_username: status.account.username, id: status.id, format: format }
          end

          context 'as JSON' do
          context 'with JSON' do
            let(:format) { 'json' }

            it 'returns http not found' do


@@ 677,7 677,7 @@ describe StatusesController do
            end
          end

          context 'as HTML' do
          context 'with HTML' do
            let(:format) { 'html' }

            it 'returns http not found' do

M spec/controllers/tags_controller_spec.rb => spec/controllers/tags_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe TagsController, type: :controller do
RSpec.describe TagsController do
  render_views

  describe 'GET #show' do

M spec/controllers/well_known/host_meta_controller_spec.rb => spec/controllers/well_known/host_meta_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe WellKnown::HostMetaController, type: :controller do
describe WellKnown::HostMetaController do
  render_views

  describe 'GET #show' do

M spec/controllers/well_known/nodeinfo_controller_spec.rb => spec/controllers/well_known/nodeinfo_controller_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe WellKnown::NodeInfoController, type: :controller do
describe WellKnown::NodeInfoController do
  render_views

  describe 'GET #index' do

M spec/controllers/well_known/webfinger_controller_spec.rb => spec/controllers/well_known/webfinger_controller_spec.rb +37 -9
@@ 2,11 2,11 @@

require 'rails_helper'

describe WellKnown::WebfingerController, type: :controller do
describe WellKnown::WebfingerController do
  render_views

  describe 'GET #show' do
    subject do
    subject(:perform_show!) do
      get :show, params: { resource: resource }, format: :json
    end



@@ 45,7 45,7 @@ describe WellKnown::WebfingerController, type: :controller do
      let(:resource) { alice.to_webfinger_s }

      before do
        subject
        perform_show!
      end

      it_behaves_like 'a successful response'


@@ 56,7 56,7 @@ describe WellKnown::WebfingerController, type: :controller do

      before do
        alice.suspend!
        subject
        perform_show!
      end

      it_behaves_like 'a successful response'


@@ 68,7 68,7 @@ describe WellKnown::WebfingerController, type: :controller do
      before do
        alice.suspend!
        alice.deletion_request.destroy
        subject
        perform_show!
      end

      it 'returns http gone' do


@@ 80,7 80,7 @@ describe WellKnown::WebfingerController, type: :controller do
      let(:resource) { 'acct:not@existing.com' }

      before do
        subject
        perform_show!
      end

      it 'returns http not found' do


@@ 92,7 92,7 @@ describe WellKnown::WebfingerController, type: :controller do
      let(:alternate_domains) { ['foo.org'] }

      before do
        subject
        perform_show!
      end

      context 'when an account exists' do


@@ 116,11 116,39 @@ describe WellKnown::WebfingerController, type: :controller do
      end
    end

    context 'when the old name scheme is used to query the instance actor' do
      let(:resource) do
        "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}"
      end

      before do
        perform_show!
      end

      it 'returns http success' do
        expect(response).to have_http_status(200)
      end

      it 'does not set a Vary header' do
        expect(response.headers['Vary']).to be_nil
      end

      it 'returns application/jrd+json' do
        expect(response.media_type).to eq 'application/jrd+json'
      end

      it 'returns links for the internal account' do
        json = body_as_json
        expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io'
        expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor']
      end
    end

    context 'with no resource parameter' do
      let(:resource) { nil }

      before do
        subject
        perform_show!
      end

      it 'returns http bad request' do


@@ 132,7 160,7 @@ describe WellKnown::WebfingerController, type: :controller do
      let(:resource) { 'df/:dfkj' }

      before do
        subject
        perform_show!
      end

      it 'returns http bad request' do

A spec/fabricators/bulk_import_fabricator.rb => spec/fabricators/bulk_import_fabricator.rb +12 -0
@@ 0,0 1,12 @@
# frozen_string_literal: true

Fabricator(:bulk_import) do
  type            1
  state           1
  total_items     1
  processed_items 1
  imported_items  1
  finished_at     '2022-11-18 14:55:07'
  overwrite       false
  account
end

A spec/fabricators/bulk_import_row_fabricator.rb => spec/fabricators/bulk_import_row_fabricator.rb +6 -0
@@ 0,0 1,6 @@
# frozen_string_literal: true

Fabricator(:bulk_import_row) do
  bulk_import
  data ''
end

A spec/fixtures/files/empty.csv => spec/fixtures/files/empty.csv +0 -0
A spec/fixtures/files/following_accounts.csv => spec/fixtures/files/following_accounts.csv +5 -0
@@ 0,0 1,5 @@
Account address,Show boosts,Notify on new posts,Languages

user@example.com,true,false,

user@test.com,true,true,"en,fr"

A spec/fixtures/files/muted_accounts.csv => spec/fixtures/files/muted_accounts.csv +5 -0
@@ 0,0 1,5 @@
Account address,Hide notifications

user@example.com,true

user@test.com,false

M spec/helpers/accounts_helper_spec.rb => spec/helpers/accounts_helper_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AccountsHelper, type: :helper do
RSpec.describe AccountsHelper do
  def set_not_embedded_view
    params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}"
    params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}"

M spec/helpers/admin/account_moderation_notes_helper_spec.rb => spec/helpers/admin/account_moderation_notes_helper_spec.rb +3 -3
@@ 2,11 2,11 @@

require 'rails_helper'

RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
RSpec.describe Admin::AccountModerationNotesHelper do
  include AccountsHelper

  describe '#admin_account_link_to' do
    context 'account is nil' do
    context 'when Account is nil' do
      let(:account) { nil }

      it 'returns nil' do


@@ 30,7 30,7 @@ RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
  end

  describe '#admin_account_inline_link_to' do
    context 'account is nil' do
    context 'when Account is nil' do
      let(:account) { nil }

      it 'returns nil' do

M spec/helpers/admin/action_logs_helper_spec.rb => spec/helpers/admin/action_logs_helper_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Admin::ActionLogsHelper, type: :helper do
RSpec.describe Admin::ActionLogsHelper do
end

M spec/helpers/application_helper_spec.rb => spec/helpers/application_helper_spec.rb +158 -0
@@ 124,6 124,164 @@ describe ApplicationHelper do
    end
  end

  describe 'available_sign_up_path' do
    context 'when registrations are closed' do
      before do
        without_partial_double_verification do
          allow(Setting).to receive(:registrations_mode).and_return('none')
        end
      end

      it 'redirects to joinmastodon site' do
        expect(helper.available_sign_up_path).to match(/joinmastodon.org/)
      end
    end

    context 'when in omniauth only mode' do
      around do |example|
        ClimateControl.modify OMNIAUTH_ONLY: 'true' do
          example.run
        end
      end

      it 'redirects to joinmastodon site' do
        expect(helper.available_sign_up_path).to match(/joinmastodon.org/)
      end
    end

    context 'when registrations are allowed' do
      it 'returns a link to the registration page' do
        expect(helper.available_sign_up_path).to eq(new_user_registration_path)
      end
    end
  end

  describe 'omniauth_only?' do
    context 'when env var is set to true' do
      around do |example|
        ClimateControl.modify OMNIAUTH_ONLY: 'true' do
          example.run
        end
      end

      it 'returns true' do
        expect(helper).to be_omniauth_only
      end
    end

    context 'when env var is not set' do
      around do |example|
        ClimateControl.modify OMNIAUTH_ONLY: nil do
          example.run
        end
      end

      it 'returns false' do
        expect(helper).to_not be_omniauth_only
      end
    end
  end

  describe 'quote_wrap' do
    it 'indents and quote wraps text' do
      text = <<~TEXT
        Hello this is a nice message for you to quote.
        Be careful because it has two lines.
      TEXT

      expect(helper.quote_wrap(text)).to eq <<~EXPECTED.strip
        > Hello this is a nice message for you to quote.
        > Be careful because it has two lines.
      EXPECTED
    end
  end

  describe 'storage_host' do
    context 'when S3 alias is present' do
      around do |example|
        ClimateControl.modify S3_ALIAS_HOST: 's3.alias' do
          example.run
        end
      end

      it 'returns true' do
        expect(helper.storage_host).to eq('https://s3.alias')
      end
    end

    context 'when S3 cloudfront is present' do
      around do |example|
        ClimateControl.modify S3_CLOUDFRONT_HOST: 's3.cloudfront' do
          example.run
        end
      end

      it 'returns true' do
        expect(helper.storage_host).to eq('https://s3.cloudfront')
      end
    end

    context 'when neither env value is present' do
      it 'returns false' do
        expect(helper.storage_host).to eq('https:')
      end
    end
  end

  describe 'storage_host?' do
    context 'when S3 alias is present' do
      around do |example|
        ClimateControl.modify S3_ALIAS_HOST: 's3.alias' do
          example.run
        end
      end

      it 'returns true' do
        expect(helper.storage_host?).to be true
      end
    end

    context 'when S3 cloudfront is present' do
      around do |example|
        ClimateControl.modify S3_CLOUDFRONT_HOST: 's3.cloudfront' do
          example.run
        end
      end

      it 'returns true' do
        expect(helper.storage_host?).to be true
      end
    end

    context 'when neither env value is present' do
      it 'returns false' do
        expect(helper.storage_host?).to be false
      end
    end
  end

  describe 'visibility_icon' do
    it 'returns a globe icon for a public visible status' do
      result = helper.visibility_icon Status.new(visibility: 'public')
      expect(result).to match(/globe/)
    end

    it 'returns an unlock icon for a unlisted visible status' do
      result = helper.visibility_icon Status.new(visibility: 'unlisted')
      expect(result).to match(/unlock/)
    end

    it 'returns a lock icon for a private visible status' do
      result = helper.visibility_icon Status.new(visibility: 'private')
      expect(result).to match(/lock/)
    end

    it 'returns an at icon for a direct visible status' do
      result = helper.visibility_icon Status.new(visibility: 'direct')
      expect(result).to match(/at/)
    end
  end

  describe 'title' do
    around do |example|
      site_title = Setting.site_title

M spec/helpers/flashes_helper_spec.rb => spec/helpers/flashes_helper_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe FlashesHelper, type: :helper do
describe FlashesHelper do
  describe 'user_facing_flashes' do
    it 'returns user facing flashes' do
      flash[:alert] = 'an alert'

M spec/helpers/formatting_helper_spec.rb => spec/helpers/formatting_helper_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe FormattingHelper, type: :helper do
describe FormattingHelper do
  include Devise::Test::ControllerHelpers

  describe '#rss_status_content_format' do

M spec/helpers/home_helper_spec.rb => spec/helpers/home_helper_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe HomeHelper, type: :helper do
RSpec.describe HomeHelper do
  describe 'default_props' do
    it 'returns default properties according to the context' do
      expect(helper.default_props).to eq locale: I18n.locale

M spec/helpers/jsonld_helper_spec.rb => spec/helpers/jsonld_helper_spec.rb +5 -5
@@ 22,14 22,14 @@ describe JsonLdHelper do
  end

  describe '#first_of_value' do
    context 'value.is_a?(Array)' do
    context 'when value.is_a?(Array)' do
      it 'returns value.first' do
        value = ['a']
        expect(helper.first_of_value(value)).to be 'a'
      end
    end

    context '!value.is_a?(Array)' do
    context 'with !value.is_a?(Array)' do
      it 'returns value' do
        value = 'a'
        expect(helper.first_of_value(value)).to be 'a'


@@ 38,14 38,14 @@ describe JsonLdHelper do
  end

  describe '#supported_context?' do
    context "!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)" do
    context 'when json is present and in an activitypub tagmanager context' do
      it 'returns true' do
        json = { '@context' => ActivityPub::TagManager::CONTEXT }.as_json
        expect(helper.supported_context?(json)).to be true
      end
    end

    context 'else' do
    context 'when not in activitypub tagmanager context' do
      it 'returns false' do
        json = nil
        expect(helper.supported_context?(json)).to be false


@@ 90,7 90,7 @@ describe JsonLdHelper do
    end
  end

  context 'compaction and forwarding' do
  context 'with compaction and forwarding' do
    let(:json) do
      {
        '@context' => [

M spec/helpers/routing_helper_spec.rb => spec/helpers/routing_helper_spec.rb +3 -3
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe RoutingHelper, type: :helper do
RSpec.describe RoutingHelper do
  describe '.full_asset_url' do
    around do |example|
      use_s3 = Rails.configuration.x.use_s3


@@ 24,7 24,7 @@ RSpec.describe RoutingHelper, type: :helper do
      end
    end

    context 'Do not use S3' do
    context 'when not using S3' do
      before do
        Rails.configuration.x.use_s3 = false
      end


@@ 32,7 32,7 @@ RSpec.describe RoutingHelper, type: :helper do
      it_behaves_like 'returns full path URL'
    end

    context 'Use S3' do
    context 'when using S3' do
      before do
        Rails.configuration.x.use_s3 = true
      end

M spec/lib/activitypub/activity/accept_spec.rb => spec/lib/activitypub/activity/accept_spec.rb +1 -1
@@ 43,7 43,7 @@ RSpec.describe ActivityPub::Activity::Accept do
    end
  end

  context 'given a relay' do
  context 'when given a relay' do
    subject { described_class.new(json, sender) }

    let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }

M spec/lib/activitypub/activity/announce_spec.rb => spec/lib/activitypub/activity/announce_spec.rb +6 -6
@@ 39,7 39,7 @@ RSpec.describe ActivityPub::Activity::Announce do
        subject.perform
      end

      context 'a known status' do
      context 'with known status' do
        let(:object_json) do
          ActivityPub::TagManager.instance.uri_for(status)
        end


@@ 49,7 49,7 @@ RSpec.describe ActivityPub::Activity::Announce do
        end
      end

      context 'an unknown status' do
      context 'with unknown status' do
        let(:object_json) { 'https://example.com/actor/hello-world' }

        it 'creates a reblog by sender of status' do


@@ 60,7 60,7 @@ RSpec.describe ActivityPub::Activity::Announce do
        end
      end

      context 'self-boost of a previously unknown status with correct attributedTo' do
      context 'when self-boost of a previously unknown status with correct attributedTo' do
        let(:object_json) do
          {
            id: 'https://example.com/actor#bar',


@@ 76,7 76,7 @@ RSpec.describe ActivityPub::Activity::Announce do
        end
      end

      context 'self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do
      context 'when self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do
        let(:object_json) do
          {
            id: 'https://example.com/actor#bar',


@@ 123,7 123,7 @@ RSpec.describe ActivityPub::Activity::Announce do
        stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
      end

      context 'and the relay is enabled' do
      context 'when the relay is enabled' do
        before do
          relay.update(state: :accepted)
          subject.perform


@@ 135,7 135,7 @@ RSpec.describe ActivityPub::Activity::Announce do
        end
      end

      context 'and the relay is disabled' do
      context 'when the relay is disabled' do
        before do
          subject.perform
        end

M spec/lib/activitypub/activity/create_spec.rb => spec/lib/activitypub/activity/create_spec.rb +19 -19
@@ 31,7 31,7 @@ RSpec.describe ActivityPub::Activity::Create do
        subject.perform
      end

      context 'object has been edited' do
      context 'when object has been edited' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 57,7 57,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'object has update date equal to creation date' do
      context 'when object has update date equal to creation date' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 83,7 83,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'unknown object type' do
      context 'with an unknown object type' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 97,7 97,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'standalone' do
      context 'with a standalone' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 121,7 121,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'public with explicit public address' do
      context 'when public with explicit public address' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 139,7 139,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'public with as:Public' do
      context 'when public with as:Public' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 157,7 157,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'public with Public' do
      context 'when public with Public' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 175,7 175,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'unlisted with explicit public address' do
      context 'when unlisted with explicit public address' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 193,7 193,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'unlisted with as:Public' do
      context 'when unlisted with as:Public' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 211,7 211,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'unlisted with Public' do
      context 'when unlisted with Public' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 229,7 229,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'private' do
      context 'when private' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 247,7 247,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'private with inlined Collection in audience' do
      context 'when private with inlined Collection in audience' do
        let(:object_json) do
          {
            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,


@@ 269,7 269,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'limited' do
      context 'when limited' do
        let(:recipient) { Fabricate(:account) }

        let(:object_json) do


@@ 294,7 294,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'limited when direct message assertion is false' do
      context 'when directMessage attribute is false' do
        let(:recipient) { Fabricate(:account) }

        let(:object_json) do


@@ 311,7 311,7 @@ RSpec.describe ActivityPub::Activity::Create do
          }
        end

        it 'creates status' do
        it 'creates status with limited visibility' do
          status = sender.statuses.first

          expect(status).to_not be_nil


@@ 319,7 319,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'direct' do
      context 'when direct' do
        let(:recipient) { Fabricate(:account) }

        let(:object_json) do


@@ 335,7 335,7 @@ RSpec.describe ActivityPub::Activity::Create do
          }
        end

        it 'creates status' do
        it 'creates status with direct visibility' do
          status = sender.statuses.first

          expect(status).to_not be_nil


@@ 343,7 343,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'direct when direct message assertion is true' do
      context 'when directMessage attribute is true' do
        let(:recipient) { Fabricate(:account) }

        let(:object_json) do


@@ 364,7 364,7 @@ RSpec.describe ActivityPub::Activity::Create do
        end
      end

      context 'as a reply' do
      context 'with a reply' do
        let(:original_status) { Fabricate(:status) }

        let(:object_json) do

M spec/lib/activitypub/activity/follow_spec.rb => spec/lib/activitypub/activity/follow_spec.rb +10 -10
@@ 20,7 20,7 @@ RSpec.describe ActivityPub::Activity::Follow do
    subject { described_class.new(json, sender) }

    context 'with no prior follow' do
      context 'unlocked account' do
      context 'with an unlocked account' do
        before do
          subject.perform
        end


@@ 35,7 35,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'silenced account following an unlocked account' do
      context 'when silenced account following an unlocked account' do
        before do
          sender.touch(:silenced_at)
          subject.perform


@@ 51,7 51,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'unlocked account muting the sender' do
      context 'with an unlocked account muting the sender' do
        before do
          recipient.mute!(sender)
          subject.perform


@@ 67,7 67,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'locked account' do
      context 'when locked account' do
        before do
          recipient.update(locked: true)
          subject.perform


@@ 89,7 89,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        sender.active_relationships.create!(target_account: recipient, uri: 'bar')
      end

      context 'unlocked account' do
      context 'with an unlocked account' do
        before do
          subject.perform
        end


@@ 103,7 103,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'silenced account following an unlocked account' do
      context 'when silenced account following an unlocked account' do
        before do
          sender.touch(:silenced_at)
          subject.perform


@@ 118,7 118,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'unlocked account muting the sender' do
      context 'with an unlocked account muting the sender' do
        before do
          recipient.mute!(sender)
          subject.perform


@@ 133,7 133,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'locked account' do
      context 'when locked account' do
        before do
          recipient.update(locked: true)
          subject.perform


@@ 154,7 154,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        sender.follow_requests.create!(target_account: recipient, uri: 'bar')
      end

      context 'silenced account following an unlocked account' do
      context 'when silenced account following an unlocked account' do
        before do
          sender.touch(:silenced_at)
          subject.perform


@@ 170,7 170,7 @@ RSpec.describe ActivityPub::Activity::Follow do
        end
      end

      context 'locked account' do
      context 'when locked account' do
        before do
          recipient.update(locked: true)
          subject.perform

M spec/lib/activitypub/activity/reject_spec.rb => spec/lib/activitypub/activity/reject_spec.rb +7 -7
@@ 27,7 27,7 @@ RSpec.describe ActivityPub::Activity::Reject do
  describe '#perform' do
    subject { described_class.new(json, sender) }

    context 'rejecting a pending follow request by target' do
    context 'when rejecting a pending follow request by target' do
      before do
        Fabricate(:follow_request, account: recipient, target_account: sender)
        subject.perform


@@ 42,7 42,7 @@ RSpec.describe ActivityPub::Activity::Reject do
      end
    end

    context 'rejecting a pending follow request by uri' do
    context 'when rejecting a pending follow request by uri' do
      before do
        Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
        subject.perform


@@ 57,7 57,7 @@ RSpec.describe ActivityPub::Activity::Reject do
      end
    end

    context 'rejecting a pending follow request by uri only' do
    context 'when rejecting a pending follow request by uri only' do
      let(:object_json) { 'bar' }

      before do


@@ 74,7 74,7 @@ RSpec.describe ActivityPub::Activity::Reject do
      end
    end

    context 'rejecting an existing follow relationship by target' do
    context 'when rejecting an existing follow relationship by target' do
      before do
        Fabricate(:follow, account: recipient, target_account: sender)
        subject.perform


@@ 89,7 89,7 @@ RSpec.describe ActivityPub::Activity::Reject do
      end
    end

    context 'rejecting an existing follow relationship by uri' do
    context 'when rejecting an existing follow relationship by uri' do
      before do
        Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
        subject.perform


@@ 104,7 104,7 @@ RSpec.describe ActivityPub::Activity::Reject do
      end
    end

    context 'rejecting an existing follow relationship by uri only' do
    context 'when rejecting an existing follow relationship by uri only' do
      let(:object_json) { 'bar' }

      before do


@@ 122,7 122,7 @@ RSpec.describe ActivityPub::Activity::Reject do
    end
  end

  context 'given a relay' do
  context 'when given a relay' do
    subject { described_class.new(json, sender) }

    let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }

M spec/lib/activitypub/adapter_spec.rb => spec/lib/activitypub/adapter_spec.rb +34 -26
@@ 3,43 3,51 @@
require 'rails_helper'

RSpec.describe ActivityPub::Adapter do
  class TestObject < ActiveModelSerializers::Model
    attributes :foo
  end
  before do
    test_object_class = Class.new(ActiveModelSerializers::Model) do
      attributes :foo
    end
    stub_const('TestObject', test_object_class)

  class TestWithBasicContextSerializer < ActivityPub::Serializer
    attributes :foo
  end
    test_with_basic_context_serializer = Class.new(ActivityPub::Serializer) do
      attributes :foo
    end
    stub_const('TestWithBasicContextSerializer', test_with_basic_context_serializer)

  class TestWithNamedContextSerializer < ActivityPub::Serializer
    context :security
    attributes :foo
  end
    test_with_named_context_serializer = Class.new(ActivityPub::Serializer) do
      context :security
      attributes :foo
    end
    stub_const('TestWithNamedContextSerializer', test_with_named_context_serializer)

  class TestWithNestedNamedContextSerializer < ActivityPub::Serializer
    attributes :foo
    test_with_nested_named_context_serializer = Class.new(ActivityPub::Serializer) do
      attributes :foo

    has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer
      has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer

    def virtual_object
      object
      def virtual_object
        object
      end
    end
  end
    stub_const('TestWithNestedNamedContextSerializer', test_with_nested_named_context_serializer)

  class TestWithContextExtensionSerializer < ActivityPub::Serializer
    context_extensions :sensitive
    attributes :foo
  end
    test_with_context_extension_serializer = Class.new(ActivityPub::Serializer) do
      context_extensions :sensitive
      attributes :foo
    end
    stub_const('TestWithContextExtensionSerializer', test_with_context_extension_serializer)

  class TestWithNestedContextExtensionSerializer < ActivityPub::Serializer
    context_extensions :manually_approves_followers
    attributes :foo
    test_with_nested_context_extension_serializer = Class.new(ActivityPub::Serializer) do
      context_extensions :manually_approves_followers
      attributes :foo

    has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer
      has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer

    def virtual_object
      object
      def virtual_object
        object
      end
    end
    stub_const('TestWithNestedContextExtensionSerializer', test_with_nested_context_extension_serializer)
  end

  describe '#serializable_hash' do

M spec/lib/connection_pool/shared_connection_pool_spec.rb => spec/lib/connection_pool/shared_connection_pool_spec.rb +9 -7
@@ 3,22 3,24 @@
require 'rails_helper'

describe ConnectionPool::SharedConnectionPool do
  class MiniConnection
    attr_reader :site
  subject { described_class.new(size: 5, timeout: 5) { |site| mini_connection_class.new(site) } }

    def initialize(site)
      @site = site
  let(:mini_connection_class) do
    Class.new do
      attr_reader :site

      def initialize(site)
        @site = site
      end
    end
  end

  subject { described_class.new(size: 5, timeout: 5) { |site| MiniConnection.new(site) } }

  describe '#with' do
    it 'runs a block with a connection' do
      block_run = false

      subject.with('foo') do |connection|
        expect(connection).to be_a MiniConnection
        expect(connection).to be_a mini_connection_class
        block_run = true
      end


M spec/lib/connection_pool/shared_timed_stack_spec.rb => spec/lib/connection_pool/shared_timed_stack_spec.rb +15 -13
@@ 3,30 3,32 @@
require 'rails_helper'

describe ConnectionPool::SharedTimedStack do
  class MiniConnection
    attr_reader :site
  subject { described_class.new(5) { |site| mini_connection_class.new(site) } }

    def initialize(site)
      @site = site
  let(:mini_connection_class) do
    Class.new do
      attr_reader :site

      def initialize(site)
        @site = site
      end
    end
  end

  subject { described_class.new(5) { |site| MiniConnection.new(site) } }

  describe '#push' do
    it 'keeps the connection in the stack' do
      subject.push(MiniConnection.new('foo'))
      subject.push(mini_connection_class.new('foo'))
      expect(subject.size).to eq 1
    end
  end

  describe '#pop' do
    it 'returns a connection' do
      expect(subject.pop('foo')).to be_a MiniConnection
      expect(subject.pop('foo')).to be_a mini_connection_class
    end

    it 'returns the same connection that was pushed in' do
      connection = MiniConnection.new('foo')
      connection = mini_connection_class.new('foo')
      subject.push(connection)
      expect(subject.pop('foo')).to be connection
    end


@@ 36,8 38,8 @@ describe ConnectionPool::SharedTimedStack do
    end

    it 'repurposes a connection for a different site when maximum amount is reached' do
      5.times { subject.push(MiniConnection.new('foo')) }
      expect(subject.pop('bar')).to be_a MiniConnection
      5.times { subject.push(mini_connection_class.new('foo')) }
      expect(subject.pop('bar')).to be_a mini_connection_class
    end
  end



@@ 47,14 49,14 @@ describe ConnectionPool::SharedTimedStack do
    end

    it 'returns false when there are connections on the stack' do
      subject.push(MiniConnection.new('foo'))
      subject.push(mini_connection_class.new('foo'))
      expect(subject.empty?).to be false
    end
  end

  describe '#size' do
    it 'returns the number of connections on the stack' do
      2.times { subject.push(MiniConnection.new('foo')) }
      2.times { subject.push(mini_connection_class.new('foo')) }
      expect(subject.size).to eq 2
    end
  end

M spec/lib/emoji_formatter_spec.rb => spec/lib/emoji_formatter_spec.rb +5 -5
@@ 14,7 14,7 @@ RSpec.describe EmojiFormatter do

    let(:emojis) { [emoji] }

    context 'given text that is not marked as html-safe' do
    context 'when given text that is not marked as html-safe' do
      let(:text) { 'Foo' }

      it 'raises an argument error' do


@@ 22,7 22,7 @@ RSpec.describe EmojiFormatter do
      end
    end

    context 'given text with an emoji shortcode at the start' do
    context 'when given text with an emoji shortcode at the start' do
      let(:text) { preformat_text(':coolcat: Beep boop') }

      it 'converts the shortcode to an image tag' do


@@ 30,7 30,7 @@ RSpec.describe EmojiFormatter do
      end
    end

    context 'given text with an emoji shortcode in the middle' do
    context 'when given text with an emoji shortcode in the middle' do
      let(:text) { preformat_text('Beep :coolcat: boop') }

      it 'converts the shortcode to an image tag' do


@@ 38,7 38,7 @@ RSpec.describe EmojiFormatter do
      end
    end

    context 'given text with concatenated emoji shortcodes' do
    context 'when given text with concatenated emoji shortcodes' do
      let(:text) { preformat_text(':coolcat::coolcat:') }

      it 'does not touch the shortcodes' do


@@ 46,7 46,7 @@ RSpec.describe EmojiFormatter do
      end
    end

    context 'given text with an emoji shortcode at the end' do
    context 'when given text with an emoji shortcode at the end' do
      let(:text) { preformat_text('Beep boop :coolcat:') }

      it 'converts the shortcode to an image tag' do

M spec/lib/entity_cache_spec.rb => spec/lib/entity_cache_spec.rb +1 -1
@@ 9,7 9,7 @@ RSpec.describe EntityCache do
  describe '#emoji' do
    subject { EntityCache.instance.emoji(shortcodes, domain) }

    context 'called with an empty list of shortcodes' do
    context 'when called with an empty list of shortcodes' do
      let(:shortcodes) { [] }
      let(:domain)     { 'example.org' }


M spec/lib/feed_manager_spec.rb => spec/lib/feed_manager_spec.rb +8 -8
@@ 27,7 27,7 @@ RSpec.describe FeedManager do
    let(:bob)   { Fabricate(:account, username: 'bob', domain: 'example.com') }
    let(:jeff)  { Fabricate(:account, username: 'jeff') }

    context 'for home feed' do
    context 'with home feed' do
      it 'returns false for followee\'s status' do
        status = Fabricate(:status, text: 'Hello world', account: alice)
        bob.follow!(alice)


@@ 162,7 162,7 @@ RSpec.describe FeedManager do
      end
    end

    context 'for mentions feed' do
    context 'with mentions feed' do
      it 'returns true for status that mentions blocked account' do
        bob.block!(jeff)
        status = PostStatusService.new.call(alice, text: 'Hey @jeff')


@@ 195,7 195,7 @@ RSpec.describe FeedManager do
    it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do
      account = Fabricate(:account)
      status = Fabricate(:status)
      members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] }
      members = Array.new(FeedManager::MAX_ITEMS) { |count| [count, count] }
      redis.zadd("feed:home:#{account.id}", members)

      FeedManager.instance.push_to_home(account, status)


@@ 203,7 203,7 @@ RSpec.describe FeedManager do
      expect(redis.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS
    end

    context 'reblogs' do
    context 'with reblogs' do
      it 'saves reblogs of unseen statuses' do
        account = Fabricate(:account)
        reblogged = Fabricate(:status)


@@ 240,7 240,7 @@ RSpec.describe FeedManager do
      it 'does not save a new reblog of a recently-reblogged status' do
        account = Fabricate(:account)
        reblogged = Fabricate(:status)
        reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) }
        reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }

        # The first reblog will be accepted
        FeedManager.instance.push_to_home(account, reblogs.first)


@@ 269,7 269,7 @@ RSpec.describe FeedManager do
      it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
        account   = Fabricate(:account)
        reblogged = Fabricate(:status)
        reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) }
        reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }

        # Accept the reblogs
        FeedManager.instance.push_to_home(account, reblogs[0])


@@ 285,7 285,7 @@ RSpec.describe FeedManager do
      it 'saves a new reblog of a long-ago-reblogged status' do
        account = Fabricate(:account)
        reblogged = Fabricate(:status)
        reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) }
        reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }

        # The first reblog will be accepted
        FeedManager.instance.push_to_home(account, reblogs.first)


@@ 466,7 466,7 @@ RSpec.describe FeedManager do

    it 'leaves a multiply-reblogged status if another reblog was in feed' do
      reblogged = Fabricate(:status)
      reblogs   = 3.times.map { Fabricate(:status, reblog: reblogged) }
      reblogs   = Array.new(3) { Fabricate(:status, reblog: reblogged) }

      reblogs.each do |reblog|
        FeedManager.instance.push_to_home(receiver, reblog)

M spec/lib/html_aware_formatter_spec.rb => spec/lib/html_aware_formatter_spec.rb +3 -3
@@ 18,7 18,7 @@ RSpec.describe HtmlAwareFormatter do
    context 'when remote' do
      let(:local) { false }

      context 'given plain text' do
      context 'when given plain text' do
        let(:text) { 'Beep boop' }

        it 'keeps the plain text' do


@@ 26,7 26,7 @@ RSpec.describe HtmlAwareFormatter do
        end
      end

      context 'given text containing script tags' do
      context 'when given text containing script tags' do
        let(:text) { '<script>alert("Hello")</script>' }

        it 'strips the scripts' do


@@ 34,7 34,7 @@ RSpec.describe HtmlAwareFormatter do
        end
      end

      context 'given text containing malicious classes' do
      context 'when given text containing malicious classes' do
        let(:text) { '<span class="mention  status__content__spoiler-link">Show more</span>' }

        it 'strips the malicious classes' do

M spec/lib/link_details_extractor_spec.rb => spec/lib/link_details_extractor_spec.rb +2 -2
@@ 40,7 40,7 @@ RSpec.describe LinkDetailsExtractor do
  context 'when structured data is present' do
    let(:original_url) { 'https://example.com/page.html' }

    context 'and is wrapped in CDATA tags' do
    context 'when is wrapped in CDATA tags' do
      let(:html) { <<~HTML }
        <!doctype html>
        <html>


@@ 79,7 79,7 @@ RSpec.describe LinkDetailsExtractor do
      end
    end

    context 'but the first tag is invalid JSON' do
    context 'with the first tag is invalid JSON' do
      let(:html) { <<~HTML }
        <!doctype html>
        <html>

A spec/lib/mastodon/settings_cli_spec.rb => spec/lib/mastodon/settings_cli_spec.rb +64 -0
@@ 0,0 1,64 @@
# frozen_string_literal: true

require 'rails_helper'
require 'mastodon/settings_cli'

RSpec.describe Mastodon::SettingsCLI do
  describe 'subcommand "registrations"' do
    let(:cli) { Mastodon::RegistrationsCLI.new }

    before do
      Setting.registrations_mode = nil
    end

    describe '#open' do
      it 'changes "registrations_mode" to "open"' do
        expect { cli.open }.to change(Setting, :registrations_mode).from(nil).to('open')
      end

      it 'displays success message' do
        expect { cli.open }.to output(
          a_string_including('OK')
        ).to_stdout
      end
    end

    describe '#approved' do
      it 'changes "registrations_mode" to "approved"' do
        expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved')
      end

      it 'displays success message' do
        expect { cli.approved }.to output(
          a_string_including('OK')
        ).to_stdout
      end

      context 'with --require-reason' do
        before do
          cli.options = { require_reason: true }
        end

        it 'changes "registrations_mode" to "approved"' do
          expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved')
        end

        it 'sets "require_invite_text" to "true"' do
          expect { cli.approved }.to change(Setting, :require_invite_text).from(false).to(true)
        end
      end
    end

    describe '#close' do
      it 'changes "registrations_mode" to "none"' do
        expect { cli.close }.to change(Setting, :registrations_mode).from(nil).to('none')
      end

      it 'displays success message' do
        expect { cli.close }.to output(
          a_string_including('OK')
        ).to_stdout
      end
    end
  end
end

M spec/lib/ostatus/tag_manager_spec.rb => spec/lib/ostatus/tag_manager_spec.rb +3 -3
@@ 40,7 40,7 @@ describe OStatus::TagManager do
  describe '#uri_for' do
    subject { OStatus::TagManager.instance.uri_for(target) }

    context 'comment object' do
    context 'with comment object' do
      let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }

      it 'returns the unique tag for status' do


@@ 49,7 49,7 @@ describe OStatus::TagManager do
      end
    end

    context 'note object' do
    context 'with note object' do
      let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) }

      it 'returns the unique tag for status' do


@@ 58,7 58,7 @@ describe OStatus::TagManager do
      end
    end

    context 'person object' do
    context 'when person object' do
      let(:target) { Fabricate(:account, username: 'alice') }

      it 'returns the URL for account' do

M spec/lib/request_pool_spec.rb => spec/lib/request_pool_spec.rb +1 -1
@@ 33,7 33,7 @@ describe RequestPool do

      subject

      threads = 20.times.map do |_i|
      threads = Array.new(20) do |_i|
        Thread.new do
          20.times do
            subject.with('http://example.com') do |http_client|

M spec/lib/scope_transformer_spec.rb => spec/lib/scope_transformer_spec.rb +11 -11
@@ 20,67 20,67 @@ describe ScopeTransformer do
      end
    end

    context 'for scope "read"' do
    context 'with scope "read"' do
      let(:input) { 'read' }

      it_behaves_like 'a scope', nil, 'all', 'read'
    end

    context 'for scope "write"' do
    context 'with scope "write"' do
      let(:input) { 'write' }

      it_behaves_like 'a scope', nil, 'all', 'write'
    end

    context 'for scope "follow"' do
    context 'with scope "follow"' do
      let(:input) { 'follow' }

      it_behaves_like 'a scope', nil, 'follow', 'read/write'
    end

    context 'for scope "crypto"' do
    context 'with scope "crypto"' do
      let(:input) { 'crypto' }

      it_behaves_like 'a scope', nil, 'crypto', 'read/write'
    end

    context 'for scope "push"' do
    context 'with scope "push"' do
      let(:input) { 'push' }

      it_behaves_like 'a scope', nil, 'push', 'read/write'
    end

    context 'for scope "admin:read"' do
    context 'with scope "admin:read"' do
      let(:input) { 'admin:read' }

      it_behaves_like 'a scope', 'admin', 'all', 'read'
    end

    context 'for scope "admin:write"' do
    context 'with scope "admin:write"' do
      let(:input) { 'admin:write' }

      it_behaves_like 'a scope', 'admin', 'all', 'write'
    end

    context 'for scope "admin:read:accounts"' do
    context 'with scope "admin:read:accounts"' do
      let(:input) { 'admin:read:accounts' }

      it_behaves_like 'a scope', 'admin', 'accounts', 'read'
    end

    context 'for scope "admin:write:accounts"' do
    context 'with scope "admin:write:accounts"' do
      let(:input) { 'admin:write:accounts' }

      it_behaves_like 'a scope', 'admin', 'accounts', 'write'
    end

    context 'for scope "read:accounts"' do
    context 'with scope "read:accounts"' do
      let(:input) { 'read:accounts' }

      it_behaves_like 'a scope', nil, 'accounts', 'read'
    end

    context 'for scope "write:accounts"' do
    context 'with scope "write:accounts"' do
      let(:input) { 'write:accounts' }

      it_behaves_like 'a scope', nil, 'accounts', 'write'

M spec/lib/status_cache_hydrator_spec.rb => spec/lib/status_cache_hydrator_spec.rb +7 -7
@@ 44,7 44,7 @@ describe StatusCacheHydrator do
        let(:reblog) { Fabricate(:status) }
        let(:status) { Fabricate(:status, reblog: reblog) }

        context 'that has been favourited' do
        context 'when it has been favourited' do
          before do
            FavouriteService.new.call(account, reblog)
          end


@@ 54,7 54,7 @@ describe StatusCacheHydrator do
          end
        end

        context 'that has been reblogged' do
        context 'when it has been reblogged' do
          before do
            ReblogService.new.call(account, reblog)
          end


@@ 64,7 64,7 @@ describe StatusCacheHydrator do
          end
        end

        context 'that has been pinned' do
        context 'when it has been pinned' do
          let(:reblog) { Fabricate(:status, account: account) }

          before do


@@ 76,7 76,7 @@ describe StatusCacheHydrator do
          end
        end

        context 'that has been followed tags' do
        context 'when it has been followed tags' do
          let(:followed_tag) { Fabricate(:tag) }

          before do


@@ 90,7 90,7 @@ describe StatusCacheHydrator do
          end
        end

        context 'that has a poll authored by the user' do
        context 'when it has a poll authored by the user' do
          let(:poll) { Fabricate(:poll, account: account) }
          let(:reblog) { Fabricate(:status, poll: poll, account: account) }



@@ 99,7 99,7 @@ describe StatusCacheHydrator do
          end
        end

        context 'that has been voted in' do
        context 'when it has been voted in' do
          let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
          let(:reblog) { Fabricate(:status, poll: poll) }



@@ 112,7 112,7 @@ describe StatusCacheHydrator do
          end
        end

        context 'that matches account filters' do
        context 'when it matches account filters' do
          let(:reblog) { Fabricate(:status, text: 'this toot is about that banned word') }

          before do

M spec/lib/status_reach_finder_spec.rb => spec/lib/status_reach_finder_spec.rb +1 -1
@@ 4,7 4,7 @@ require 'rails_helper'

describe StatusReachFinder do
  describe '#inboxes' do
    context 'for a local status' do
    context 'with a local status' do
      subject { described_class.new(status) }

      let(:parent_status) { nil }

M spec/lib/text_formatter_spec.rb => spec/lib/text_formatter_spec.rb +33 -33
@@ 8,7 8,7 @@ RSpec.describe TextFormatter do

    let(:preloaded_accounts) { nil }

    context 'given text containing plain text' do
    context 'when given text containing plain text' do
      let(:text) { 'text' }

      it 'paragraphizes the text' do


@@ 16,7 16,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing line feeds' do
    context 'when given text containing line feeds' do
      let(:text) { "line\nfeed" }

      it 'removes line feeds' do


@@ 24,7 24,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing linkable mentions' do
    context 'when given text containing linkable mentions' do
      let(:preloaded_accounts) { [Fabricate(:account, username: 'alice')] }
      let(:text) { '@alice' }



@@ 33,7 33,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing unlinkable mentions' do
    context 'when given text containing unlinkable mentions' do
      let(:preloaded_accounts) { [] }
      let(:text) { '@alice' }



@@ 42,7 42,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a stand-alone medium URL' do
    context 'when given a stand-alone medium URL' do
      let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' }

      it 'matches the full URL' do


@@ 50,7 50,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a stand-alone google URL' do
    context 'when given a stand-alone google URL' do
      let(:text) { 'http://google.com' }

      it 'matches the full URL' do


@@ 58,7 58,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a stand-alone URL with a newer TLD' do
    context 'when given a stand-alone URL with a newer TLD' do
      let(:text) { 'http://example.gay' }

      it 'matches the full URL' do


@@ 66,7 66,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a stand-alone IDN URL' do
    context 'when given a stand-alone IDN URL' do
      let(:text) { 'https://nic.みんな/' }

      it 'matches the full URL' do


@@ 78,7 78,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with a trailing period' do
    context 'when given a URL with a trailing period' do
      let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' }

      it 'matches the full URL but not the period' do


@@ 86,7 86,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL enclosed with parentheses' do
    context 'when given a URL enclosed with parentheses' do
      let(:text) { '(http://google.com/)' }

      it 'matches the full URL but not the parentheses' do


@@ 94,7 94,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with a trailing exclamation point' do
    context 'when given a URL with a trailing exclamation point' do
      let(:text) { 'http://www.google.com!' }

      it 'matches the full URL but not the exclamation point' do


@@ 102,7 102,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with a trailing single quote' do
    context 'when given a URL with a trailing single quote' do
      let(:text) { "http://www.google.com'" }

      it 'matches the full URL but not the single quote' do


@@ 110,7 110,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with a trailing angle bracket' do
    context 'when given a URL with a trailing angle bracket' do
      let(:text) { 'http://www.google.com>' }

      it 'matches the full URL but not the angle bracket' do


@@ 118,7 118,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with a query string' do
    context 'when given a URL with a query string' do
      context 'with escaped unicode character' do
        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }



@@ 152,7 152,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with parentheses in it' do
    context 'when given a URL with parentheses in it' do
      let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }

      it 'matches the full URL' do


@@ 160,7 160,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL in quotation marks' do
    context 'when given a URL in quotation marks' do
      let(:text) { '"https://example.com/"' }

      it 'does not match the quotation marks' do


@@ 168,7 168,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL in angle brackets' do
    context 'when given a URL in angle brackets' do
      let(:text) { '<https://example.com/>' }

      it 'does not match the angle brackets' do


@@ 176,7 176,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with Japanese path string' do
    context 'when given a URL with Japanese path string' do
      let(:text) { 'https://ja.wikipedia.org/wiki/日本' }

      it 'matches the full URL' do


@@ 184,7 184,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with Korean path string' do
    context 'when given a URL with Korean path string' do
      let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' }

      it 'matches the full URL' do


@@ 192,7 192,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with a full-width space' do
    context 'when given a URL with a full-width space' do
      let(:text) { 'https://example.com/ abc123' }

      it 'does not match the full-width space' do


@@ 200,7 200,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL in Japanese quotation marks' do
    context 'when given a URL in Japanese quotation marks' do
      let(:text) { '「[https://example.org/」' }

      it 'does not match the quotation marks' do


@@ 208,7 208,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with Simplified Chinese path string' do
    context 'when given a URL with Simplified Chinese path string' do
      let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }

      it 'matches the full URL' do


@@ 216,7 216,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL with Traditional Chinese path string' do
    context 'when given a URL with Traditional Chinese path string' do
      let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' }

      it 'matches the full URL' do


@@ 224,7 224,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL containing unsafe code (XSS attack, visible part)' do
    context 'when given a URL containing unsafe code (XSS attack, visible part)' do
      let(:text) { 'http://example.com/b<del>b</del>' }

      it 'does not include the HTML in the URL' do


@@ 236,7 236,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given a URL containing unsafe code (XSS attack, invisible part)' do
    context 'when given a URL containing unsafe code (XSS attack, invisible part)' do
      let(:text) { 'http://example.com/blahblahblahblah/a<script>alert("Hello")</script>' }

      it 'does not include the HTML in the URL' do


@@ 248,7 248,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing HTML code (script tag)' do
    context 'when given text containing HTML code (script tag)' do
      let(:text) { '<script>alert("Hello")</script>' }

      it 'escapes the HTML' do


@@ 256,7 256,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing HTML (XSS attack)' do
    context 'when given text containing HTML (XSS attack)' do
      let(:text) { %q{<img src="javascript:alert('XSS');">} }

      it 'escapes the HTML' do


@@ 264,7 264,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given an invalid URL' do
    context 'when given an invalid URL' do
      let(:text) { 'http://www\.google\.com' }

      it 'outputs the raw URL' do


@@ 272,7 272,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing a hashtag' do
    context 'when given text containing a hashtag' do
      let(:text)  { '#hashtag' }

      it 'creates a hashtag link' do


@@ 280,7 280,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing a hashtag with Unicode chars' do
    context 'when given text containing a hashtag with Unicode chars' do
      let(:text)  { '#hashtagタグ' }

      it 'creates a hashtag link' do


@@ 288,7 288,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text with a stand-alone xmpp: URI' do
    context 'when given text with a stand-alone xmpp: URI' do
      let(:text) { 'xmpp:user@instance.com' }

      it 'matches the full URI' do


@@ 296,7 296,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text with an xmpp: URI with a query-string' do
    context 'when given text with an xmpp: URI with a query-string' do
      let(:text) { 'please join xmpp:muc@instance.com?join right now' }

      it 'matches the full URI' do


@@ 304,7 304,7 @@ RSpec.describe TextFormatter do
      end
    end

    context 'given text containing a magnet: URI' do
    context 'when given text containing a magnet: URI' do
      let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' }

      it 'matches the full URI' do

A spec/lib/vacuum/imports_vacuum_spec.rb => spec/lib/vacuum/imports_vacuum_spec.rb +19 -0
@@ 0,0 1,19 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Vacuum::ImportsVacuum do
  subject { described_class.new }

  let!(:old_unconfirmed) { Fabricate(:bulk_import, state: :unconfirmed, created_at: 2.days.ago) }
  let!(:new_unconfirmed) { Fabricate(:bulk_import, state: :unconfirmed, created_at: 10.seconds.ago) }
  let!(:recent_ongoing)  { Fabricate(:bulk_import, state: :in_progress, created_at: 20.minutes.ago) }
  let!(:recent_finished) { Fabricate(:bulk_import, state: :finished, created_at: 1.day.ago) }
  let!(:old_finished)    { Fabricate(:bulk_import, state: :finished, created_at: 2.months.ago) }

  describe '#perform' do
    it 'cleans up the expected imports' do
      expect { subject.perform }.to change { BulkImport.all.pluck(:id) }.from([old_unconfirmed, new_unconfirmed, recent_ongoing, recent_finished, old_finished].map(&:id)).to([new_unconfirmed, recent_ongoing, recent_finished].map(&:id))
    end
  end
end

M spec/mailers/admin_mailer_spec.rb => spec/mailers/admin_mailer_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AdminMailer, type: :mailer do
RSpec.describe AdminMailer do
  describe '.new_report' do
    let(:sender)    { Fabricate(:account, username: 'John') }
    let(:recipient) { Fabricate(:account, username: 'Mike') }

M spec/mailers/notification_mailer_spec.rb => spec/mailers/notification_mailer_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe NotificationMailer, type: :mailer do
RSpec.describe NotificationMailer do
  let(:receiver)       { Fabricate(:user) }
  let(:sender)         { Fabricate(:account, username: 'bob') }
  let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') }

M spec/mailers/user_mailer_spec.rb => spec/mailers/user_mailer_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

describe UserMailer, type: :mailer do
describe UserMailer do
  let(:receiver) { Fabricate(:user) }

  shared_examples 'localized subject' do |*args, **kwrest|

M spec/models/account/field_spec.rb => spec/models/account/field_spec.rb +16 -16
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Account::Field, type: :model do
RSpec.describe Account::Field do
  describe '#verified?' do
    subject { described_class.new(account, 'name' => 'Foo', 'value' => 'Bar', 'verified_at' => verified_at) }



@@ 49,10 49,10 @@ RSpec.describe Account::Field, type: :model do

    let(:account) { double('Account', local?: local) }

    context 'for local accounts' do
    context 'with local accounts' do
      let(:local) { true }

      context 'for a URL with misleading authentication' do
      context 'with a URL with misleading authentication' do
        let(:value) { 'https://spacex.com                                                                                            @h.43z.one' }

        it 'returns false' do


@@ 60,7 60,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for a URL' do
      context 'with a URL' do
        let(:value) { 'https://example.com' }

        it 'returns true' do


@@ 68,7 68,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for an IDN URL' do
      context 'with an IDN URL' do
        let(:value) { 'https://twitter.com∕dougallj∕status∕1590357240443437057.ê.cc/twitter.html' }

        it 'returns false' do


@@ 76,7 76,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for a URL with a non-normalized path' do
      context 'with a URL with a non-normalized path' do
        let(:value) { 'https://github.com/octocatxxxxxxxx/../mastodon' }

        it 'returns false' do


@@ 84,7 84,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for text that is not a URL' do
      context 'with text that is not a URL' do
        let(:value) { 'Hello world' }

        it 'returns false' do


@@ 92,7 92,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for text that contains a URL' do
      context 'with text that contains a URL' do
        let(:value) { 'Hello https://example.com world' }

        it 'returns false' do


@@ 100,7 100,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for text which is blank' do
      context 'with text which is blank' do
        let(:value) { '' }

        it 'returns false' do


@@ 109,10 109,10 @@ RSpec.describe Account::Field, type: :model do
      end
    end

    context 'for remote accounts' do
    context 'with remote accounts' do
      let(:local) { false }

      context 'for a link' do
      context 'with a link' do
        let(:value) { '<a href="https://www.patreon.com/mastodon" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://www.</span><span class="">patreon.com/mastodon</span><span class="invisible"></span></a>' }

        it 'returns true' do


@@ 120,7 120,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for a link with misleading authentication' do
      context 'with a link with misleading authentication' do
        let(:value) { '<a href="https://google.com                                                                                            @h.43z.one" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://</span><span class="">google.com</span><span class="invisible">                                                                                            @h.43z.one</span></a>' }

        it 'returns false' do


@@ 128,7 128,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for HTML that has more than just a link' do
      context 'with HTML that has more than just a link' do
        let(:value) { '<a href="https://google.com" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://</span><span class="">google.com</span><span class="invisible"></span></a>                                                                                            @h.43z.one' }

        it 'returns false' do


@@ 136,7 136,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for a link with different visible text' do
      context 'with a link with different visible text' do
        let(:value) { '<a href="https://google.com/bar">https://example.com/foo</a>' }

        it 'returns false' do


@@ 144,7 144,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for text that is a URL but is not linked' do
      context 'with text that is a URL but is not linked' do
        let(:value) { 'https://example.com/foo' }

        it 'returns false' do


@@ 152,7 152,7 @@ RSpec.describe Account::Field, type: :model do
        end
      end

      context 'for text which is blank' do
      context 'with text which is blank' do
        let(:value) { '' }

        it 'returns false' do

M spec/models/account_alias_spec.rb => spec/models/account_alias_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe AccountAlias, type: :model do
RSpec.describe AccountAlias do
end

M spec/models/account_conversation_spec.rb => spec/models/account_conversation_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AccountConversation, type: :model do
RSpec.describe AccountConversation do
  let!(:alice) { Fabricate(:account, username: 'alice') }
  let!(:bob)   { Fabricate(:account, username: 'bob') }
  let!(:mark)  { Fabricate(:account, username: 'mark') }

M spec/models/account_deletion_request_spec.rb => spec/models/account_deletion_request_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe AccountDeletionRequest, type: :model do
RSpec.describe AccountDeletionRequest do
end

M spec/models/account_domain_block_spec.rb => spec/models/account_domain_block_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AccountDomainBlock, type: :model do
RSpec.describe AccountDomainBlock do
  it 'removes blocking cache after creation' do
    account = Fabricate(:account)
    Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked')

M spec/models/account_migration_spec.rb => spec/models/account_migration_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AccountMigration, type: :model do
RSpec.describe AccountMigration do
  describe 'validations' do
    let(:source_account) { Fabricate(:account) }
    let(:target_acct)    { target_account.acct }

M spec/models/account_moderation_note_spec.rb => spec/models/account_moderation_note_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe AccountModerationNote, type: :model do
RSpec.describe AccountModerationNote do
end

M spec/models/account_spec.rb => spec/models/account_spec.rb +7 -7
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Account, type: :model do
RSpec.describe Account do
  context do
    subject { Fabricate(:account) }



@@ 171,7 171,7 @@ RSpec.describe Account, type: :model do
  describe '#possibly_stale?' do
    let(:account) { Fabricate(:account, last_webfingered_at: last_webfingered_at) }

    context 'last_webfingered_at is nil' do
    context 'when last_webfingered_at is nil' do
      let(:last_webfingered_at) { nil }

      it 'returns true' do


@@ 179,7 179,7 @@ RSpec.describe Account, type: :model do
      end
    end

    context 'last_webfingered_at is more than 24 hours before' do
    context 'when last_webfingered_at is more than 24 hours before' do
      let(:last_webfingered_at) { 25.hours.ago }

      it 'returns true' do


@@ 187,7 187,7 @@ RSpec.describe Account, type: :model do
      end
    end

    context 'last_webfingered_at is less than 24 hours before' do
    context 'when last_webfingered_at is less than 24 hours before' do
      let(:last_webfingered_at) { 23.hours.ago }

      it 'returns false' do


@@ 200,7 200,7 @@ RSpec.describe Account, type: :model do
    let(:account) { Fabricate(:account, domain: domain) }
    let(:acct)    { account.acct }

    context 'domain is nil' do
    context 'when domain is nil' do
      let(:domain) { nil }

      it 'returns nil' do


@@ 213,7 213,7 @@ RSpec.describe Account, type: :model do
      end
    end

    context 'domain is present' do
    context 'when domain is present' do
      let(:domain) { 'example.com' }

      it 'calls ResolveAccountService#call' do


@@ 902,7 902,7 @@ RSpec.describe Account, type: :model do

    describe 'recent' do
      it 'returns a relation of accounts sorted by recent creation' do
        matches = 2.times.map { Fabricate(:account) }
        matches = Array.new(2) { Fabricate(:account) }
        expect(Account.where('id > 0').recent).to match_array(matches)
      end
    end

M spec/models/account_statuses_cleanup_policy_spec.rb => spec/models/account_statuses_cleanup_policy_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe AccountStatusesCleanupPolicy, type: :model do
RSpec.describe AccountStatusesCleanupPolicy do
  let(:account) { Fabricate(:account, username: 'alice', domain: nil) }

  describe 'validation' do

M spec/models/admin/account_action_spec.rb => spec/models/admin/account_action_spec.rb +10 -10
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::AccountAction, type: :model do
RSpec.describe Admin::AccountAction do
  let(:account_action) { described_class.new }

  describe '#save!' do


@@ 20,7 20,7 @@ RSpec.describe Admin::AccountAction, type: :model do
      )
    end

    context 'type is "disable"' do
    context 'when type is "disable"' do
      let(:type) { 'disable' }

      it 'disable user' do


@@ 29,7 29,7 @@ RSpec.describe Admin::AccountAction, type: :model do
      end
    end

    context 'type is "silence"' do
    context 'when type is "silence"' do
      let(:type) { 'silence' }

      it 'silences account' do


@@ 38,7 38,7 @@ RSpec.describe Admin::AccountAction, type: :model do
      end
    end

    context 'type is "suspend"' do
    context 'when type is "suspend"' do
      let(:type) { 'suspend' }

      it 'suspends account' do


@@ 75,7 75,7 @@ RSpec.describe Admin::AccountAction, type: :model do
  describe '#report' do
    subject { account_action.report }

    context 'report_id.present?' do
    context 'with report_id.present?' do
      before do
        account_action.report_id = Fabricate(:report).id
      end


@@ 85,7 85,7 @@ RSpec.describe Admin::AccountAction, type: :model do
      end
    end

    context '!report_id.present?' do
    context 'with !report_id.present?' do
      it 'returns nil' do
        expect(subject).to be_nil
      end


@@ 95,7 95,7 @@ RSpec.describe Admin::AccountAction, type: :model do
  describe '#with_report?' do
    subject { account_action.with_report? }

    context '!report.nil?' do
    context 'with !report.nil?' do
      before do
        account_action.report_id = Fabricate(:report).id
      end


@@ 105,7 105,7 @@ RSpec.describe Admin::AccountAction, type: :model do
      end
    end

    context '!(!report.nil?)' do
    context 'with !(!report.nil?)' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 115,7 115,7 @@ RSpec.describe Admin::AccountAction, type: :model do
  describe '.types_for_account' do
    subject { described_class.types_for_account(account) }

    context 'account.local?' do
    context 'when Account.local?' do
      let(:account) { Fabricate(:account, domain: nil) }

      it 'returns ["none", "disable", "sensitive", "silence", "suspend"]' do


@@ 123,7 123,7 @@ RSpec.describe Admin::AccountAction, type: :model do
      end
    end

    context '!account.local?' do
    context 'with !account.local?' do
      let(:account) { Fabricate(:account, domain: 'hoge.com') }

      it 'returns ["sensitive", "silence", "suspend"]' do

M spec/models/admin/action_log_spec.rb => spec/models/admin/action_log_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Admin::ActionLog, type: :model do
RSpec.describe Admin::ActionLog do
  describe '#action' do
    it 'returns action' do
      action_log = described_class.new(action: 'hoge')

M spec/models/announcement_mute_spec.rb => spec/models/announcement_mute_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe AnnouncementMute, type: :model do
RSpec.describe AnnouncementMute do
end

M spec/models/announcement_reaction_spec.rb => spec/models/announcement_reaction_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe AnnouncementReaction, type: :model do
RSpec.describe AnnouncementReaction do
end

M spec/models/announcement_spec.rb => spec/models/announcement_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Announcement, type: :model do
RSpec.describe Announcement do
end

M spec/models/backup_spec.rb => spec/models/backup_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Backup, type: :model do
RSpec.describe Backup do
end

M spec/models/block_spec.rb => spec/models/block_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Block, type: :model do
RSpec.describe Block do
  describe 'validations' do
    it 'is invalid without an account' do
      block = Fabricate.build(:block, account: nil)

M spec/models/canonical_email_block_spec.rb => spec/models/canonical_email_block_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe CanonicalEmailBlock, type: :model do
RSpec.describe CanonicalEmailBlock do
  describe '#email=' do
    let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' }


M spec/models/concerns/account_interactions_spec.rb => spec/models/concerns/account_interactions_spec.rb +85 -57
@@ 13,21 13,21 @@ describe AccountInteractions do
  describe '.following_map' do
    subject { Account.following_map(target_account_ids, account_id) }

    context 'account with Follow' do
      it 'returns { target_account_id => { reblogs: true } }' do
    context 'when Account with Follow' do
      it 'returns { target_account_id => true }' do
        Fabricate(:follow, account: account, target_account: target_account)
        expect(subject).to eq(target_account_id => { reblogs: true, notify: false, languages: nil })
      end
    end

    context 'account with Follow but with reblogs disabled' do
    context 'when Account with Follow but with reblogs disabled' do
      it 'returns { target_account_id => { reblogs: false } }' do
        Fabricate(:follow, account: account, target_account: target_account, show_reblogs: false)
        expect(subject).to eq(target_account_id => { reblogs: false, notify: false, languages: nil })
      end
    end

    context 'account without Follow' do
    context 'when Account without Follow' do
      it 'returns {}' do
        expect(subject).to eq({})
      end


@@ 37,14 37,14 @@ describe AccountInteractions do
  describe '.followed_by_map' do
    subject { Account.followed_by_map(target_account_ids, account_id) }

    context 'account with Follow' do
    context 'when Account with Follow' do
      it 'returns { target_account_id => true }' do
        Fabricate(:follow, account: target_account, target_account: account)
        expect(subject).to eq(target_account_id => true)
      end
    end

    context 'account without Follow' do
    context 'when Account without Follow' do
      it 'returns {}' do
        expect(subject).to eq({})
      end


@@ 54,14 54,14 @@ describe AccountInteractions do
  describe '.blocking_map' do
    subject { Account.blocking_map(target_account_ids, account_id) }

    context 'account with Block' do
    context 'when Account with Block' do
      it 'returns { target_account_id => true }' do
        Fabricate(:block, account: account, target_account: target_account)
        expect(subject).to eq(target_account_id => true)
      end
    end

    context 'account without Block' do
    context 'when Account without Block' do
      it 'returns {}' do
        expect(subject).to eq({})
      end


@@ 71,12 71,12 @@ describe AccountInteractions do
  describe '.muting_map' do
    subject { Account.muting_map(target_account_ids, account_id) }

    context 'account with Mute' do
    context 'when Account with Mute' do
      before do
        Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide)
      end

      context 'if Mute#hide_notifications?' do
      context 'when Mute#hide_notifications?' do
        let(:hide) { true }

        it 'returns { target_account_id => { notifications: true } }' do


@@ 84,7 84,7 @@ describe AccountInteractions do
        end
      end

      context 'unless Mute#hide_notifications?' do
      context 'when not Mute#hide_notifications?' do
        let(:hide) { false }

        it 'returns { target_account_id => { notifications: false } }' do


@@ 93,7 93,7 @@ describe AccountInteractions do
      end
    end

    context 'account without Mute' do
    context 'when Account without Mute' do
      it 'returns {}' do
        expect(subject).to eq({})
      end


@@ 119,8 119,8 @@ describe AccountInteractions do
  describe '#mute!' do
    subject { account.mute!(target_account, notifications: arg_notifications) }

    context 'Mute does not exist yet' do
      context 'arg :notifications is nil' do
    context 'when Mute does not exist yet' do
      context 'when arg :notifications is nil' do
        let(:arg_notifications) { nil }

        it 'creates Mute, and returns Mute' do


@@ 130,7 130,7 @@ describe AccountInteractions do
        end
      end

      context 'arg :notifications is false' do
      context 'when arg :notifications is false' do
        let(:arg_notifications) { false }

        it 'creates Mute, and returns Mute' do


@@ 140,7 140,7 @@ describe AccountInteractions do
        end
      end

      context 'arg :notifications is true' do
      context 'when arg :notifications is true' do
        let(:arg_notifications) { true }

        it 'creates Mute, and returns Mute' do


@@ 151,7 151,7 @@ describe AccountInteractions do
      end
    end

    context 'Mute already exists' do
    context 'when Mute already exists' do
      before do
        account.mute_relationships << mute
      end


@@ 163,10 163,10 @@ describe AccountInteractions do
                  hide_notifications: hide_notifications)
      end

      context 'mute.hide_notifications is true' do
      context 'when mute.hide_notifications is true' do
        let(:hide_notifications) { true }

        context 'arg :notifications is nil' do
        context 'when arg :notifications is nil' do
          let(:arg_notifications) { nil }

          it 'returns Mute without updating mute.hide_notifications' do


@@ 176,7 176,7 @@ describe AccountInteractions do
          end
        end

        context 'arg :notifications is false' do
        context 'when arg :notifications is false' do
          let(:arg_notifications) { false }

          it 'returns Mute, and updates mute.hide_notifications false' do


@@ 186,7 186,7 @@ describe AccountInteractions do
          end
        end

        context 'arg :notifications is true' do
        context 'when arg :notifications is true' do
          let(:arg_notifications) { true }

          it 'returns Mute without updating mute.hide_notifications' do


@@ 197,10 197,10 @@ describe AccountInteractions do
        end
      end

      context 'mute.hide_notifications is false' do
      context 'when mute.hide_notifications is false' do
        let(:hide_notifications) { false }

        context 'arg :notifications is nil' do
        context 'when arg :notifications is nil' do
          let(:arg_notifications) { nil }

          it 'returns Mute, and updates mute.hide_notifications true' do


@@ 210,7 210,7 @@ describe AccountInteractions do
          end
        end

        context 'arg :notifications is false' do
        context 'when arg :notifications is false' do
          let(:arg_notifications) { false }

          it 'returns Mute without updating mute.hide_notifications' do


@@ 220,7 220,7 @@ describe AccountInteractions do
          end
        end

        context 'arg :notifications is true' do
        context 'when arg :notifications is true' do
          let(:arg_notifications) { true }

          it 'returns Mute, and updates mute.hide_notifications true' do


@@ 260,7 260,7 @@ describe AccountInteractions do
  describe '#unfollow!' do
    subject { account.unfollow!(target_account) }

    context 'following target_account' do
    context 'when following target_account' do
      it 'returns destroyed Follow' do
        account.active_relationships.create(target_account: target_account)
        expect(subject).to be_a Follow


@@ 268,7 268,7 @@ describe AccountInteractions do
      end
    end

    context 'not following target_account' do
    context 'when not following target_account' do
      it 'returns nil' do
        expect(subject).to be_nil
      end


@@ 278,7 278,7 @@ describe AccountInteractions do
  describe '#unblock!' do
    subject { account.unblock!(target_account) }

    context 'blocking target_account' do
    context 'when blocking target_account' do
      it 'returns destroyed Block' do
        account.block_relationships.create(target_account: target_account)
        expect(subject).to be_a Block


@@ 286,7 286,7 @@ describe AccountInteractions do
      end
    end

    context 'not blocking target_account' do
    context 'when not blocking target_account' do
      it 'returns nil' do
        expect(subject).to be_nil
      end


@@ 296,7 296,7 @@ describe AccountInteractions do
  describe '#unmute!' do
    subject { account.unmute!(target_account) }

    context 'muting target_account' do
    context 'when muting target_account' do
      it 'returns destroyed Mute' do
        account.mute_relationships.create(target_account: target_account)
        expect(subject).to be_a Mute


@@ 304,7 304,7 @@ describe AccountInteractions do
      end
    end

    context 'not muting target_account' do
    context 'when not muting target_account' do
      it 'returns nil' do
        expect(subject).to be_nil
      end


@@ 316,7 316,7 @@ describe AccountInteractions do

    let(:conversation) { Fabricate(:conversation) }

    context 'muting the conversation' do
    context 'when muting the conversation' do
      it 'returns destroyed ConversationMute' do
        account.conversation_mutes.create(conversation: conversation)
        expect(subject).to be_a ConversationMute


@@ 324,7 324,7 @@ describe AccountInteractions do
      end
    end

    context 'not muting the conversation' do
    context 'when not muting the conversation' do
      it 'returns nil' do
        expect(subject).to be_nil
      end


@@ 336,7 336,7 @@ describe AccountInteractions do

    let(:domain) { 'example.com' }

    context 'blocking the domain' do
    context 'when blocking the domain' do
      it 'returns destroyed AccountDomainBlock' do
        account_domain_block = Fabricate(:account_domain_block, domain: domain)
        account.domain_blocks << account_domain_block


@@ 345,7 345,7 @@ describe AccountInteractions do
      end
    end

    context 'unblocking the domain' do
    context 'when unblocking the domain' do
      it 'returns nil' do
        expect(subject).to be_nil
      end


@@ 355,14 355,14 @@ describe AccountInteractions do
  describe '#following?' do
    subject { account.following?(target_account) }

    context 'following target_account' do
    context 'when following target_account' do
      it 'returns true' do
        account.active_relationships.create(target_account: target_account)
        expect(subject).to be true
      end
    end

    context 'not following target_account' do
    context 'when not following target_account' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 372,14 372,14 @@ describe AccountInteractions do
  describe '#followed_by?' do
    subject { account.followed_by?(target_account) }

    context 'followed by target_account' do
    context 'when followed by target_account' do
      it 'returns true' do
        account.passive_relationships.create(account: target_account)
        expect(subject).to be true
      end
    end

    context 'not followed by target_account' do
    context 'when not followed by target_account' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 389,14 389,14 @@ describe AccountInteractions do
  describe '#blocking?' do
    subject { account.blocking?(target_account) }

    context 'blocking target_account' do
    context 'when blocking target_account' do
      it 'returns true' do
        account.block_relationships.create(target_account: target_account)
        expect(subject).to be true
      end
    end

    context 'not blocking target_account' do
    context 'when not blocking target_account' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 408,7 408,7 @@ describe AccountInteractions do

    let(:domain) { 'example.com' }

    context 'blocking the domain' do
    context 'when blocking the domain' do
      it 'returns true' do
        account_domain_block = Fabricate(:account_domain_block, domain: domain)
        account.domain_blocks << account_domain_block


@@ 416,7 416,7 @@ describe AccountInteractions do
      end
    end

    context 'not blocking the domain' do
    context 'when not blocking the domain' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 426,7 426,7 @@ describe AccountInteractions do
  describe '#muting?' do
    subject { account.muting?(target_account) }

    context 'muting target_account' do
    context 'when muting target_account' do
      it 'returns true' do
        mute = Fabricate(:mute, account: account, target_account: target_account)
        account.mute_relationships << mute


@@ 434,7 434,7 @@ describe AccountInteractions do
      end
    end

    context 'not muting target_account' do
    context 'when not muting target_account' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 446,14 446,14 @@ describe AccountInteractions do

    let(:conversation) { Fabricate(:conversation) }

    context 'muting the conversation' do
    context 'when muting the conversation' do
      it 'returns true' do
        account.conversation_mutes.create(conversation: conversation)
        expect(subject).to be true
      end
    end

    context 'not muting the conversation' do
    context 'when not muting the conversation' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 468,7 468,7 @@ describe AccountInteractions do
      account.mute_relationships << mute
    end

    context 'muting notifications of target_account' do
    context 'when muting notifications of target_account' do
      let(:hide) { true }

      it 'returns true' do


@@ 476,7 476,7 @@ describe AccountInteractions do
      end
    end

    context 'not muting notifications of target_account' do
    context 'when not muting notifications of target_account' do
      let(:hide) { false }

      it 'returns false' do


@@ 488,14 488,14 @@ describe AccountInteractions do
  describe '#requested?' do
    subject { account.requested?(target_account) }

    context 'requested by target_account' do
    context 'with requested by target_account' do
      it 'returns true' do
        Fabricate(:follow_request, account: account, target_account: target_account)
        expect(subject).to be true
      end
    end

    context 'not requested by target_account' do
    context 'when not requested by target_account' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 507,7 507,7 @@ describe AccountInteractions do

    let(:status) { Fabricate(:status, account: account, favourites: favourites) }

    context 'favorited' do
    context 'when favorited' do
      let(:favourites) { [Fabricate(:favourite, account: account)] }

      it 'returns true' do


@@ 515,7 515,7 @@ describe AccountInteractions do
      end
    end

    context 'not favorited' do
    context 'when not favorited' do
      let(:favourites) { [] }

      it 'returns false' do


@@ 529,7 529,7 @@ describe AccountInteractions do

    let(:status) { Fabricate(:status, account: account, reblogs: reblogs) }

    context 'reblogged' do
    context 'with reblogged' do
      let(:reblogs) { [Fabricate(:status, account: account)] }

      it 'returns true' do


@@ 537,7 537,7 @@ describe AccountInteractions do
      end
    end

    context 'not reblogged' do
    context 'when not reblogged' do
      let(:reblogs) { [] }

      it 'returns false' do


@@ 551,14 551,14 @@ describe AccountInteractions do

    let(:status) { Fabricate(:status, account: account) }

    context 'pinned' do
    context 'when pinned' do
      it 'returns true' do
        Fabricate(:status_pin, account: account, status: status)
        expect(subject).to be true
      end
    end

    context 'not pinned' do
    context 'when not pinned' do
      it 'returns false' do
        expect(subject).to be false
      end


@@ 690,4 690,32 @@ describe AccountInteractions do
      end
    end
  end

  describe '#lists_for_local_distribution' do
    let(:account)                 { Fabricate(:user, current_sign_in_at: Time.now.utc).account }
    let!(:inactive_follower_user) { Fabricate(:user, current_sign_in_at: 5.years.ago) }
    let!(:follower_user)          { Fabricate(:user, current_sign_in_at: Time.now.utc) }
    let!(:follow_request_user)    { Fabricate(:user, current_sign_in_at: Time.now.utc) }

    let!(:inactive_follower_list) { Fabricate(:list, account: inactive_follower_user.account) }
    let!(:follower_list)          { Fabricate(:list, account: follower_user.account) }
    let!(:follow_request_list)    { Fabricate(:list, account: follow_request_user.account) }

    let!(:self_list)              { Fabricate(:list, account: account) }

    before do
      inactive_follower_user.account.follow!(account)
      follower_user.account.follow!(account)
      follow_request_user.account.follow_requests.create!(target_account: account)

      inactive_follower_list.accounts << account
      follower_list.accounts << account
      follow_request_list.accounts << account
      self_list.accounts << account
    end

    it 'includes only the list from the active follower and from oneself' do
      expect(account.lists_for_local_distribution.to_a).to contain_exactly(follower_list, self_list)
    end
  end
end

M spec/models/concerns/remotable_spec.rb => spec/models/concerns/remotable_spec.rb +25 -26
@@ 3,48 3,47 @@
require 'rails_helper'

RSpec.describe Remotable do
  class Foo
    def initialize
      @attrs = {}
    end
  let(:foo_class) do
    Class.new do
      def initialize
        @attrs = {}
      end

    def [](arg)
      @attrs[arg]
    end
      def [](arg)
        @attrs[arg]
      end

    def []=(arg1, arg2)
      @attrs[arg1] = arg2
    end
      def []=(arg1, arg2)
        @attrs[arg1] = arg2
      end

    def hoge=(arg); end
      def hoge=(arg); end

    def hoge_file_name; end
      def hoge_file_name; end

    def hoge_file_name=(arg); end
      def hoge_file_name=(arg); end

    def has_attribute?(arg); end
      def has_attribute?(arg); end

    def self.attachment_definitions
      { hoge: nil }
    end
  end

  before do
    class Foo
      include Remotable

      remotable_attachment :hoge, 1.kilobyte
      def self.attachment_definitions
        { hoge: nil }
      end
    end
  end

  let(:attribute_name) { "#{hoge}_remote_url".to_sym }
  let(:code)           { 200 }
  let(:file)           { 'filename="foo.txt"' }
  let(:foo)            { Foo.new }
  let(:foo)            { foo_class.new }
  let(:headers)        { { 'content-disposition' => file } }
  let(:hoge)           { :hoge }
  let(:url)            { 'https://google.com' }

  before do
    foo_class.include described_class
    foo_class.remotable_attachment :hoge, 1.kilobyte
  end

  it 'defines a method #hoge_remote_url=' do
    expect(foo).to respond_to(:hoge_remote_url=)
  end


@@ 157,7 156,7 @@ RSpec.describe Remotable do
      context 'when the response is successful' do
        let(:code) { 200 }

        context 'and contains Content-Disposition header' do
        context 'when contains Content-Disposition header' do
          let(:file)      { 'filename="foo.txt"' }
          let(:headers)   { { 'content-disposition' => file } }


M spec/models/conversation_mute_spec.rb => spec/models/conversation_mute_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe ConversationMute, type: :model do
RSpec.describe ConversationMute do
end

M spec/models/conversation_spec.rb => spec/models/conversation_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Conversation, type: :model do
RSpec.describe Conversation do
  describe '#local?' do
    it 'returns true when URI is nil' do
      expect(Fabricate(:conversation).local?).to be true

M spec/models/custom_emoji_filter_spec.rb => spec/models/custom_emoji_filter_spec.rb +7 -7
@@ 10,8 10,8 @@ RSpec.describe CustomEmojiFilter do
    let!(:custom_emoji_1) { Fabricate(:custom_emoji, domain: 'b') }
    let!(:custom_emoji_2) { Fabricate(:custom_emoji, domain: nil, shortcode: 'hoge') }

    context 'params have values' do
      context 'local' do
    context 'when params have values' do
      context 'when local' do
        let(:params) { { local: true } }

        it 'returns ActiveRecord::Relation' do


@@ 20,7 20,7 @@ RSpec.describe CustomEmojiFilter do
        end
      end

      context 'remote' do
      context 'when remote' do
        let(:params) { { remote: true } }

        it 'returns ActiveRecord::Relation' do


@@ 29,7 29,7 @@ RSpec.describe CustomEmojiFilter do
        end
      end

      context 'by_domain' do
      context 'with by_domain' do
        let(:params) { { by_domain: 'a' } }

        it 'returns ActiveRecord::Relation' do


@@ 38,7 38,7 @@ RSpec.describe CustomEmojiFilter do
        end
      end

      context 'shortcode' do
      context 'when shortcode' do
        let(:params) { { shortcode: 'hoge' } }

        it 'returns ActiveRecord::Relation' do


@@ 47,7 47,7 @@ RSpec.describe CustomEmojiFilter do
        end
      end

      context 'else' do
      context 'when some other case' do
        let(:params) { { else: 'else' } }

        it 'raises Mastodon::InvalidParameterError' do


@@ 58,7 58,7 @@ RSpec.describe CustomEmojiFilter do
      end
    end

    context 'params without value' do
    context 'when params without value' do
      let(:params) { { hoge: nil } }

      it 'returns ActiveRecord::Relation' do

M spec/models/custom_emoji_spec.rb => spec/models/custom_emoji_spec.rb +5 -5
@@ 2,13 2,13 @@

require 'rails_helper'

RSpec.describe CustomEmoji, type: :model do
RSpec.describe CustomEmoji do
  describe '#search' do
    subject { described_class.search(search_term) }

    let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: shortcode) }

    context 'shortcode is exact' do
    context 'when shortcode is exact' do
      let(:shortcode) { 'blobpats' }
      let(:search_term) { 'blobpats' }



@@ 17,7 17,7 @@ RSpec.describe CustomEmoji, type: :model do
      end
    end

    context 'shortcode is partial' do
    context 'when shortcode is partial' do
      let(:shortcode) { 'blobpats' }
      let(:search_term) { 'blob' }



@@ 32,7 32,7 @@ RSpec.describe CustomEmoji, type: :model do

    let(:custom_emoji) { Fabricate(:custom_emoji, domain: domain) }

    context 'domain is nil' do
    context 'when domain is nil' do
      let(:domain) { nil }

      it 'returns true' do


@@ 40,7 40,7 @@ RSpec.describe CustomEmoji, type: :model do
      end
    end

    context 'domain is present' do
    context 'when domain is present' do
      let(:domain) { 'example.com' }

      it 'returns false' do

M spec/models/custom_filter_keyword_spec.rb => spec/models/custom_filter_keyword_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe CustomFilterKeyword, type: :model do
RSpec.describe CustomFilterKeyword do
end

M spec/models/custom_filter_spec.rb => spec/models/custom_filter_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe CustomFilter, type: :model do
RSpec.describe CustomFilter do
end

M spec/models/device_spec.rb => spec/models/device_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Device, type: :model do
RSpec.describe Device do
end

M spec/models/domain_block_spec.rb => spec/models/domain_block_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe DomainBlock, type: :model do
RSpec.describe DomainBlock do
  describe 'validations' do
    it 'is invalid without a domain' do
      domain_block = Fabricate.build(:domain_block, domain: nil)

M spec/models/email_domain_block_spec.rb => spec/models/email_domain_block_spec.rb +3 -3
@@ 2,11 2,11 @@

require 'rails_helper'

RSpec.describe EmailDomainBlock, type: :model do
RSpec.describe EmailDomainBlock do
  describe 'block?' do
    let(:input) { nil }

    context 'given an e-mail address' do
    context 'when given an e-mail address' do
      let(:input) { "foo@#{domain}" }

      context do


@@ 33,7 33,7 @@ RSpec.describe EmailDomainBlock, type: :model do
      end
    end

    context 'given an array of domains' do
    context 'when given an array of domains' do
      let(:input) { %w(foo.com mail.foo.com) }

      it 'returns true if the domain is blocked' do

M spec/models/encrypted_message_spec.rb => spec/models/encrypted_message_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe EncryptedMessage, type: :model do
RSpec.describe EncryptedMessage do
end

M spec/models/export_spec.rb => spec/models/export_spec.rb +6 -6
@@ 10,7 10,7 @@ describe Export do

  describe 'to_csv' do
    it 'returns a csv of the blocked accounts' do
      target_accounts.each(&account.method(:block!))
      target_accounts.each { |target_account| account.block!(target_account) }

      export = Export.new(account).to_blocked_accounts_csv
      results = export.strip.split


@@ 20,7 20,7 @@ describe Export do
    end

    it 'returns a csv of the muted accounts' do
      target_accounts.each(&account.method(:mute!))
      target_accounts.each { |target_account| account.mute!(target_account) }

      export = Export.new(account).to_muted_accounts_csv
      results = export.strip.split("\n")


@@ 31,7 31,7 @@ describe Export do
    end

    it 'returns a csv of the following accounts' do
      target_accounts.each(&account.method(:follow!))
      target_accounts.each { |target_account| account.follow!(target_account) }

      export = Export.new(account).to_following_accounts_csv
      results = export.strip.split("\n")


@@ 51,17 51,17 @@ describe Export do

  describe 'total_follows' do
    it 'returns the total number of the followed accounts' do
      target_accounts.each(&account.method(:follow!))
      target_accounts.each { |target_account| account.follow!(target_account) }
      expect(Export.new(account.reload).total_follows).to eq 2
    end

    it 'returns the total number of the blocked accounts' do
      target_accounts.each(&account.method(:block!))
      target_accounts.each { |target_account| account.block!(target_account) }
      expect(Export.new(account.reload).total_blocks).to eq 2
    end

    it 'returns the total number of the muted accounts' do
      target_accounts.each(&account.method(:mute!))
      target_accounts.each { |target_account| account.mute!(target_account) }
      expect(Export.new(account.reload).total_mutes).to eq 2
    end
  end

M spec/models/favourite_spec.rb => spec/models/favourite_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Favourite, type: :model do
RSpec.describe Favourite do
  let(:account) { Fabricate(:account) }

  context 'when status is a reblog' do

M spec/models/featured_tag_spec.rb => spec/models/featured_tag_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe FeaturedTag, type: :model do
RSpec.describe FeaturedTag do
end

M spec/models/follow_recommendation_suppression_spec.rb => spec/models/follow_recommendation_suppression_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe FollowRecommendationSuppression, type: :model do
RSpec.describe FollowRecommendationSuppression do
end

M spec/models/follow_request_spec.rb => spec/models/follow_request_spec.rb +38 -6
@@ 2,15 2,29 @@

require 'rails_helper'

RSpec.describe FollowRequest, type: :model do
RSpec.describe FollowRequest do
  describe '#authorize!' do
    let(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) }
    let(:account)        { Fabricate(:account) }
    let(:target_account) { Fabricate(:account) }
    let!(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) }
    let(:account)         { Fabricate(:account) }
    let(:target_account)  { Fabricate(:account) }

    context 'when the to-be-followed person has been added to a list' do
      let!(:list) { Fabricate(:list, account: account) }

      before do
        list.accounts << target_account
      end

      it 'updates the ListAccount' do
        expect { follow_request.authorize! }.to change { [list.list_accounts.first.follow_request_id, list.list_accounts.first.follow_id] }.from([follow_request.id, nil]).to([nil, anything])
      end
    end

    it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
      expect(account).to        receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true)
      expect(MergeWorker).to    receive(:perform_async).with(target_account.id, account.id)
      expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true) do
        account.active_relationships.create!(target_account: target_account)
      end
      expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id)
      expect(follow_request).to receive(:destroy!)
      follow_request.authorize!
    end


@@ 36,4 50,22 @@ RSpec.describe FollowRequest, type: :model do
      expect(follow_request.account.muting_reblogs?(target)).to be true
    end
  end

  describe '#reject!' do
    let!(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) }
    let(:account)         { Fabricate(:account) }
    let(:target_account)  { Fabricate(:account) }

    context 'when the to-be-followed person has been added to a list' do
      let!(:list) { Fabricate(:list, account: account) }

      before do
        list.accounts << target_account
      end

      it 'deletes the ListAccount record' do
        expect { follow_request.reject! }.to change { list.accounts.count }.from(1).to(0)
      end
    end
  end
end

M spec/models/follow_spec.rb => spec/models/follow_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Follow, type: :model do
RSpec.describe Follow do
  let(:alice) { Fabricate(:account, username: 'alice') }
  let(:bob)   { Fabricate(:account, username: 'bob') }


A spec/models/form/import_spec.rb => spec/models/form/import_spec.rb +281 -0
@@ 0,0 1,281 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Form::Import do
  subject { described_class.new(current_account: account, type: import_type, mode: import_mode, data: data) }

  let(:account)     { Fabricate(:account) }
  let(:data)        { fixture_file_upload(import_file) }
  let(:import_mode) { 'merge' }

  describe 'validations' do
    shared_examples 'incompatible import type' do |type, file|
      let(:import_file) { file }
      let(:import_type) { type }

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('imports.errors.incompatible_type'))
      end
    end

    shared_examples 'too many CSV rows' do |type, file, allowed_rows|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        stub_const 'Form::Import::ROWS_PROCESSING_LIMIT', allowed_rows
      end

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: Form::Import::ROWS_PROCESSING_LIMIT))
      end
    end

    shared_examples 'valid import' do |type, file|
      let(:import_file) { file }
      let(:import_type) { type }

      it 'passes validation' do
        expect(subject).to be_valid
      end
    end

    context 'when the file too large' do
      let(:import_type) { 'following' }
      let(:import_file) { 'imports.txt' }

      before do
        stub_const 'Form::Import::FILE_SIZE_LIMIT', 5
      end

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('imports.errors.too_large'))
      end
    end

    context 'when the CSV file is malformed CSV' do
      let(:import_type) { 'following' }
      let(:import_file) { 'boop.ogg' }

      it 'has errors' do
        # NOTE: not testing more specific error because we don't know the string to match
        expect(subject).to model_have_error_on_field(:data)
      end
    end

    context 'when importing more follows than allowed' do
      let(:import_type) { 'following' }
      let(:import_file) { 'imports.txt' }

      before do
        allow(FollowLimitValidator).to receive(:limit_for_account).with(account).and_return(1)
      end

      it 'has errors' do
        subject.validate
        expect(subject.errors[:data]).to include(I18n.t('users.follow_limit_reached', limit: 1))
      end
    end

    it_behaves_like 'too many CSV rows', 'following', 'imports.txt', 1
    it_behaves_like 'too many CSV rows', 'blocking', 'imports.txt', 1
    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

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

    # Importing domain blocks with headers into expected type
    it_behaves_like 'valid import', 'domain_blocking', 'domain_blocks.csv'

    # Importing bookmarks list with no headers into expected type
    it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt'

    # 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'
    it_behaves_like 'valid import', 'muting', 'following_accounts.csv'

    # Importing domain blocks with headers into incompatible types
    it_behaves_like 'incompatible import type', 'following', 'domain_blocks.csv'
    it_behaves_like 'incompatible import type', 'blocking', 'domain_blocks.csv'
    it_behaves_like 'incompatible import type', 'muting', 'domain_blocks.csv'
    it_behaves_like 'incompatible import type', 'bookmarks', 'domain_blocks.csv'

    # Importing followed accounts with headers into incompatible types
    it_behaves_like 'incompatible import type', 'domain_blocking', 'following_accounts.csv'
    it_behaves_like 'incompatible import type', 'bookmarks', 'following_accounts.csv'
  end

  describe '#guessed_type' do
    shared_examples 'with enough information' do |type, file, original_filename, expected_guess|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        allow(data).to receive(:original_filename).and_return(original_filename)
      end

      it 'guesses the expected type' do
        expect(subject.guessed_type).to eq expected_guess
      end
    end

    context 'when the headers are enough to disambiguate' do
      it_behaves_like 'with enough information', 'following', 'following_accounts.csv', 'import.csv', :following
      it_behaves_like 'with enough information', 'blocking', 'following_accounts.csv', 'import.csv', :following
      it_behaves_like 'with enough information', 'muting', 'following_accounts.csv', 'import.csv', :following

      it_behaves_like 'with enough information', 'following', 'muted_accounts.csv', 'imports.csv', :muting
      it_behaves_like 'with enough information', 'blocking', 'muted_accounts.csv', 'imports.csv', :muting
      it_behaves_like 'with enough information', 'muting', 'muted_accounts.csv', 'imports.csv', :muting
    end

    context 'when the file name is enough to disambiguate' do
      it_behaves_like 'with enough information', 'following', 'imports.txt', 'following_accounts.csv', :following
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'following_accounts.csv', :following
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'following_accounts.csv', :following

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'follows.csv', :following
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'follows.csv', :following
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'follows.csv', :following

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocked_accounts.csv', :blocking
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocked_accounts.csv', :blocking
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocked_accounts.csv', :blocking

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocks.csv', :blocking
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocks.csv', :blocking
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocks.csv', :blocking

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'muted_accounts.csv', :muting
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'muted_accounts.csv', :muting
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'muted_accounts.csv', :muting

      it_behaves_like 'with enough information', 'following', 'imports.txt', 'mutes.csv', :muting
      it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'mutes.csv', :muting
      it_behaves_like 'with enough information', 'muting', 'imports.txt', 'mutes.csv', :muting
    end
  end

  describe '#likely_mismatched?' do
    shared_examples 'with matching types' do |type, file, original_filename = nil|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present?
      end

      it 'returns false' do
        expect(subject.likely_mismatched?).to be false
      end
    end

    shared_examples 'with mismatching types' do |type, file, original_filename = nil|
      let(:import_file) { file }
      let(:import_type) { type }

      before do
        allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present?
      end

      it 'returns true' do
        expect(subject.likely_mismatched?).to be true
      end
    end

    it_behaves_like 'with matching types', 'following', 'following_accounts.csv'
    it_behaves_like 'with matching types', 'following', 'following_accounts.csv', 'imports.txt'
    it_behaves_like 'with matching types', 'following', 'imports.txt'
    it_behaves_like 'with matching types', 'blocking', 'imports.txt', 'blocks.csv'
    it_behaves_like 'with matching types', 'blocking', 'imports.txt'
    it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv'
    it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv', 'imports.txt'
    it_behaves_like 'with matching types', 'muting', 'imports.txt'
    it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv'
    it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv', 'imports.txt'
    it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt'
    it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt', 'imports.txt'

    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocks.csv'
    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocked_accounts.csv'
    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'mutes.csv'
    it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'follows.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'mutes.csv'
    it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'muted_accounts.csv'
    it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv', 'imports.txt'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'follows.csv'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'following_accounts.csv'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocks.csv'
    it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocked_accounts.csv'
  end

  describe 'save' do
    shared_examples 'on successful import' do |type, mode, file, expected_rows|
      let(:import_type) { type }
      let(:import_file) { file }
      let(:import_mode) { mode }

      before do
        subject.save
      end

      it 'creates the expected rows' do
        expect(account.bulk_imports.first.rows.pluck(:data)).to match_array(expected_rows)
      end

      it 'creates a BulkImport with expected attributes' do
        bulk_import = account.bulk_imports.first
        expect(bulk_import).to_not be_nil
        expect(bulk_import.type.to_sym).to eq subject.type.to_sym
        expect(bulk_import.original_filename).to eq subject.data.original_filename
        expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched?
        expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation
        expect(bulk_import.processed_items).to eq 0
        expect(bulk_import.imported_items).to eq 0
        expect(bulk_import.total_items).to eq bulk_import.rows.count
        expect(bulk_import.unconfirmed?).to be true
      end
    end

    it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
    it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } })
    it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } })

    it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [
      { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil },
      { 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => ['en', 'fr'] },
    ]

    it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [
      { 'acct' => 'user@example.com', 'hide_notifications' => true },
      { 'acct' => 'user@test.com', 'hide_notifications' => false },
    ]

    # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
    #
    # https://github.com/mastodon/mastodon/issues/20571
    it_behaves_like 'on successful import', 'following', 'merge', 'utf8-followers.txt', [{ 'acct' => 'nare@թութ.հայ' }]
  end
end

M spec/models/home_feed_spec.rb => spec/models/home_feed_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe HomeFeed, type: :model do
RSpec.describe HomeFeed do
  subject { described_class.new(account) }

  let(:account) { Fabricate(:account) }

M spec/models/identity_spec.rb => spec/models/identity_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Identity, type: :model do
RSpec.describe Identity do
  describe '.find_for_oauth' do
    let(:auth) { Fabricate(:identity, user: Fabricate(:user)) }


M spec/models/import_spec.rb => spec/models/import_spec.rb +1 -16
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Import, type: :model do
RSpec.describe Import do
  let(:account) { Fabricate(:account) }
  let(:type) { 'following' }
  let(:data) { attachment_fixture('imports.txt') }


@@ 22,20 22,5 @@ RSpec.describe Import, type: :model do
      import = Import.create(account: account, type: type)
      expect(import).to model_have_error_on_field(:data)
    end

    it 'is invalid with malformed data' do
      import = Import.create(account: account, type: type, data: StringIO.new('\"test'))
      expect(import).to model_have_error_on_field(:data)
    end

    it 'is invalid with too many rows in data' do
      import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (ImportService::ROWS_PROCESSING_LIMIT + 10)))
      expect(import).to model_have_error_on_field(:data)
    end

    it 'is invalid when there are more rows when following limit' do
      import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (FollowLimitValidator.limit_for_account(account) + 10)))
      expect(import).to model_have_error_on_field(:data)
    end
  end
end

M spec/models/invite_spec.rb => spec/models/invite_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Invite, type: :model do
RSpec.describe Invite do
  describe '#valid_for_use?' do
    it 'returns true when there are no limitations' do
      invite = Fabricate(:invite, max_uses: nil, expires_at: nil)

M spec/models/list_account_spec.rb => spec/models/list_account_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe ListAccount, type: :model do
RSpec.describe ListAccount do
end

M spec/models/list_spec.rb => spec/models/list_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe List, type: :model do
RSpec.describe List do
end

M spec/models/login_activity_spec.rb => spec/models/login_activity_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe LoginActivity, type: :model do
RSpec.describe LoginActivity do
end

M spec/models/media_attachment_spec.rb => spec/models/media_attachment_spec.rb +8 -8
@@ 2,13 2,13 @@

require 'rails_helper'

RSpec.describe MediaAttachment, type: :model do
RSpec.describe MediaAttachment do
  describe 'local?' do
    subject { media_attachment.local? }

    let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url) }

    context 'remote_url is blank' do
    context 'when remote_url is blank' do
      let(:remote_url) { '' }

      it 'returns true' do


@@ 16,7 16,7 @@ RSpec.describe MediaAttachment, type: :model do
      end
    end

    context 'remote_url is present' do
    context 'when remote_url is present' do
      let(:remote_url) { 'remote_url' }

      it 'returns false' do


@@ 30,10 30,10 @@ RSpec.describe MediaAttachment, type: :model do

    let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url, file: file) }

    context 'file is blank' do
    context 'when file is blank' do
      let(:file) { nil }

      context 'remote_url is present' do
      context 'when remote_url is present' do
        let(:remote_url) { 'remote_url' }

        it 'returns true' do


@@ 42,10 42,10 @@ RSpec.describe MediaAttachment, type: :model do
      end
    end

    context 'file is present' do
    context 'when file is present' do
      let(:file) { attachment_fixture('avatar.gif') }

      context 'remote_url is blank' do
      context 'when remote_url is blank' do
        let(:remote_url) { '' }

        it 'returns false' do


@@ 53,7 53,7 @@ RSpec.describe MediaAttachment, type: :model do
        end
      end

      context 'remote_url is present' do
      context 'when remote_url is present' do
        let(:remote_url) { 'remote_url' }

        it 'returns true' do

M spec/models/mention_spec.rb => spec/models/mention_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Mention, type: :model do
RSpec.describe Mention do
  describe 'validations' do
    it 'is invalid without an account' do
      mention = Fabricate.build(:mention, account: nil)

M spec/models/mute_spec.rb => spec/models/mute_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Mute, type: :model do
RSpec.describe Mute do
end

M spec/models/notification_spec.rb => spec/models/notification_spec.rb +6 -6
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Notification, type: :model do
RSpec.describe Notification do
  describe '#target_status' do
    let(:notification) { Fabricate(:notification, activity: activity) }
    let(:status)       { Fabricate(:status) }


@@ 10,7 10,7 @@ RSpec.describe Notification, type: :model do
    let(:favourite)    { Fabricate(:favourite, status: status) }
    let(:mention)      { Fabricate(:mention, status: status) }

    context 'activity is reblog' do
    context 'when Activity is reblog' do
      let(:activity) { reblog }

      it 'returns status' do


@@ 18,7 18,7 @@ RSpec.describe Notification, type: :model do
      end
    end

    context 'activity is favourite' do
    context 'when Activity is favourite' do
      let(:type)     { :favourite }
      let(:activity) { favourite }



@@ 27,7 27,7 @@ RSpec.describe Notification, type: :model do
      end
    end

    context 'activity is mention' do
    context 'when Activity is mention' do
      let(:activity) { mention }

      it 'returns status' do


@@ 66,7 66,7 @@ RSpec.describe Notification, type: :model do
      end
    end

    context 'notifications are empty' do
    context 'when notifications are empty' do
      let(:notifications) { [] }

      it 'returns []' do


@@ 74,7 74,7 @@ RSpec.describe Notification, type: :model do
      end
    end

    context 'notifications are present' do
    context 'when notifications are present' do
      before do
        notifications.each(&:reload)
      end

M spec/models/poll_vote_spec.rb => spec/models/poll_vote_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe PollVote, type: :model do
RSpec.describe PollVote do
  describe '#object_type' do
    let(:poll_vote) { Fabricate.build(:poll_vote) }


M spec/models/preview_card_spec.rb => spec/models/preview_card_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe PreviewCard, type: :model do
RSpec.describe PreviewCard do
end

M spec/models/preview_card_trend_spec.rb => spec/models/preview_card_trend_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe PreviewCardTrend, type: :model do
RSpec.describe PreviewCardTrend do
end

M spec/models/public_feed_spec.rb => spec/models/public_feed_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe PublicFeed, type: :model do
RSpec.describe PublicFeed do
  let(:account) { Fabricate(:account) }

  describe '#get' do

M spec/models/relay_spec.rb => spec/models/relay_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Relay, type: :model do
RSpec.describe Relay do
end

M spec/models/remote_follow_spec.rb => spec/models/remote_follow_spec.rb +4 -4
@@ 13,7 13,7 @@ RSpec.describe RemoteFollow do
  describe '.initialize' do
    subject { remote_follow.acct }

    context 'attrs with acct' do
    context 'when attrs with acct' do
      let(:attrs) { { acct: 'gargron@quitter.no' } }

      it 'returns acct' do


@@ 21,7 21,7 @@ RSpec.describe RemoteFollow do
      end
    end

    context 'attrs without acct' do
    context 'when attrs without acct' do
      let(:attrs) { {} }

      it do


@@ 33,7 33,7 @@ RSpec.describe RemoteFollow do
  describe '#valid?' do
    subject { remote_follow.valid? }

    context 'attrs with acct' do
    context 'when attrs with acct' do
      let(:attrs) { { acct: 'gargron@quitter.no' } }

      it do


@@ 41,7 41,7 @@ RSpec.describe RemoteFollow do
      end
    end

    context 'attrs without acct' do
    context 'when attrs without acct' do
      let(:attrs) { {} }

      it do

M spec/models/report_spec.rb => spec/models/report_spec.rb +2 -2
@@ 89,13 89,13 @@ describe Report do

    let(:report) { Fabricate(:report, action_taken_at: action_taken) }

    context 'if action is taken' do
    context 'when action is taken' do
      let(:action_taken) { Time.now.utc }

      it { is_expected.to be false }
    end

    context 'if action not is taken' do
    context 'when action not is taken' do
      let(:action_taken) { nil }

      it { is_expected.to be true }

M spec/models/scheduled_status_spec.rb => spec/models/scheduled_status_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe ScheduledStatus, type: :model do
RSpec.describe ScheduledStatus do
end

M spec/models/session_activation_spec.rb => spec/models/session_activation_spec.rb +7 -7
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe SessionActivation, type: :model do
RSpec.describe SessionActivation do
  describe '#detection' do
    let(:session_activation) { Fabricate(:session_activation, user_agent: 'Chrome/62.0.3202.89') }



@@ 40,7 40,7 @@ RSpec.describe SessionActivation, type: :model do
  describe '.active?' do
    subject { described_class.active?(id) }

    context 'id is absent' do
    context 'when id is absent' do
      let(:id) { nil }

      it 'returns nil' do


@@ 48,17 48,17 @@ RSpec.describe SessionActivation, type: :model do
      end
    end

    context 'id is present' do
    context 'when id is present' do
      let(:id) { '1' }
      let!(:session_activation) { Fabricate(:session_activation, session_id: id) }

      context 'id exists as session_id' do
      context 'when id exists as session_id' do
        it 'returns true' do
          expect(subject).to be true
        end
      end

      context 'id does not exist as session_id' do
      context 'when id does not exist as session_id' do
        before do
          session_activation.update!(session_id: '2')
        end


@@ 85,7 85,7 @@ RSpec.describe SessionActivation, type: :model do
  end

  describe '.deactivate' do
    context 'id is absent' do
    context 'when id is absent' do
      let(:id) { nil }

      it 'returns nil' do


@@ 93,7 93,7 @@ RSpec.describe SessionActivation, type: :model do
      end
    end

    context 'id exists' do
    context 'when id exists' do
      let(:id) { '1' }

      it 'calls where.destroy_all' do

M spec/models/setting_spec.rb => spec/models/setting_spec.rb +15 -15
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Setting, type: :model do
RSpec.describe Setting do
  describe '#to_param' do
    let(:setting) { Fabricate(:setting, var: var) }
    let(:var)     { 'var' }


@@ 19,7 19,7 @@ RSpec.describe Setting, type: :model do

    let(:key) { 'key' }

    context 'rails_initialized? is falsey' do
    context 'when rails_initialized? is falsey' do
      let(:rails_initialized) { false }

      it 'calls RailsSettings::Base#[]' do


@@ 28,7 28,7 @@ RSpec.describe Setting, type: :model do
      end
    end

    context 'rails_initialized? is truthy' do
    context 'when rails_initialized? is truthy' do
      before do
        allow(RailsSettings::Base).to receive(:cache_key).with(key, nil).and_return(cache_key)
      end


@@ 42,7 42,7 @@ RSpec.describe Setting, type: :model do
        described_class[key]
      end

      context 'Rails.cache does not exists' do
      context 'when Rails.cache does not exists' do
        before do
          allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object)
          allow(described_class).to receive(:default_settings).and_return(default_settings)


@@ 60,11 60,11 @@ RSpec.describe Setting, type: :model do
          described_class[key]
        end

        context 'RailsSettings::Settings.object returns truthy' do
        context 'when RailsSettings::Settings.object returns truthy' do
          let(:object) { db_val }
          let(:db_val) { double(value: 'db_val') }

          context 'default_value is a Hash' do
          context 'when default_value is a Hash' do
            let(:default_value) { { default_value: 'default_value' } }

            it 'calls default_value.with_indifferent_access.merge!' do


@@ 75,7 75,7 @@ RSpec.describe Setting, type: :model do
            end
          end

          context 'default_value is not a Hash' do
          context 'when default_value is not a Hash' do
            let(:default_value) { 'default_value' }

            it 'returns db_val.value' do


@@ 84,7 84,7 @@ RSpec.describe Setting, type: :model do
          end
        end

        context 'RailsSettings::Settings.object returns falsey' do
        context 'when RailsSettings::Settings.object returns falsey' do
          let(:object) { nil }

          it 'returns default_settings[key]' do


@@ 93,7 93,7 @@ RSpec.describe Setting, type: :model do
        end
      end

      context 'Rails.cache exists' do
      context 'when Rails.cache exists' do
        before do
          Rails.cache.write(cache_key, cache_value)
        end


@@ 130,7 130,7 @@ RSpec.describe Setting, type: :model do
      expect(described_class.all_as_records).to be_a Hash
    end

    context 'records includes Setting with var as the key' do
    context 'when records includes Setting with var as the key' do
      let(:records) { [original_setting] }

      it 'includes the original Setting' do


@@ 139,10 139,10 @@ RSpec.describe Setting, type: :model do
      end
    end

    context 'records includes nothing' do
    context 'when records includes nothing' do
      let(:records) { [] }

      context 'default_value is not a Hash' do
      context 'when default_value is not a Hash' do
        it 'includes Setting with value of default_value' do
          setting = described_class.all_as_records[key]



@@ 152,7 152,7 @@ RSpec.describe Setting, type: :model do
        end
      end

      context 'default_value is a Hash' do
      context 'when default_value is a Hash' do
        let(:default_value) { { 'foo' => 'fuga' } }

        it 'returns {}' do


@@ 169,7 169,7 @@ RSpec.describe Setting, type: :model do
      allow(RailsSettings::Default).to receive(:enabled?).and_return(enabled)
    end

    context 'RailsSettings::Default.enabled? is false' do
    context 'when RailsSettings::Default.enabled? is false' do
      let(:enabled) { false }

      it 'returns {}' do


@@ 177,7 177,7 @@ RSpec.describe Setting, type: :model do
      end
    end

    context 'RailsSettings::Settings.enabled? is true' do
    context 'when RailsSettings::Settings.enabled? is true' do
      let(:enabled) { true }

      it 'returns instance of RailsSettings::Default' do

M spec/models/site_upload_spec.rb => spec/models/site_upload_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe SiteUpload, type: :model do
RSpec.describe SiteUpload do
  describe '#cache_key' do
    let(:site_upload) { SiteUpload.new(var: 'var') }


M spec/models/status_pin_spec.rb => spec/models/status_pin_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe StatusPin, type: :model do
RSpec.describe StatusPin do
  describe 'validations' do
    it 'allows pins of own statuses' do
      account = Fabricate(:account)

M spec/models/status_spec.rb => spec/models/status_spec.rb +9 -9
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Status, type: :model do
RSpec.describe Status do
  subject { Fabricate(:status, account: alice) }

  let(:alice) { Fabricate(:account, username: 'alice') }


@@ 49,22 49,22 @@ RSpec.describe Status, type: :model do
  end

  describe '#verb' do
    context 'if destroyed?' do
    context 'when destroyed?' do
      it 'returns :delete' do
        subject.destroy!
        expect(subject.verb).to be :delete
      end
    end

    context 'unless destroyed?' do
      context 'if reblog?' do
    context 'when not destroyed?' do
      context 'when reblog?' do
        it 'returns :share' do
          subject.reblog = other
          expect(subject.verb).to be :share
        end
      end

      context 'unless reblog?' do
      context 'when not reblog?' do
        it 'returns :post' do
          subject.reblog = nil
          expect(subject.verb).to be :post


@@ 85,28 85,28 @@ RSpec.describe Status, type: :model do
  end

  describe '#hidden?' do
    context 'if private_visibility?' do
    context 'when private_visibility?' do
      it 'returns true' do
        subject.visibility = :private
        expect(subject.hidden?).to be true
      end
    end

    context 'if direct_visibility?' do
    context 'when direct_visibility?' do
      it 'returns true' do
        subject.visibility = :direct
        expect(subject.hidden?).to be true
      end
    end

    context 'if public_visibility?' do
    context 'when public_visibility?' do
      it 'returns false' do
        subject.visibility = :public
        expect(subject.hidden?).to be false
      end
    end

    context 'if unlisted_visibility?' do
    context 'when unlisted_visibility?' do
      it 'returns false' do
        subject.visibility = :unlisted
        expect(subject.hidden?).to be false

M spec/models/status_stat_spec.rb => spec/models/status_stat_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe StatusStat, type: :model do
RSpec.describe StatusStat do
end

M spec/models/status_trend_spec.rb => spec/models/status_trend_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe StatusTrend, type: :model do
RSpec.describe StatusTrend do
end

M spec/models/system_key_spec.rb => spec/models/system_key_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe SystemKey, type: :model do
RSpec.describe SystemKey do
end

M spec/models/tag_follow_spec.rb => spec/models/tag_follow_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe TagFollow, type: :model do
RSpec.describe TagFollow do
end

M spec/models/unavailable_domain_spec.rb => spec/models/unavailable_domain_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe UnavailableDomain, type: :model do
RSpec.describe UnavailableDomain do
end

M spec/models/user_invite_request_spec.rb => spec/models/user_invite_request_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe UserInviteRequest, type: :model do
RSpec.describe UserInviteRequest do
end

M spec/models/user_role_spec.rb => spec/models/user_role_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe UserRole, type: :model do
RSpec.describe UserRole do
  subject { described_class.create(name: 'Foo', position: 1) }

  describe '#can?' do

M spec/models/user_spec.rb => spec/models/user_spec.rb +1 -1
@@ 3,7 3,7 @@
require 'rails_helper'
require 'devise_two_factor/spec_helpers'

RSpec.describe User, type: :model do
RSpec.describe User do
  let(:password) { 'abcd1234' }
  let(:account) { Fabricate(:account, username: 'alice') }


M spec/models/web/push_subscription_spec.rb => spec/models/web/push_subscription_spec.rb +5 -5
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Web::PushSubscription, type: :model do
RSpec.describe Web::PushSubscription do
  subject { described_class.new(data: data) }

  let(:account) { Fabricate(:account) }


@@ 56,7 56,7 @@ RSpec.describe Web::PushSubscription, type: :model do
    context 'when policy is followed' do
      let(:policy) { 'followed' }

      context 'and notification is from someone you follow' do
      context 'when notification is from someone you follow' do
        before do
          account.follow!(notification.from_account)
        end


@@ 66,7 66,7 @@ RSpec.describe Web::PushSubscription, type: :model do
        end
      end

      context 'and notification is not from someone you follow' do
      context 'when notification is not from someone you follow' do
        it 'returns false' do
          expect(subject.pushable?(notification)).to be false
        end


@@ 76,7 76,7 @@ RSpec.describe Web::PushSubscription, type: :model do
    context 'when policy is follower' do
      let(:policy) { 'follower' }

      context 'and notification is from someone who follows you' do
      context 'when notification is from someone who follows you' do
        before do
          notification.from_account.follow!(account)
        end


@@ 86,7 86,7 @@ RSpec.describe Web::PushSubscription, type: :model do
        end
      end

      context 'and notification is not from someone who follows you' do
      context 'when notification is not from someone who follows you' do
        it 'returns false' do
          expect(subject.pushable?(notification)).to be false
        end

M spec/models/web/setting_spec.rb => spec/models/web/setting_spec.rb +1 -1
@@ 2,5 2,5 @@

require 'rails_helper'

RSpec.describe Web::Setting, type: :model do
RSpec.describe Web::Setting do
end

M spec/models/webauthn_credentials_spec.rb => spec/models/webauthn_credentials_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe WebauthnCredential, type: :model do
RSpec.describe WebauthnCredential do
  describe 'validations' do
    it 'is invalid without an external id' do
      webauthn_credential = Fabricate.build(:webauthn_credential, external_id: nil)

M spec/models/webhook_spec.rb => spec/models/webhook_spec.rb +1 -1
@@ 2,7 2,7 @@

require 'rails_helper'

RSpec.describe Webhook, type: :model do
RSpec.describe Webhook do
  let(:webhook) { Fabricate(:webhook) }

  describe '#rotate_secret!' do

M spec/policies/account_moderation_note_policy_spec.rb => spec/policies/account_moderation_note_policy_spec.rb +5 -5
@@ 9,13 9,13 @@ RSpec.describe AccountModerationNotePolicy do
  let(:john)    { Fabricate(:account) }

  permissions :create? do
    context 'staff' do
    context 'when staff' do
      it 'grants to create' do
        expect(subject).to permit(admin, AccountModerationNotePolicy)
      end
    end

    context 'not staff' do
    context 'when not staff' do
      it 'denies to create' do
        expect(subject).to_not permit(john, AccountModerationNotePolicy)
      end


@@ 29,19 29,19 @@ RSpec.describe AccountModerationNotePolicy do
                target_account: Fabricate(:account))
    end

    context 'admin' do
    context 'when admin' do
      it 'grants to destroy' do
        expect(subject).to permit(admin, account_moderation_note)
      end
    end

    context 'owner' do
    context 'when owner' do
      it 'grants to destroy' do
        expect(subject).to permit(john, account_moderation_note)
      end
    end

    context 'neither admin nor owner' do
    context 'when neither admin nor owner' do
      let(:kevin) { Fabricate(:account) }

      it 'denies to destroy' do

M spec/policies/account_policy_spec.rb => spec/policies/account_policy_spec.rb +20 -20
@@ 10,13 10,13 @@ RSpec.describe AccountPolicy do
  let(:alice)   { Fabricate(:account) }

  permissions :index? do
    context 'staff' do
    context 'when staff' do
      it 'permits' do
        expect(subject).to permit(admin)
      end
    end

    context 'not staff' do
    context 'when not staff' do
      it 'denies' do
        expect(subject).to_not permit(john)
      end


@@ 24,13 24,13 @@ RSpec.describe AccountPolicy do
  end

  permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do
    context 'staff' do
    context 'when staff' do
      it 'permits' do
        expect(subject).to permit(admin, alice)
      end
    end

    context 'not staff' do
    context 'when not staff' do
      it 'denies' do
        expect(subject).to_not permit(john, alice)
      end


@@ 42,13 42,13 @@ RSpec.describe AccountPolicy do
      alice.suspend!
    end

    context 'staff' do
    context 'when staff' do
      it 'permits' do
        expect(subject).to permit(admin, alice)
      end
    end

    context 'not staff' do
    context 'when not staff' do
      it 'denies' do
        expect(subject).to_not permit(john, alice)
      end


@@ 56,13 56,13 @@ RSpec.describe AccountPolicy do
  end

  permissions :redownload? do
    context 'admin' do
    context 'when admin' do
      it 'permits' do
        expect(subject).to permit(admin)
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john)
      end


@@ 72,21 72,21 @@ RSpec.describe AccountPolicy do
  permissions :suspend?, :silence? do
    let(:staff) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }

    context 'staff' do
      context 'record is staff' do
    context 'when staff' do
      context 'when record is staff' do
        it 'denies' do
          expect(subject).to_not permit(admin, staff)
        end
      end

      context 'record is not staff' do
      context 'when record is not staff' do
        it 'permits' do
          expect(subject).to permit(admin, john)
        end
      end
    end

    context 'not staff' do
    context 'when not staff' do
      it 'denies' do
        expect(subject).to_not permit(john, Account)
      end


@@ 96,21 96,21 @@ RSpec.describe AccountPolicy do
  permissions :memorialize? do
    let(:other_admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }

    context 'admin' do
      context 'record is admin' do
    context 'when admin' do
      context 'when record is admin' do
        it 'denies' do
          expect(subject).to_not permit(admin, other_admin)
        end
      end

      context 'record is not admin' do
      context 'when record is not admin' do
        it 'permits' do
          expect(subject).to permit(admin, john)
        end
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john, Account)
      end


@@ 118,13 118,13 @@ RSpec.describe AccountPolicy do
  end

  permissions :review? do
    context 'admin' do
    context 'when admin' do
      it 'permits' do
        expect(subject).to permit(admin)
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john)
      end


@@ 132,7 132,7 @@ RSpec.describe AccountPolicy do
  end

  permissions :destroy? do
    context 'admin' do
    context 'when admin' do
      context 'with a temporarily suspended account' do
        before { allow(alice).to receive(:suspended_temporarily?).and_return(true) }



@@ 150,7 150,7 @@ RSpec.describe AccountPolicy do
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john, alice)
      end

M spec/policies/backup_policy_spec.rb => spec/policies/backup_policy_spec.rb +5 -5
@@ 8,20 8,20 @@ RSpec.describe BackupPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :create? do
    context 'not user_signed_in?' do
    context 'when not user_signed_in?' do
      it 'denies' do
        expect(subject).to_not permit(nil, Backup)
      end
    end

    context 'user_signed_in?' do
      context 'no backups' do
    context 'when user_signed_in?' do
      context 'with no backups' do
        it 'permits' do
          expect(subject).to permit(john, Backup)
        end
      end

      context 'backups are too old' do
      context 'when backups are too old' do
        it 'permits' do
          travel(-8.days) do
            Fabricate(:backup, user: john.user)


@@ 31,7 31,7 @@ RSpec.describe BackupPolicy do
        end
      end

      context 'backups are newer' do
      context 'when backups are newer' do
        it 'denies' do
          travel(-3.days) do
            Fabricate(:backup, user: john.user)

M spec/policies/custom_emoji_policy_spec.rb => spec/policies/custom_emoji_policy_spec.rb +4 -4
@@ 9,13 9,13 @@ RSpec.describe CustomEmojiPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :index?, :enable?, :disable? do
    context 'staff' do
    context 'when staff' do
      it 'permits' do
        expect(subject).to permit(admin, CustomEmoji)
      end
    end

    context 'not staff' do
    context 'when not staff' do
      it 'denies' do
        expect(subject).to_not permit(john, CustomEmoji)
      end


@@ 23,13 23,13 @@ RSpec.describe CustomEmojiPolicy do
  end

  permissions :create?, :update?, :copy?, :destroy? do
    context 'admin' do
    context 'when admin' do
      it 'permits' do
        expect(subject).to permit(admin, CustomEmoji)
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john, CustomEmoji)
      end

M spec/policies/domain_block_policy_spec.rb => spec/policies/domain_block_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe DomainBlockPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :index?, :show?, :create?, :destroy? do
    context 'admin' do
    context 'when admin' do
      it 'permits' do
        expect(subject).to permit(admin, DomainBlock)
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john, DomainBlock)
      end

M spec/policies/email_domain_block_policy_spec.rb => spec/policies/email_domain_block_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe EmailDomainBlockPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :index?, :show?, :create?, :destroy? do
    context 'admin' do
    context 'when admin' do
      it 'permits' do
        expect(subject).to permit(admin, EmailDomainBlock)
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john, EmailDomainBlock)
      end

M spec/policies/instance_policy_spec.rb => spec/policies/instance_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe InstancePolicy do
  let(:john)    { Fabricate(:account) }

  permissions :index?, :show?, :destroy? do
    context 'admin' do
    context 'when admin' do
      it 'permits' do
        expect(subject).to permit(admin, Instance)
      end
    end

    context 'not admin' do
    context 'when not admin' do
      it 'denies' do
        expect(subject).to_not permit(john, Instance)
      end

M spec/policies/invite_policy_spec.rb => spec/policies/invite_policy_spec.rb +9 -9
@@ 9,7 9,7 @@ RSpec.describe InvitePolicy do
  let(:john)    { Fabricate(:user).account }

  permissions :index? do
    context 'staff?' do
    context 'when staff?' do
      it 'permits' do
        expect(subject).to permit(admin, Invite)
      end


@@ 17,7 17,7 @@ RSpec.describe InvitePolicy do
  end

  permissions :create? do
    context 'has privilege' do
    context 'with privilege' do
      before do
        UserRole.everyone.update(permissions: UserRole::FLAGS[:invite_users])
      end


@@ 27,7 27,7 @@ RSpec.describe InvitePolicy do
      end
    end

    context 'does not have privilege' do
    context 'when does not have privilege' do
      before do
        UserRole.everyone.update(permissions: UserRole::Flags::NONE)
      end


@@ 39,13 39,13 @@ RSpec.describe InvitePolicy do
  end

  permissions :deactivate_all? do
    context 'admin?' do
    context 'when admin?' do
      it 'permits' do
        expect(subject).to permit(admin, Invite)
      end
    end

    context 'not admin?' do
    context 'when not admin?' do
      it 'denies' do
        expect(subject).to_not permit(john, Invite)
      end


@@ 53,20 53,20 @@ RSpec.describe InvitePolicy do
  end

  permissions :destroy? do
    context 'owner?' do
    context 'when owner?' do
      it 'permits' do
        expect(subject).to permit(john, Fabricate(:invite, user: john.user))
      end
    end

    context 'not owner?' do
      context 'admin?' do
    context 'when not owner?' do
      context 'when admin?' do
        it 'permits' do
          expect(subject).to permit(admin, Fabricate(:invite))
        end
      end

      context 'not admin?' do
      context 'when not admin?' do
        it 'denies' do
          expect(subject).to_not permit(john, Fabricate(:invite))
        end

M spec/policies/relay_policy_spec.rb => spec/policies/relay_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe RelayPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :update? do
    context 'admin?' do
    context 'when admin?' do
      it 'permits' do
        expect(subject).to permit(admin, Relay)
      end
    end

    context '!admin?' do
    context 'with !admin?' do
      it 'denies' do
        expect(subject).to_not permit(john, Relay)
      end

M spec/policies/report_note_policy_spec.rb => spec/policies/report_note_policy_spec.rb +6 -6
@@ 9,13 9,13 @@ RSpec.describe ReportNotePolicy do
  let(:john)    { Fabricate(:account) }

  permissions :create? do
    context 'staff?' do
    context 'when staff?' do
      it 'permits' do
        expect(subject).to permit(admin, ReportNote)
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, ReportNote)
      end


@@ 23,22 23,22 @@ RSpec.describe ReportNotePolicy do
  end

  permissions :destroy? do
    context 'admin?' do
    context 'when admin?' do
      it 'permit' do
        report_note = Fabricate(:report_note, account: john)
        expect(subject).to permit(admin, report_note)
      end
    end

    context 'admin?' do
      context 'owner?' do
    context 'when admin?' do
      context 'when owner?' do
        it 'permit' do
          report_note = Fabricate(:report_note, account: john)
          expect(subject).to permit(john, report_note)
        end
      end

      context '!owner?' do
      context 'with !owner?' do
        it 'denies' do
          report_note = Fabricate(:report_note)
          expect(subject).to_not permit(john, report_note)

M spec/policies/report_policy_spec.rb => spec/policies/report_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe ReportPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :update?, :index?, :show? do
    context 'staff?' do
    context 'when staff?' do
      it 'permits' do
        expect(subject).to permit(admin, Report)
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, Report)
      end

M spec/policies/settings_policy_spec.rb => spec/policies/settings_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe SettingsPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :update?, :show?, :destroy? do
    context 'admin?' do
    context 'when admin?' do
      it 'permits' do
        expect(subject).to permit(admin, Settings)
      end
    end

    context '!admin?' do
    context 'with !admin?' do
      it 'denies' do
        expect(subject).to_not permit(john, Settings)
      end

M spec/policies/tag_policy_spec.rb => spec/policies/tag_policy_spec.rb +2 -2
@@ 9,13 9,13 @@ RSpec.describe TagPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :index?, :show?, :update?, :review? do
    context 'staff?' do
    context 'when staff?' do
      it 'permits' do
        expect(subject).to permit(admin, Tag)
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, Tag)
      end

M spec/policies/user_policy_spec.rb => spec/policies/user_policy_spec.rb +18 -18
@@ 9,21 9,21 @@ RSpec.describe UserPolicy do
  let(:john)    { Fabricate(:account) }

  permissions :reset_password?, :change_email? do
    context 'staff?' do
      context '!record.staff?' do
    context 'when staff?' do
      context 'with !record.staff?' do
        it 'permits' do
          expect(subject).to permit(admin, john.user)
        end
      end

      context 'record.staff?' do
      context 'when record.staff?' do
        it 'denies' do
          expect(subject).to_not permit(admin, admin.user)
        end
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, User)
      end


@@ 31,21 31,21 @@ RSpec.describe UserPolicy do
  end

  permissions :disable_2fa? do
    context 'admin?' do
      context '!record.staff?' do
    context 'when admin?' do
      context 'with !record.staff?' do
        it 'permits' do
          expect(subject).to permit(admin, john.user)
        end
      end

      context 'record.staff?' do
      context 'when record.staff?' do
        it 'denies' do
          expect(subject).to_not permit(admin, admin.user)
        end
      end
    end

    context '!admin?' do
    context 'with !admin?' do
      it 'denies' do
        expect(subject).to_not permit(john, User)
      end


@@ 53,15 53,15 @@ RSpec.describe UserPolicy do
  end

  permissions :confirm? do
    context 'staff?' do
      context '!record.confirmed?' do
    context 'when staff?' do
      context 'with !record.confirmed?' do
        it 'permits' do
          john.user.update(confirmed_at: nil)
          expect(subject).to permit(admin, john.user)
        end
      end

      context 'record.confirmed?' do
      context 'when record.confirmed?' do
        it 'denies' do
          john.user.confirm!
          expect(subject).to_not permit(admin, john.user)


@@ 69,7 69,7 @@ RSpec.describe UserPolicy do
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, User)
      end


@@ 77,13 77,13 @@ RSpec.describe UserPolicy do
  end

  permissions :enable? do
    context 'staff?' do
    context 'when staff?' do
      it 'permits' do
        expect(subject).to permit(admin, User)
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, User)
      end


@@ 91,21 91,21 @@ RSpec.describe UserPolicy do
  end

  permissions :disable? do
    context 'staff?' do
      context '!record.admin?' do
    context 'when staff?' do
      context 'with !record.admin?' do
        it 'permits' do
          expect(subject).to permit(admin, john.user)
        end
      end

      context 'record.admin?' do
      context 'when record.admin?' do
        it 'denies' do
          expect(subject).to_not permit(admin, admin.user)
        end
      end
    end

    context '!staff?' do
    context 'with !staff?' do
      it 'denies' do
        expect(subject).to_not permit(john, User)
      end

M spec/presenters/account_relationships_presenter_spec.rb => spec/presenters/account_relationships_presenter_spec.rb +8 -8
@@ 19,7 19,7 @@ RSpec.describe AccountRelationshipsPresenter do
    let(:account_ids)        { [Fabricate(:account).id] }
    let(:default_map)        { { 1 => true } }

    context 'options are not set' do
    context 'when options are not set' do
      let(:options) { {} }

      it 'sets default maps' do


@@ 32,7 32,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:following_map] is set' do
    context 'when options[:following_map] is set' do
      let(:options) { { following_map: { 2 => true } } }

      it 'sets @following merged with default_map and options[:following_map]' do


@@ 40,7 40,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:followed_by_map] is set' do
    context 'when options[:followed_by_map] is set' do
      let(:options) { { followed_by_map: { 3 => true } } }

      it 'sets @followed_by merged with default_map and options[:followed_by_map]' do


@@ 48,7 48,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:blocking_map] is set' do
    context 'when options[:blocking_map] is set' do
      let(:options) { { blocking_map: { 4 => true } } }

      it 'sets @blocking merged with default_map and options[:blocking_map]' do


@@ 56,7 56,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:muting_map] is set' do
    context 'when options[:muting_map] is set' do
      let(:options) { { muting_map: { 5 => true } } }

      it 'sets @muting merged with default_map and options[:muting_map]' do


@@ 64,7 64,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:requested_map] is set' do
    context 'when options[:requested_map] is set' do
      let(:options) { { requested_map: { 6 => true } } }

      it 'sets @requested merged with default_map and options[:requested_map]' do


@@ 72,7 72,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:requested_by_map] is set' do
    context 'when options[:requested_by_map] is set' do
      let(:options) { { requested_by_map: { 6 => true } } }

      it 'sets @requested merged with default_map and options[:requested_by_map]' do


@@ 80,7 80,7 @@ RSpec.describe AccountRelationshipsPresenter do
      end
    end

    context 'options[:domain_blocking_map] is set' do
    context 'when options[:domain_blocking_map] is set' do
      let(:options) { { domain_blocking_map: { 7 => true } } }

      it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do

M spec/presenters/status_relationships_presenter_spec.rb => spec/presenters/status_relationships_presenter_spec.rb +6 -6
@@ 18,7 18,7 @@ RSpec.describe StatusRelationshipsPresenter do
    let(:status_ids)         { statuses.map(&:id) + statuses.map(&:reblog_of_id).compact }
    let(:default_map)        { { 1 => true } }

    context 'options are not set' do
    context 'when options are not set' do
      let(:options) { {} }

      it 'sets default maps' do


@@ 30,7 30,7 @@ RSpec.describe StatusRelationshipsPresenter do
      end
    end

    context 'options[:reblogs_map] is set' do
    context 'when options[:reblogs_map] is set' do
      let(:options) { { reblogs_map: { 2 => true } } }

      it 'sets @reblogs_map merged with default_map and options[:reblogs_map]' do


@@ 38,7 38,7 @@ RSpec.describe StatusRelationshipsPresenter do
      end
    end

    context 'options[:favourites_map] is set' do
    context 'when options[:favourites_map] is set' do
      let(:options) { { favourites_map: { 3 => true } } }

      it 'sets @favourites_map merged with default_map and options[:favourites_map]' do


@@ 46,7 46,7 @@ RSpec.describe StatusRelationshipsPresenter do
      end
    end

    context 'options[:bookmarks_map] is set' do
    context 'when options[:bookmarks_map] is set' do
      let(:options) { { bookmarks_map: { 4 => true } } }

      it 'sets @bookmarks_map merged with default_map and options[:bookmarks_map]' do


@@ 54,7 54,7 @@ RSpec.describe StatusRelationshipsPresenter do
      end
    end

    context 'options[:mutes_map] is set' do
    context 'when options[:mutes_map] is set' do
      let(:options) { { mutes_map: { 5 => true } } }

      it 'sets @mutes_map merged with default_map and options[:mutes_map]' do


@@ 62,7 62,7 @@ RSpec.describe StatusRelationshipsPresenter do
      end
    end

    context 'options[:pins_map] is set' do
    context 'when options[:pins_map] is set' do
      let(:options) { { pins_map: { 6 => true } } }

      it 'sets @pins_map merged with default_map and options[:pins_map]' do

M spec/rails_helper.rb => spec/rails_helper.rb +24 -2
@@ 12,7 12,7 @@ require 'paperclip/matchers'
require 'capybara/rspec'
require 'chewy/rspec'

Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!
WebMock.disable_net_connect!(allow: Chewy.settings[:host])


@@ 35,8 35,28 @@ Devise::Test::ControllerHelpers.module_eval do
  end
end

module SignedRequestHelpers
  def get(path, headers: nil, sign_with: nil, **args)
    return super path, headers: headers, **args if sign_with.nil?

    headers ||= {}
    headers['Date'] = Time.now.utc.httpdate
    headers['Host'] = ENV.fetch('LOCAL_DOMAIN')
    signed_headers = headers.merge('(request-target)' => "get #{path}").slice('(request-target)', 'Host', 'Date')

    key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with)
    keypair = sign_with.keypair
    signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
    signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))

    headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""

    super path, headers: headers, **args
  end
end

RSpec.configure do |config|
  config.fixture_path = "#{Rails.root}/spec/fixtures"
  config.fixture_path = Rails.root.join('spec', 'fixtures')
  config.use_transactional_fixtures = true
  config.order = 'random'
  config.infer_spec_type_from_file_location!


@@ 46,10 66,12 @@ RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :helper
  config.include Devise::Test::ControllerHelpers, type: :view
  config.include Devise::Test::IntegrationHelpers, type: :feature
  config.include Devise::Test::IntegrationHelpers, type: :request
  config.include Paperclip::Shoulda::Matchers
  config.include ActiveSupport::Testing::TimeHelpers
  config.include Chewy::Rspec::Helpers
  config.include Redisable
  config.include SignedRequestHelpers, type: :request

  config.before :each, type: :feature do
    https = ENV['LOCAL_HTTPS'] == 'true'

A spec/requests/cache_spec.rb => spec/requests/cache_spec.rb +685 -0
@@ 0,0 1,685 @@
# frozen_string_literal: true

require 'rails_helper'

module TestEndpoints
  # Endpoints that do not include authorization-dependent results
  # and should be cacheable no matter what.
  ALWAYS_CACHED = %w(
    /.well-known/host-meta
    /.well-known/nodeinfo
    /nodeinfo/2.0
    /manifest
    /custom.css
    /actor
    /api/v1/instance/extended_description
    /api/v1/instance/rules
    /api/v1/instance/peers
    /api/v1/instance
    /api/v2/instance
  ).freeze

  # Endpoints that should be cachable when accessed anonymously but have a Vary
  # on Cookie to prevent logged-in users from getting values from logged-out cache.
  COOKIE_DEPENDENT_CACHABLE = %w(
    /
    /explore
    /public
    /about
    /privacy-policy
    /directory
    /@alice
    /@alice/110224538612341312
  ).freeze

  # Endpoints that should be cachable when accessed anonymously but have a Vary
  # on Authorization to prevent logged-in users from getting values from logged-out cache.
  AUTHORIZATION_DEPENDENT_CACHABLE = %w(
    /api/v1/accounts/lookup?acct=alice
    /api/v1/statuses/110224538612341312
    /api/v1/statuses/110224538612341312/context
    /api/v1/polls/12345
    /api/v1/trends/statuses
    /api/v1/directory
  ).freeze

  # Private status that should only be returned with to a valid signature from
  # a specific user.
  # Should never be cached.
  REQUIRE_SIGNATURE = %w(
    /users/alice/statuses/110224538643211312
  ).freeze

  # Pages only available to logged-in users.
  # Should never be cached.
  REQUIRE_LOGIN = %w(
    /settings/preferences/appearance
    /settings/profile
    /settings/featured_tags
    /settings/export
    /relationships
    /filters
    /statuses_cleanup
    /auth/edit
    /oauth/authorized_applications
    /admin/dashboard
  ).freeze

  # API endpoints only available to logged-in users.
  # Should never be cached.
  REQUIRE_TOKEN = %w(
    /api/v1/announcements
    /api/v1/timelines/home
    /api/v1/notifications
    /api/v1/bookmarks
    /api/v1/favourites
    /api/v1/follow_requests
    /api/v1/conversations
    /api/v1/statuses/110224538643211312
    /api/v1/statuses/110224538643211312/context
    /api/v1/lists
    /api/v2/filters
  ).freeze

  # Pages that are only shown to logged-out users, and should never get cached
  # because of CSRF protection.
  REQUIRE_LOGGED_OUT = %w(
    /invite/abcdef
    /auth/sign_in
    /auth/sign_up
    /auth/password/new
    /auth/confirmation/new
  ).freeze

  # Non-exhaustive list of endpoints that feature language-dependent results
  # and thus need to have a Vary on Accept-Language
  LANGUAGE_DEPENDENT = %w(
    /
    /explore
    /about
    /api/v1/trends/statuses
  ).freeze

  module AuthorizedFetch
    # Endpoints that require a signature with AUTHORIZED_FETCH and LIMITED_FEDERATION_MODE
    # and thus should not be cached in those modes.
    REQUIRE_SIGNATURE = %w(
      /users/alice
    ).freeze
  end

  module DisabledAnonymousAPI
    # Endpoints that require a signature with DISALLOW_UNAUTHENTICATED_API_ACCESS
    # and thus should not be cached in this mode.
    REQUIRE_TOKEN = %w(
      /api/v1/custom_emojis
    ).freeze
  end
end

describe 'Caching behavior' do
  shared_examples 'cachable response' do
    it 'does not set cookies' do
      expect(response.cookies).to be_empty
    end

    it 'sets public cache control' do
      # expect(response.cache_control[:max_age]&.to_i).to be_positive
      expect(response.cache_control[:public]).to be_truthy
      expect(response.cache_control[:private]).to be_falsy
      expect(response.cache_control[:no_store]).to be_falsy
      expect(response.cache_control[:no_cache]).to be_falsy
    end
  end

  shared_examples 'non-cacheable response' do
    it 'sets private cache control' do
      expect(response.cache_control[:private]).to be_truthy
      expect(response.cache_control[:no_store]).to be_truthy
    end
  end

  shared_examples 'non-cacheable error' do
    it 'does not return HTTP success' do
      expect(response).to_not have_http_status(200)
    end

    it 'does not have cache headers' do
      expect(response.cache_control[:public]).to be_falsy
    end
  end

  shared_examples 'language-dependent' do
    it 'has a Vary on Accept-Language' do
      expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept-language')
    end
  end

  # Enable CSRF protection like it is in production, as it can cause cookies
  # to be set and thus mess with cache.
  around do |example|
    old = ActionController::Base.allow_forgery_protection
    ActionController::Base.allow_forgery_protection = true

    example.run

    ActionController::Base.allow_forgery_protection = old
  end

  let(:alice) { Fabricate(:account, username: 'alice') }
  let(:user)  { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) }

  before do
    # rubocop:disable Style/NumericLiterals
    status = Fabricate(:status, account: alice, id: 110224538612341312)
    Fabricate(:status, account: alice, id: 110224538643211312, visibility: :private)
    Fabricate(:invite, code: 'abcdef')
    Fabricate(:poll, status: status, account: alice, id: 12345)
    # rubocop:enable Style/NumericLiterals

    user.account.follow!(alice)
  end

  context 'when anonymously accessed' do
    TestEndpoints::ALWAYS_CACHED.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'cachable response'
        it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
      end
    end

    TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'cachable response'

        it 'has a Vary on Cookie' do
          expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie')
        end

        it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
      end
    end

    TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'cachable response'

        it 'has a Vary on Authorization' do
          expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization')
        end

        it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
      end
    end

    TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'non-cacheable response'
      end
    end

    (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::REQUIRE_LOGIN + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'non-cacheable error'
      end
    end

    describe '/api/v1/instance/domain_blocks' do
      around do |example|
        old_setting = Setting.show_domain_blocks
        Setting.show_domain_blocks = show_domain_blocks

        example.run

        Setting.show_domain_blocks = old_setting
      end

      before { get '/api/v1/instance/domain_blocks' }

      context 'when set to be publicly-available' do
        let(:show_domain_blocks) { 'all' }

        it_behaves_like 'cachable response'
      end

      context 'when allowed for local users only' do
        let(:show_domain_blocks) { 'users' }

        it_behaves_like 'non-cacheable error'
      end

      context 'when disabled' do
        let(:show_domain_blocks) { 'disabled' }

        it_behaves_like 'non-cacheable error'
      end
    end
  end

  context 'when logged in' do
    before do
      sign_in user, scope: :user

      # Unfortunately, devise's `sign_in` helper causes the `session` to be
      # loaded in the next request regardless of whether it's actually accessed
      # by the client code.
      #
      # So, we make an extra query to clear issue a session cookie instead.
      #
      # A less resource-intensive way to deal with that would be to generate the
      # session cookie manually, but this seems pretty involved.
      get '/'
    end

    TestEndpoints::ALWAYS_CACHED.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'cachable response'
        it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
      end
    end

    TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'non-cacheable response'

        it 'has a Vary on Cookie' do
          expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie')
        end
      end
    end

    TestEndpoints::REQUIRE_LOGIN.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'non-cacheable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end
    end

    TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
      describe endpoint do
        before { get endpoint }

        it_behaves_like 'non-cacheable error'
      end
    end
  end

  context 'with an auth token' do
    let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }

    TestEndpoints::ALWAYS_CACHED.each do |endpoint|
      describe endpoint do
        before do
          get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
        end

        it_behaves_like 'cachable response'
        it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
      end
    end

    TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
      describe endpoint do
        before do
          get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
        end

        it_behaves_like 'non-cacheable response'

        it 'has a Vary on Authorization' do
          expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization')
        end
      end
    end

    (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
      describe endpoint do
        before do
          get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
        end

        it_behaves_like 'non-cacheable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end
    end

    describe '/api/v1/instance/domain_blocks' do
      around do |example|
        old_setting = Setting.show_domain_blocks
        Setting.show_domain_blocks = show_domain_blocks

        example.run

        Setting.show_domain_blocks = old_setting
      end

      before do
        get '/api/v1/instance/domain_blocks', headers: { 'Authorization' => "Bearer #{token.token}" }
      end

      context 'when set to be publicly-available' do
        let(:show_domain_blocks) { 'all' }

        it_behaves_like 'cachable response'
      end

      context 'when allowed for local users only' do
        let(:show_domain_blocks) { 'users' }

        it_behaves_like 'non-cacheable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end

      context 'when disabled' do
        let(:show_domain_blocks) { 'disabled' }

        it_behaves_like 'non-cacheable error'
      end
    end
  end

  context 'with a Signature header' do
    let(:remote_actor)    { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
    let(:dummy_signature) { 'dummy-signature' }

    before do
      remote_actor.follow!(alice)
    end

    describe '/actor' do
      before do
        get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
      end

      it_behaves_like 'cachable response'

      it 'returns HTTP success' do
        expect(response).to have_http_status(200)
      end
    end

    TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint|
      describe endpoint do
        before do
          get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
        end

        it_behaves_like 'non-cacheable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end
    end
  end

  context 'when enabling AUTHORIZED_FETCH mode' do
    around do |example|
      ClimateControl.modify AUTHORIZED_FETCH: 'true' do
        example.run
      end
    end

    context 'when not providing a Signature' do
      describe '/actor' do
        before do
          get '/actor', headers: { 'Accept' => 'application/activity+json' }
        end

        it_behaves_like 'cachable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end

      (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
        describe endpoint do
          before do
            get endpoint, headers: { 'Accept' => 'application/activity+json' }
          end

          it_behaves_like 'non-cacheable error'
        end
      end
    end

    context 'when providing a Signature' do
      let(:remote_actor)    { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
      let(:dummy_signature) { 'dummy-signature' }

      before do
        remote_actor.follow!(alice)
      end

      describe '/actor' do
        before do
          get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
        end

        it_behaves_like 'cachable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end

      (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
        describe endpoint do
          before do
            get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
          end

          it_behaves_like 'non-cacheable response'

          it 'returns HTTP success' do
            expect(response).to have_http_status(200)
          end
        end
      end
    end
  end

  context 'when enabling LIMITED_FEDERATION_MODE mode' do
    around do |example|
      ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do
        old_whitelist_mode = Rails.configuration.x.whitelist_mode
        Rails.configuration.x.whitelist_mode = true

        example.run

        Rails.configuration.x.whitelist_mode = old_whitelist_mode
      end
    end

    context 'when not providing a Signature' do
      describe '/actor' do
        before do
          get '/actor', headers: { 'Accept' => 'application/activity+json' }
        end

        it_behaves_like 'cachable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end

      (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
        describe endpoint do
          before do
            get endpoint, headers: { 'Accept' => 'application/activity+json' }
          end

          it_behaves_like 'non-cacheable error'
        end
      end
    end

    context 'when providing a Signature from an allowed domain' do
      let(:remote_actor)    { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
      let(:dummy_signature) { 'dummy-signature' }

      before do
        DomainAllow.create!(domain: remote_actor.domain)
        remote_actor.follow!(alice)
      end

      describe '/actor' do
        before do
          get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
        end

        it_behaves_like 'cachable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end

      (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
        describe endpoint do
          before do
            get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
          end

          it_behaves_like 'non-cacheable response'

          it 'returns HTTP success' do
            expect(response).to have_http_status(200)
          end
        end
      end
    end

    context 'when providing a Signature from a non-allowed domain' do
      let(:remote_actor)    { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
      let(:dummy_signature) { 'dummy-signature' }

      describe '/actor' do
        before do
          get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
        end

        it_behaves_like 'cachable response'

        it 'returns HTTP success' do
          expect(response).to have_http_status(200)
        end
      end

      (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
        describe endpoint do
          before do
            get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
          end

          it_behaves_like 'non-cacheable error'
        end
      end
    end
  end

  context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
    around do |example|
      ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
        example.run
      end
    end

    context 'when anonymously accessed' do
      TestEndpoints::ALWAYS_CACHED.each do |endpoint|
        describe endpoint do
          before { get endpoint }

          it_behaves_like 'cachable response'
          it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
        end
      end

      TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
        describe endpoint do
          before { get endpoint }

          it_behaves_like 'non-cacheable response'
        end
      end

      (TestEndpoints::REQUIRE_TOKEN + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
        describe endpoint do
          before { get endpoint }

          it_behaves_like 'non-cacheable error'
        end
      end
    end

    context 'with an auth token' do
      let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }

      TestEndpoints::ALWAYS_CACHED.each do |endpoint|
        describe endpoint do
          before do
            get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
          end

          it_behaves_like 'cachable response'
          it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
        end
      end

      TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
        describe endpoint do
          before do
            get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
          end

          it_behaves_like 'non-cacheable response'

          it 'has a Vary on Authorization' do
            expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization')
          end
        end
      end

      (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
        describe endpoint do
          before do
            get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
          end

          it_behaves_like 'non-cacheable response'

          it 'returns HTTP success' do
            expect(response).to have_http_status(200)
          end
        end
      end
    end
  end
end

M spec/requests/link_headers_spec.rb => spec/requests/link_headers_spec.rb +2 -2
@@ 13,7 13,7 @@ describe 'Link headers' do
    it 'contains webfinger url in link header' do
      link_header = link_header_with_type('application/jrd+json')

      expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
      expect(link_header.href).to eq 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
      expect(link_header.attr_pairs.first).to eq %w(rel lrdd)
    end



@@ 26,7 26,7 @@ describe 'Link headers' do

    def link_header_with_type(type)
      LinkHeader.parse(response.headers['Link'].to_s).links.find do |link|
        link.attr_pairs.any? { |pair| pair == ['type', type] }
        link.attr_pairs.any?(['type', type])
      end
    end
  end

M spec/services/account_search_service_spec.rb => spec/services/account_search_service_spec.rb +1 -1
@@ 20,7 20,7 @@ describe AccountSearchService, type: :service do
      end
    end

    context 'searching for a simple term that is not an exact match' do
    context 'when searching for a simple term that is not an exact match' do
      it 'does not return a nil entry in the array for the exact match' do
        account = Fabricate(:account, username: 'matchingusername')
        results = subject.call('match', nil, limit: 5)

M spec/services/account_statuses_cleanup_service_spec.rb => spec/services/account_statuses_cleanup_service_spec.rb +2 -2
@@ 20,13 20,13 @@ describe AccountStatusesCleanupService, type: :service do
      let!(:another_old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) }
      let!(:recent_status)      { Fabricate(:status, created_at: 1.day.ago, account: account) }

      context 'given a budget of 1' do
      context 'when given a budget of 1' do
        it 'reports 1 deleted toot' do
          expect(subject.call(account_policy, 1)).to eq 1
        end
      end

      context 'given a normal budget of 10' do
      context 'when given a normal budget of 10' do
        it 'reports 3 deleted statuses' do
          expect(subject.call(account_policy, 10)).to eq 3
        end

M spec/services/activitypub/fetch_remote_status_service_spec.rb => spec/services/activitypub/fetch_remote_status_service_spec.rb +3 -3
@@ 226,12 226,12 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
    end
  end

  context 'statuses referencing other statuses' do
  context 'with statuses referencing other statuses' do
    before do
      stub_const 'ActivityPub::FetchRemoteStatusService::DISCOVERIES_PER_REQUEST', 5
    end

    context 'using inReplyTo' do
    context 'when using inReplyTo' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',


@@ 267,7 267,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
      end
    end

    context 'using replies' do
    context 'when using replies' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',

M spec/services/activitypub/process_account_service_spec.rb => spec/services/activitypub/process_account_service_spec.rb +5 -5
@@ 5,7 5,7 @@ require 'rails_helper'
RSpec.describe ActivityPub::ProcessAccountService, type: :service do
  subject { described_class.new }

  context 'property values' do
  context 'with property values' do
    let(:payload) do
      {
        id: 'https://foo.test',


@@ 82,7 82,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
      account.suspend!(origin: suspension_origin)
    end

    context 'locally' do
    context 'when locally' do
      let(:suspension_origin) { :local }

      it 'does not unsuspend it' do


@@ 94,7 94,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
      end
    end

    context 'remotely' do
    context 'when remotely' do
      let(:suspension_origin) { :remote }

      it 'unsuspends it' do


@@ 112,7 112,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
    end
  end

  context 'discovering many subdomains in a short timeframe' do
  context 'when discovering many subdomains in a short timeframe' do
    before do
      stub_const 'ActivityPub::ProcessAccountService::SUBDOMAINS_RATELIMIT', 5
    end


@@ 138,7 138,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
    end
  end

  context 'accounts referencing other accounts' do
  context 'when Accounts referencing other accounts' do
    before do
      stub_const 'ActivityPub::ProcessAccountService::DISCOVERIES_PER_REQUEST', 5
    end

M spec/services/activitypub/process_status_update_service_spec.rb => spec/services/activitypub/process_status_update_service_spec.rb +8 -8
@@ 269,7 269,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally without tags' do
    context 'when originally without tags' do
      before do
        subject.call(status, json)
      end


@@ 279,7 279,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally with tags' do
    context 'when originally with tags' do
      let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] }

      let(:payload) do


@@ 305,7 305,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally without mentions' do
    context 'when originally without mentions' do
      before do
        subject.call(status, json)
      end


@@ 315,7 315,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally with mentions' do
    context 'when originally with mentions' do
      let(:mentions) { [alice, bob] }

      before do


@@ 327,7 327,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally without media attachments' do
    context 'when originally without media attachments' do
      before do
        stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png'))
        subject.call(status, json)


@@ 362,7 362,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally with media attachments' do
    context 'when originally with media attachments' do
      let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }

      let(:payload) do


@@ 404,7 404,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally with a poll' do
    context 'when originally with a poll' do
      before do
        poll = Fabricate(:poll, status: status)
        status.update(preloadable_poll: poll)


@@ 420,7 420,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
      end
    end

    context 'originally without a poll' do
    context 'when originally without a poll' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',

A spec/services/backup_service_spec.rb => spec/services/backup_service_spec.rb +67 -0
@@ 0,0 1,67 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BackupService, type: :service do
  subject(:service_call) { described_class.new.call(backup) }

  let!(:user)           { Fabricate(:user) }
  let!(:attachment)     { Fabricate(:media_attachment, account: user.account) }
  let!(:status)         { Fabricate(:status, account: user.account, text: 'Hello', visibility: :public, media_attachments: [attachment]) }
  let!(:private_status) { Fabricate(:status, account: user.account, text: 'secret', visibility: :private) }
  let!(:favourite)      { Fabricate(:favourite, account: user.account) }
  let!(:bookmark)       { Fabricate(:bookmark, account: user.account) }
  let!(:backup)         { Fabricate(:backup, user: user) }

  def read_zip_file(backup, filename)
    file = Paperclip.io_adapters.for(backup.dump)
    Zip::File.open(file) do |zipfile|
      entry = zipfile.glob(filename).first
      return entry.get_input_stream.read
    end
  end

  it 'marks the backup as processed' do
    expect { service_call }.to change(backup, :processed).from(false).to(true)
  end

  it 'exports outbox.json as expected' do
    service_call

    json = Oj.load(read_zip_file(backup, 'outbox.json'))
    expect(json['@context']).to_not be_nil
    expect(json['type']).to eq 'OrderedCollection'
    expect(json['totalItems']).to eq 2
    expect(json['orderedItems'][0]['@context']).to be_nil
    expect(json['orderedItems'][0]).to include({
      'type' => 'Create',
      'object' => include({
        'id' => ActivityPub::TagManager.instance.uri_for(status),
        'content' => '<p>Hello</p>',
      }),
    })
    expect(json['orderedItems'][1]).to include({
      'type' => 'Create',
      'object' => include({
        'id' => ActivityPub::TagManager.instance.uri_for(private_status),
        'content' => '<p>secret</p>',
      }),
    })
  end

  it 'exports likes.json as expected' do
    service_call

    json = Oj.load(read_zip_file(backup, 'likes.json'))
    expect(json['type']).to eq 'OrderedCollection'
    expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)]
  end

  it 'exports bookmarks.json as expected' do
    service_call

    json = Oj.load(read_zip_file(backup, 'bookmarks.json'))
    expect(json['type']).to eq 'OrderedCollection'
    expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)]
  end
end

A spec/services/bulk_import_row_service_spec.rb => spec/services/bulk_import_row_service_spec.rb +95 -0
@@ 0,0 1,95 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BulkImportRowService do
  subject { described_class.new }

  let(:account)    { Fabricate(:account) }
  let(:import)     { Fabricate(:bulk_import, account: account, type: import_type) }
  let(:import_row) { Fabricate(:bulk_import_row, bulk_import: import, data: data) }

  describe '#call' do
    context 'when importing a follow' do
      let(:import_type)    { 'following' }
      let(:target_account) { Fabricate(:account) }
      let(:service_double) { instance_double(FollowService, call: nil) }
      let(:data) do
        { 'acct' => target_account.acct }
      end

      before do
        allow(FollowService).to receive(:new).and_return(service_double)
      end

      it 'calls FollowService with the expected arguments and returns true' do
        expect(subject.call(import_row)).to be true

        expect(service_double).to have_received(:call).with(account, target_account, { reblogs: nil, notify: nil, languages: nil })
      end
    end

    context 'when importing a block' do
      let(:import_type)    { 'blocking' }
      let(:target_account) { Fabricate(:account) }
      let(:service_double) { instance_double(BlockService, call: nil) }
      let(:data) do
        { 'acct' => target_account.acct }
      end

      before do
        allow(BlockService).to receive(:new).and_return(service_double)
      end

      it 'calls BlockService with the expected arguments and returns true' do
        expect(subject.call(import_row)).to be true

        expect(service_double).to have_received(:call).with(account, target_account)
      end
    end

    context 'when importing a mute' do
      let(:import_type)    { 'muting' }
      let(:target_account) { Fabricate(:account) }
      let(:service_double) { instance_double(MuteService, call: nil) }
      let(:data) do
        { 'acct' => target_account.acct }
      end

      before do
        allow(MuteService).to receive(:new).and_return(service_double)
      end

      it 'calls MuteService with the expected arguments and returns true' do
        expect(subject.call(import_row)).to be true

        expect(service_double).to have_received(:call).with(account, target_account, { notifications: nil })
      end
    end

    context 'when importing a bookmark' do
      let(:import_type) { 'bookmarks' }
      let(:data) do
        { 'uri' => ActivityPub::TagManager.instance.uri_for(target_status) }
      end

      context 'when the status is public' do
        let(:target_status) { Fabricate(:status) }

        it 'bookmarks the status and returns true' do
          expect(subject.call(import_row)).to be true
          expect(account.bookmarked?(target_status)).to be true
        end
      end

      context 'when the status is not accessible to the user' do
        let(:target_status) { Fabricate(:status, visibility: :direct) }

        it 'does not bookmark the status and returns false' do
          expect(subject.call(import_row)).to be false
          expect(account.bookmarked?(target_status)).to be false
        end
      end
    end
  end
end

A spec/services/bulk_import_service_spec.rb => spec/services/bulk_import_service_spec.rb +417 -0
@@ 0,0 1,417 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BulkImportService do
  subject { described_class.new }

  let(:account) { Fabricate(:account) }
  let(:import) { Fabricate(:bulk_import, account: account, type: import_type, overwrite: overwrite, state: :in_progress, imported_items: 0, processed_items: 0) }

  before do
    import.update(total_items: import.rows.count)
  end

  describe '#call' do
    around do |example|
      Sidekiq::Testing.fake! do
        example.run
        Sidekiq::Worker.clear_all
      end
    end

    context 'when importing follows' do
      let(:import_type) { 'following' }
      let(:overwrite)   { false }

      let!(:rows) do
        [
          { 'acct' => 'user@foo.bar' },
          { 'acct' => 'unknown@unknown.bar' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.follow!(Fabricate(:account))
      end

      it 'does not immediately change who the account follows' do
        expect { subject.call(import) }.to_not(change { account.reload.active_relationships.to_a })
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id))
      end

      it 'requests to follow all the listed users once the workers have run' do
        subject.call(import)

        resolve_account_service_double = double
        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double)
        allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) }
        allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) }

        Import::RowWorker.drain

        expect(FollowRequest.includes(:target_account).where(account: account).map(&:target_account).map(&:acct)).to contain_exactly('user@foo.bar', 'unknown@unknown.bar')
      end
    end

    context 'when importing follows with overwrite' do
      let(:import_type) { 'following' }
      let(:overwrite)   { true }

      let!(:followed)         { Fabricate(:account, username: 'followed', domain: 'foo.bar', protocol: :activitypub) }
      let!(:to_be_unfollowed) { Fabricate(:account, username: 'to_be_unfollowed', domain: 'foo.bar', protocol: :activitypub) }

      let!(:rows) do
        [
          { 'acct' => 'followed@foo.bar', 'show_reblogs' => false, 'notify' => true, 'languages' => ['en'] },
          { 'acct' => 'user@foo.bar' },
          { 'acct' => 'unknown@unknown.bar' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.follow!(followed, reblogs: true, notify: false)
        account.follow!(to_be_unfollowed)
      end

      it 'unfollows user not present on list' do
        subject.call(import)
        expect(account.following?(to_be_unfollowed)).to be false
      end

      it 'updates the existing follow relationship as expected' do
        expect { subject.call(import) }.to change { Follow.where(account: account, target_account: followed).pick(:show_reblogs, :notify, :languages) }.from([true, false, nil]).to([false, true, ['en']])
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id))
      end

      it 'requests to follow all the expected users once the workers have run' do
        subject.call(import)

        resolve_account_service_double = double
        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double)
        allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) }
        allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) }

        Import::RowWorker.drain

        expect(FollowRequest.includes(:target_account).where(account: account).map(&:target_account).map(&:acct)).to contain_exactly('user@foo.bar', 'unknown@unknown.bar')
      end
    end

    context 'when importing blocks' do
      let(:import_type) { 'blocking' }
      let(:overwrite)   { false }

      let!(:rows) do
        [
          { 'acct' => 'user@foo.bar' },
          { 'acct' => 'unknown@unknown.bar' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.block!(Fabricate(:account, username: 'already_blocked', domain: 'remote.org'))
      end

      it 'does not immediately change who the account blocks' do
        expect { subject.call(import) }.to_not(change { account.reload.blocking.to_a })
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id))
      end

      it 'blocks all the listed users once the workers have run' do
        subject.call(import)

        resolve_account_service_double = double
        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double)
        allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) }
        allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) }

        Import::RowWorker.drain

        expect(account.blocking.map(&:acct)).to contain_exactly('already_blocked@remote.org', 'user@foo.bar', 'unknown@unknown.bar')
      end
    end

    context 'when importing blocks with overwrite' do
      let(:import_type) { 'blocking' }
      let(:overwrite)   { true }

      let!(:blocked)         { Fabricate(:account, username: 'blocked', domain: 'foo.bar', protocol: :activitypub) }
      let!(:to_be_unblocked) { Fabricate(:account, username: 'to_be_unblocked', domain: 'foo.bar', protocol: :activitypub) }

      let!(:rows) do
        [
          { 'acct' => 'blocked@foo.bar' },
          { 'acct' => 'user@foo.bar' },
          { 'acct' => 'unknown@unknown.bar' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.block!(blocked)
        account.block!(to_be_unblocked)
      end

      it 'unblocks user not present on list' do
        subject.call(import)
        expect(account.blocking?(to_be_unblocked)).to be false
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id))
      end

      it 'requests to follow all the expected users once the workers have run' do
        subject.call(import)

        resolve_account_service_double = double
        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double)
        allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) }
        allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) }

        Import::RowWorker.drain

        expect(account.blocking.map(&:acct)).to contain_exactly('blocked@foo.bar', 'user@foo.bar', 'unknown@unknown.bar')
      end
    end

    context 'when importing mutes' do
      let(:import_type) { 'muting' }
      let(:overwrite)   { false }

      let!(:rows) do
        [
          { 'acct' => 'user@foo.bar' },
          { 'acct' => 'unknown@unknown.bar' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.mute!(Fabricate(:account, username: 'already_muted', domain: 'remote.org'))
      end

      it 'does not immediately change who the account blocks' do
        expect { subject.call(import) }.to_not(change { account.reload.muting.to_a })
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id))
      end

      it 'mutes all the listed users once the workers have run' do
        subject.call(import)

        resolve_account_service_double = double
        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double)
        allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) }
        allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) }

        Import::RowWorker.drain

        expect(account.muting.map(&:acct)).to contain_exactly('already_muted@remote.org', 'user@foo.bar', 'unknown@unknown.bar')
      end
    end

    context 'when importing mutes with overwrite' do
      let(:import_type) { 'muting' }
      let(:overwrite)   { true }

      let!(:muted)         { Fabricate(:account, username: 'muted', domain: 'foo.bar', protocol: :activitypub) }
      let!(:to_be_unmuted) { Fabricate(:account, username: 'to_be_unmuted', domain: 'foo.bar', protocol: :activitypub) }

      let!(:rows) do
        [
          { 'acct' => 'muted@foo.bar', 'hide_notifications' => true },
          { 'acct' => 'user@foo.bar' },
          { 'acct' => 'unknown@unknown.bar' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.mute!(muted, notifications: false)
        account.mute!(to_be_unmuted)
      end

      it 'updates the existing mute as expected' do
        expect { subject.call(import) }.to change { Mute.where(account: account, target_account: muted).pick(:hide_notifications) }.from(false).to(true)
      end

      it 'unblocks user not present on list' do
        subject.call(import)
        expect(account.muting?(to_be_unmuted)).to be false
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id))
      end

      it 'requests to follow all the expected users once the workers have run' do
        subject.call(import)

        resolve_account_service_double = double
        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double)
        allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) }
        allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) }

        Import::RowWorker.drain

        expect(account.muting.map(&:acct)).to contain_exactly('muted@foo.bar', 'user@foo.bar', 'unknown@unknown.bar')
      end
    end

    context 'when importing domain blocks' do
      let(:import_type) { 'domain_blocking' }
      let(:overwrite)   { false }

      let!(:rows) do
        [
          { 'domain' => 'blocked.com' },
          { 'domain' => 'to_block.com' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.block_domain!('alreadyblocked.com')
        account.block_domain!('blocked.com')
      end

      it 'blocks all the new domains' do
        subject.call(import)
        expect(account.domain_blocks.pluck(:domain)).to contain_exactly('alreadyblocked.com', 'blocked.com', 'to_block.com')
      end

      it 'marks the import as finished' do
        subject.call(import)
        expect(import.reload.finished?).to be true
      end
    end

    context 'when importing domain blocks with overwrite' do
      let(:import_type) { 'domain_blocking' }
      let(:overwrite)   { true }

      let!(:rows) do
        [
          { 'domain' => 'blocked.com' },
          { 'domain' => 'to_block.com' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.block_domain!('alreadyblocked.com')
        account.block_domain!('blocked.com')
      end

      it 'blocks all the new domains' do
        subject.call(import)
        expect(account.domain_blocks.pluck(:domain)).to contain_exactly('blocked.com', 'to_block.com')
      end

      it 'marks the import as finished' do
        subject.call(import)
        expect(import.reload.finished?).to be true
      end
    end

    context 'when importing bookmarks' do
      let(:import_type) { 'bookmarks' }
      let(:overwrite)   { false }

      let!(:already_bookmarked)  { Fabricate(:status, uri: 'https://already.bookmarked/1') }
      let!(:status)              { Fabricate(:status, uri: 'https://foo.bar/posts/1') }
      let!(:inaccessible_status) { Fabricate(:status, uri: 'https://foo.bar/posts/inaccessible', visibility: :direct) }
      let!(:bookmarked)          { Fabricate(:status, uri: 'https://foo.bar/posts/already-bookmarked') }

      let!(:rows) do
        [
          { 'uri' => status.uri },
          { 'uri' => inaccessible_status.uri },
          { 'uri' => bookmarked.uri },
          { 'uri' => 'https://domain.unknown/foo' },
          { 'uri' => 'https://domain.unknown/private' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.bookmarks.create!(status: already_bookmarked)
        account.bookmarks.create!(status: bookmarked)
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id))
      end

      it 'updates the bookmarks as expected once the workers have run' do
        subject.call(import)

        service_double = double
        allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double)
        allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') }
        allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) }

        Import::RowWorker.drain

        expect(account.bookmarks.map(&:status).map(&:uri)).to contain_exactly(already_bookmarked.uri, status.uri, bookmarked.uri, 'https://domain.unknown/foo')
      end
    end

    context 'when importing bookmarks with overwrite' do
      let(:import_type) { 'bookmarks' }
      let(:overwrite)   { true }

      let!(:already_bookmarked)  { Fabricate(:status, uri: 'https://already.bookmarked/1') }
      let!(:status)              { Fabricate(:status, uri: 'https://foo.bar/posts/1') }
      let!(:inaccessible_status) { Fabricate(:status, uri: 'https://foo.bar/posts/inaccessible', visibility: :direct) }
      let!(:bookmarked)          { Fabricate(:status, uri: 'https://foo.bar/posts/already-bookmarked') }

      let!(:rows) do
        [
          { 'uri' => status.uri },
          { 'uri' => inaccessible_status.uri },
          { 'uri' => bookmarked.uri },
          { 'uri' => 'https://domain.unknown/foo' },
          { 'uri' => 'https://domain.unknown/private' },
        ].map { |data| import.rows.create!(data: data) }
      end

      before do
        account.bookmarks.create!(status: already_bookmarked)
        account.bookmarks.create!(status: bookmarked)
      end

      it 'enqueues workers for the expected rows' do
        subject.call(import)
        expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id))
      end

      it 'updates the bookmarks as expected once the workers have run' do
        subject.call(import)

        service_double = double
        allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double)
        allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') }
        allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) }

        Import::RowWorker.drain

        expect(account.bookmarks.map(&:status).map(&:uri)).to contain_exactly(status.uri, bookmarked.uri, 'https://domain.unknown/foo')
      end
    end
  end
end

M spec/services/fetch_link_card_service_spec.rb => spec/services/fetch_link_card_service_spec.rb +2 -2
@@ 18,7 18,7 @@ RSpec.describe FetchLinkCardService, type: :service do
    subject.call(status)
  end

  context 'in a local status' do
  context 'with a local status' do
    context do
      let(:status) { Fabricate(:status, text: 'Check out http://example.中国') }



@@ 89,7 89,7 @@ RSpec.describe FetchLinkCardService, type: :service do
    end
  end

  context 'in a remote status' do
  context 'with a remote status' do
    let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu <a>foo</a> #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen?   Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }

    it 'parses out URLs' do

M spec/services/fetch_oembed_service_spec.rb => spec/services/fetch_oembed_service_spec.rb +6 -6
@@ 39,7 39,7 @@ describe FetchOEmbedService, type: :service do
        end
      end

      context 'Both of JSON and XML provider are discoverable' do
      context 'when both of JSON and XML provider are discoverable' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,


@@ 66,7 66,7 @@ describe FetchOEmbedService, type: :service do
        end
      end

      context 'JSON provider is discoverable while XML provider is not' do
      context 'when JSON provider is discoverable while XML provider is not' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,


@@ 87,7 87,7 @@ describe FetchOEmbedService, type: :service do
        end
      end

      context 'XML provider is discoverable while JSON provider is not' do
      context 'when XML provider is discoverable while JSON provider is not' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,


@@ 108,7 108,7 @@ describe FetchOEmbedService, type: :service do
        end
      end

      context 'Invalid XML provider is discoverable while JSON provider is not' do
      context 'with Invalid XML provider is discoverable while JSON provider is not' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,


@@ 122,7 122,7 @@ describe FetchOEmbedService, type: :service do
        end
      end

      context 'Neither of JSON and XML provider is discoverable' do
      context 'with neither of JSON and XML provider is discoverable' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,


@@ 136,7 136,7 @@ describe FetchOEmbedService, type: :service do
        end
      end

      context 'Empty JSON provider is discoverable' do
      context 'when empty JSON provider is discoverable' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,

M spec/services/fetch_remote_status_service_spec.rb => spec/services/fetch_remote_status_service_spec.rb +1 -1
@@ 16,7 16,7 @@ RSpec.describe FetchRemoteStatusService, type: :service do
    }
  end

  context 'protocol is :activitypub' do
  context 'when protocol is :activitypub' do
    subject { described_class.new.call(note[:id], prefetched_body: prefetched_body) }

    let(:prefetched_body) { Oj.dump(note) }

M spec/services/follow_service_spec.rb => spec/services/follow_service_spec.rb +2 -2
@@ 7,7 7,7 @@ RSpec.describe FollowService, type: :service do

  let(:sender) { Fabricate(:account, username: 'alice') }

  context 'local account' do
  context 'when local account' do
    describe 'locked account' do
      let(:bob) { Fabricate(:account, locked: true, username: 'bob') }



@@ 138,7 138,7 @@ RSpec.describe FollowService, type: :service do
    end
  end

  context 'remote ActivityPub account' do
  context 'when remote ActivityPub account' do
    let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }

    before do

M spec/services/import_service_spec.rb => spec/services/import_service_spec.rb +6 -6
@@ 13,7 13,7 @@ RSpec.describe ImportService, type: :service do
    stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
  end

  context 'import old-style list of muted users' do
  context 'when importing old-style list of muted users' do
    subject { ImportService.new }

    let(:csv) { attachment_fixture('mute-imports.txt') }


@@ 51,7 51,7 @@ RSpec.describe ImportService, type: :service do
    end
  end

  context 'import new-style list of muted users' do
  context 'when importing new-style list of muted users' do
    subject { ImportService.new }

    let(:csv) { attachment_fixture('new-mute-imports.txt') }


@@ 92,7 92,7 @@ RSpec.describe ImportService, type: :service do
    end
  end

  context 'import old-style list of followed users' do
  context 'when importing old-style list of followed users' do
    subject { ImportService.new }

    let(:csv) { attachment_fixture('mute-imports.txt') }


@@ 134,7 134,7 @@ RSpec.describe ImportService, type: :service do
    end
  end

  context 'import new-style list of followed users' do
  context 'when importing new-style list of followed users' do
    subject { ImportService.new }

    let(:csv) { attachment_fixture('new-following-imports.txt') }


@@ 181,7 181,7 @@ RSpec.describe ImportService, type: :service do
  # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
  #
  # https://github.com/mastodon/mastodon/issues/20571
  context 'utf-8 encoded domains' do
  context 'with a utf-8 encoded domains' do
    subject { ImportService.new }

    let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') }


@@ 200,7 200,7 @@ RSpec.describe ImportService, type: :service do
    end
  end

  context 'import bookmarks' do
  context 'when importing bookmarks' do
    subject { ImportService.new }

    let(:csv) { attachment_fixture('bookmark-imports.txt') }

M spec/services/notify_service_spec.rb => spec/services/notify_service_spec.rb +6 -6
@@ 49,7 49,7 @@ RSpec.describe NotifyService, type: :service do
    expect { subject }.to_not change(Notification, :count)
  end

  context 'for direct messages' do
  context 'with direct messages' do
    let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
    let(:type)     { :mention }



@@ 58,14 58,14 @@ RSpec.describe NotifyService, type: :service do
      user.save
    end

    context 'if recipient is supposed to be following sender' do
    context 'when recipient is supposed to be following sender' do
      let(:enabled) { true }

      it 'does not notify' do
        expect { subject }.to_not change(Notification, :count)
      end

      context 'if the message chain is initiated by recipient, but is not direct message' do
      context 'when the message chain is initiated by recipient, but is not direct message' do
        let(:reply_to) { Fabricate(:status, account: recipient) }
        let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
        let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }


@@ 75,7 75,7 @@ RSpec.describe NotifyService, type: :service do
        end
      end

      context 'if the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
      context 'when the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
        let(:reply_to) { Fabricate(:status, account: recipient) }
        let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
        let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) }


@@ 86,7 86,7 @@ RSpec.describe NotifyService, type: :service do
        end
      end

      context 'if the message chain is initiated by the recipient with a mention to the sender' do
      context 'when the message chain is initiated by the recipient with a mention to the sender' do
        let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
        let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
        let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }


@@ 97,7 97,7 @@ RSpec.describe NotifyService, type: :service do
      end
    end

    context 'if recipient is NOT supposed to be following sender' do
    context 'when recipient is NOT supposed to be following sender' do
      let(:enabled) { false }

      it 'does notify' do

M spec/services/process_mentions_service_spec.rb => spec/services/process_mentions_service_spec.rb +4 -4
@@ 33,10 33,10 @@ RSpec.describe ProcessMentionsService, type: :service do
    end
  end

  context 'resolving a mention to a remote account' do
  context 'with resolving a mention to a remote account' do
    let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: :public) }

    context 'ActivityPub' do
    context 'with ActivityPub' do
      context do
        let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }



@@ 49,7 49,7 @@ RSpec.describe ProcessMentionsService, type: :service do
        end
      end

      context 'mentioning a user several times when not saving records' do
      context 'when mentioning a user several times when not saving records' do
        let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
        let(:status)       { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct} @#{remote_user.acct} @#{remote_user.acct}", visibility: :public) }



@@ 89,7 89,7 @@ RSpec.describe ProcessMentionsService, type: :service do
      end
    end

    context 'Temporarily-unreachable ActivityPub user' do
    context 'with a Temporarily-unreachable ActivityPub user' do
      let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) }

      before do

M spec/services/reblog_service_spec.rb => spec/services/reblog_service_spec.rb +2 -2
@@ 5,7 5,7 @@ require 'rails_helper'
RSpec.describe ReblogService, type: :service do
  let(:alice)  { Fabricate(:account, username: 'alice') }

  context 'creates a reblog with appropriate visibility' do
  context 'when creates a reblog with appropriate visibility' do
    subject { ReblogService.new }

    let(:visibility)        { :public }


@@ 61,7 61,7 @@ RSpec.describe ReblogService, type: :service do
    end
  end

  context 'ActivityPub' do
  context 'with ActivityPub' do
    subject { ReblogService.new }

    let(:bob)    { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }

M spec/services/report_service_spec.rb => spec/services/report_service_spec.rb +1 -1
@@ 7,7 7,7 @@ RSpec.describe ReportService, type: :service do

  let(:source_account) { Fabricate(:account) }

  context 'for a remote account' do
  context 'with a remote account' do
    let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }

    before do

M spec/services/resolve_account_service_spec.rb => spec/services/resolve_account_service_spec.rb +4 -4
@@ 11,11 11,11 @@ RSpec.describe ResolveAccountService, type: :service do
    stub_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com').to_return(request_fixture('activitypub-webfinger.txt'))
    stub_request(:get, 'https://ap.example.com/users/foo').to_return(request_fixture('activitypub-actor.txt'))
    stub_request(:get, 'https://ap.example.com/users/foo.atom').to_return(request_fixture('activitypub-feed.txt'))
    stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
    stub_request(:get, %r{https://ap\.example\.com/users/foo/\w+}).to_return(status: 404)
    stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410)
  end

  context 'using skip_webfinger' do
  context 'when using skip_webfinger' do
    context 'when account is known' do
      let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') }



@@ 78,7 78,7 @@ RSpec.describe ResolveAccountService, type: :service do
  end

  context 'when webfinger returns http gone' do
    context 'for a previously known account' do
    context 'with a previously known account' do
      before do
        Fabricate(:account, username: 'hoge', domain: 'example.com', last_webfingered_at: nil)
        allow(AccountDeletionWorker).to receive(:perform_async)


@@ 94,7 94,7 @@ RSpec.describe ResolveAccountService, type: :service do
      end
    end

    context 'for a previously unknown account' do
    context 'with a previously unknown account' do
      it 'returns nil' do
        expect(subject.call('hoge@example.com')).to be_nil
      end

M spec/services/resolve_url_service_spec.rb => spec/services/resolve_url_service_spec.rb +3 -3
@@ 30,7 30,7 @@ describe ResolveURLService, type: :service do
      expect(subject.call(url)).to eq known_account
    end

    context 'searching for a remote private status' do
    context 'when searching for a remote private status' do
      let(:account)  { Fabricate(:account) }
      let(:poster)   { Fabricate(:account, domain: 'example.com') }
      let(:url)      { 'https://example.com/@foo/42' }


@@ 95,7 95,7 @@ describe ResolveURLService, type: :service do
      end
    end

    context 'searching for a local private status' do
    context 'when searching for a local private status' do
      let(:account) { Fabricate(:account) }
      let(:poster)  { Fabricate(:account) }
      let!(:status) { Fabricate(:status, account: poster, visibility: :private) }


@@ 127,7 127,7 @@ describe ResolveURLService, type: :service do
      end
    end

    context 'searching for a link that redirects to a local public status' do
    context 'when searching for a link that redirects to a local public status' do
      let(:account) { Fabricate(:account) }
      let(:poster)  { Fabricate(:account) }
      let!(:status) { Fabricate(:status, account: poster, visibility: :public) }

M spec/services/search_service_spec.rb => spec/services/search_service_spec.rb +5 -5
@@ 23,7 23,7 @@ describe SearchService, type: :service do
        @query = 'http://test.host/query'
      end

      context 'that does not find anything' do
      context 'when it does not find anything' do
        it 'returns the empty results' do
          service = double(call: nil)
          allow(ResolveURLService).to receive(:new).and_return(service)


@@ 34,7 34,7 @@ describe SearchService, type: :service do
        end
      end

      context 'that finds an account' do
      context 'when it finds an account' do
        it 'includes the account in the results' do
          account = Account.new
          service = double(call: account)


@@ 46,7 46,7 @@ describe SearchService, type: :service do
        end
      end

      context 'that finds a status' do
      context 'when it finds a status' do
        it 'includes the status in the results' do
          status = Status.new
          service = double(call: status)


@@ 60,7 60,7 @@ describe SearchService, type: :service do
    end

    describe 'with a non-url query' do
      context 'that matches an account' do
      context 'when it matches an account' do
        it 'includes the account in the results' do
          query = 'username'
          account = Account.new


@@ 73,7 73,7 @@ describe SearchService, type: :service do
        end
      end

      context 'that matches a tag' do
      context 'when it matches a tag' do
        it 'includes the tag in the results' do
          query = '#tag'
          tag = Tag.new

M spec/services/unallow_domain_service_spec.rb => spec/services/unallow_domain_service_spec.rb +1 -1
@@ 12,7 12,7 @@ RSpec.describe UnallowDomainService, type: :service do
  let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) }
  let!(:domain_allow) { Fabricate(:domain_allow, domain: 'evil.org') }

  context 'in limited federation mode' do
  context 'with limited federation mode' do
    before do
      allow(subject).to receive(:whitelist_mode?).and_return(true)
    end

M spec/services/verify_link_service_spec.rb => spec/services/verify_link_service_spec.rb +2 -2
@@ 5,7 5,7 @@ require 'rails_helper'
RSpec.describe VerifyLinkService, type: :service do
  subject { described_class.new }

  context 'given a local account' do
  context 'when given a local account' do
    let(:account) { Fabricate(:account, username: 'alice') }
    let(:field)   { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') }



@@ 129,7 129,7 @@ RSpec.describe VerifyLinkService, type: :service do
    end
  end

  context 'given a remote account' do
  context 'when given a remote account' do
    let(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://profile.example.com/alice') }
    let(:field)   { Account::Field.new(account, 'name' => 'Website', 'value' => '<a href="http://example.com" rel="me"><span class="invisible">http://</span><span class="">example.com</span><span class="invisible"></span></a>') }


M spec/spec_helper.rb => spec/spec_helper.rb +1 -1
@@ 38,7 38,7 @@ RSpec.configure do |config|

  config.after :suite do
    gc_counter = 0
    FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"])
    FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')])
  end

  config.after :each do

M spec/validators/disallowed_hashtags_validator_spec.rb => spec/validators/disallowed_hashtags_validator_spec.rb +2 -2
@@ 14,7 14,7 @@ RSpec.describe DisallowedHashtagsValidator, type: :validator do
    let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| "##{x}" }.join(' ')) }
    let(:errors) { double(add: nil) }

    context 'for a remote reblog' do
    context 'with a remote reblog' do
      let(:local)  { false }
      let(:reblog) { true }



@@ 23,7 23,7 @@ RSpec.describe DisallowedHashtagsValidator, type: :validator do
      end
    end

    context 'for a local original status' do
    context 'with a local original status' do
      let(:local)  { true }
      let(:reblog) { false }


M spec/validators/email_mx_validator_spec.rb => spec/validators/email_mx_validator_spec.rb +1 -1
@@ 6,7 6,7 @@ describe EmailMxValidator do
  describe '#validate' do
    let(:user) { double(email: 'foo@example.com', sign_up_ip: '1.2.3.4', errors: double(add: nil)) }

    context 'for an e-mail domain that is explicitly allowed' do
    context 'with an e-mail domain that is explicitly allowed' do
      around do |block|
        tmp = Rails.configuration.x.email_domains_whitelist
        Rails.configuration.x.email_domains_whitelist = 'example.com'

M spec/validators/follow_limit_validator_spec.rb => spec/validators/follow_limit_validator_spec.rb +4 -4
@@ 18,7 18,7 @@ RSpec.describe FollowLimitValidator, type: :validator do
    let(:_nil)    { true }
    let(:local)   { false }

    context 'follow.account.nil? || !follow.account.local?' do
    context 'with follow.account.nil? || !follow.account.local?' do
      let(:_nil)    { true }

      it 'not calls errors.add' do


@@ 26,11 26,11 @@ RSpec.describe FollowLimitValidator, type: :validator do
      end
    end

    context '!(follow.account.nil? || !follow.account.local?)' do
    context 'with !(follow.account.nil? || !follow.account.local?)' do
      let(:_nil)    { false }
      let(:local)   { true }

      context 'limit_reached?' do
      context 'when limit_reached?' do
        let(:limit_reached) { true }

        it 'calls errors.add' do


@@ 39,7 39,7 @@ RSpec.describe FollowLimitValidator, type: :validator do
        end
      end

      context '!limit_reached?' do
      context 'with !limit_reached?' do
        let(:limit_reached) { false }

        it 'not calls errors.add' do

M spec/validators/poll_validator_spec.rb => spec/validators/poll_validator_spec.rb +1 -1
@@ 18,7 18,7 @@ RSpec.describe PollValidator, type: :validator do
      expect(errors).to_not have_received(:add)
    end

    context 'expires just 5 min ago' do
    context 'when expires is just 5 min ago' do
      let(:expires_at) { 5.minutes.from_now }

      it 'not calls errors add' do

M spec/validators/status_pin_validator_spec.rb => spec/validators/status_pin_validator_spec.rb +4 -4
@@ 20,7 20,7 @@ RSpec.describe StatusPinValidator, type: :validator do
    let(:reblog)      { false }
    let(:count)       { 0 }

    context 'pin.status.reblog?' do
    context 'when pin.status.reblog?' do
      let(:reblog) { true }

      it 'calls errors.add' do


@@ 28,7 28,7 @@ RSpec.describe StatusPinValidator, type: :validator do
      end
    end

    context 'pin.account_id != pin.status.account_id' do
    context 'when pin.account_id != pin.status.account_id' do
      let(:pin_account_id)    { 1 }
      let(:status_account_id) { 2 }



@@ 37,7 37,7 @@ RSpec.describe StatusPinValidator, type: :validator do
      end
    end

    context 'if pin.status.direct_visibility?' do
    context 'when pin.status.direct_visibility?' do
      let(:visibility) { 'direct' }

      it 'calls errors.add' do


@@ 45,7 45,7 @@ RSpec.describe StatusPinValidator, type: :validator do
      end
    end

    context 'pin.account.status_pins.count > 4 && pin.account.local?' do
    context 'when pin.account.status_pins.count > 4 && pin.account.local?' do
      let(:count) { 5 }
      let(:local) { true }


M spec/validators/unreserved_username_validator_spec.rb => spec/validators/unreserved_username_validator_spec.rb +4 -4
@@ 13,7 13,7 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
    let(:account)   { double(username: username, errors: errors) }
    let(:errors) { double(add: nil) }

    context '@username.blank?' do
    context 'when @username is blank?' do
      let(:username) { nil }

      it 'not calls errors.add' do


@@ 21,10 21,10 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
      end
    end

    context '!@username.blank?' do
    context 'when @username is not blank?' do
      let(:username) { 'f' }

      context 'reserved_username?' do
      context 'with reserved_username?' do
        let(:reserved_username) { true }

        it 'calls errors.add' do


@@ 32,7 32,7 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
        end
      end

      context '!reserved_username?' do
      context 'when username is not reserved' do
        let(:reserved_username) { false }

        it 'not calls errors.add' do

M spec/validators/url_validator_spec.rb => spec/validators/url_validator_spec.rb +2 -2
@@ 15,7 15,7 @@ RSpec.describe URLValidator, type: :validator do
    let(:value)     { '' }
    let(:attribute) { :foo }

    context 'unless compliant?' do
    context 'when not compliant?' do
      let(:compliant) { false }

      it 'calls errors.add' do


@@ 23,7 23,7 @@ RSpec.describe URLValidator, type: :validator do
      end
    end

    context 'if compliant?' do
    context 'when compliant?' do
      let(:compliant) { true }

      it 'not calls errors.add' do

A spec/workers/bulk_import_worker_spec.rb => spec/workers/bulk_import_worker_spec.rb +26 -0
@@ 0,0 1,26 @@
# frozen_string_literal: true

require 'rails_helper'

describe BulkImportWorker do
  subject { described_class.new }

  let(:import) { Fabricate(:bulk_import, state: :scheduled) }

  describe '#perform' do
    let(:service_double) { instance_double(BulkImportService, call: nil) }

    before do
      allow(BulkImportService).to receive(:new).and_return(service_double)
    end

    it 'changes the import\'s state as appropriate' do
      expect { subject.perform(import.id) }.to change { import.reload.state.to_sym }.from(:scheduled).to(:in_progress)
    end

    it 'calls BulkImportService' do
      subject.perform(import.id)
      expect(service_double).to have_received(:call).with(import)
    end
  end
end

A spec/workers/import/row_worker_spec.rb => spec/workers/import/row_worker_spec.rb +127 -0
@@ 0,0 1,127 @@
# frozen_string_literal: true

require 'rails_helper'

describe Import::RowWorker do
  subject { described_class.new }

  let(:row) { Fabricate(:bulk_import_row, bulk_import: import) }

  describe '#perform' do
    before do
      allow(BulkImportRowService).to receive(:new).and_return(service_double)
    end

    shared_examples 'clean failure' do
      let(:service_double) { instance_double(BulkImportRowService, call: false) }

      it 'calls BulkImportRowService' do
        subject.perform(row.id)
        expect(service_double).to have_received(:call).with(row)
      end

      it 'increases the number of processed items' do
        expect { subject.perform(row.id) }.to(change { import.reload.processed_items }.by(+1))
      end

      it 'does not increase the number of imported items' do
        expect { subject.perform(row.id) }.to_not(change { import.reload.imported_items })
      end

      it 'does not delete the row' do
        subject.perform(row.id)
        expect(BulkImportRow.exists?(row.id)).to be true
      end
    end

    shared_examples 'unclean failure' do
      let(:service_double) { instance_double(BulkImportRowService) }

      before do
        allow(service_double).to receive(:call) do
          raise 'dummy error'
        end
      end

      it 'raises an error and does not change processed items count' do
        expect { subject.perform(row.id) }.to raise_error(StandardError, 'dummy error').and(not_change { import.reload.processed_items })
      end

      it 'does not delete the row' do
        expect { subject.perform(row.id) }.to raise_error(StandardError, 'dummy error').and(not_change { BulkImportRow.exists?(row.id) })
      end
    end

    shared_examples 'clean success' do
      let(:service_double) { instance_double(BulkImportRowService, call: true) }

      it 'calls BulkImportRowService' do
        subject.perform(row.id)
        expect(service_double).to have_received(:call).with(row)
      end

      it 'increases the number of processed items' do
        expect { subject.perform(row.id) }.to(change { import.reload.processed_items }.by(+1))
      end

      it 'increases the number of imported items' do
        expect { subject.perform(row.id) }.to(change { import.reload.imported_items }.by(+1))
      end

      it 'deletes the row' do
        expect { subject.perform(row.id) }.to change { BulkImportRow.exists?(row.id) }.from(true).to(false)
      end
    end

    context 'when there are multiple rows to process' do
      let(:import) { Fabricate(:bulk_import, total_items: 2, processed_items: 0, imported_items: 0, state: :in_progress) }

      context 'with a clean failure' do
        include_examples 'clean failure'

        it 'does not mark the import as finished' do
          expect { subject.perform(row.id) }.to_not(change { import.reload.state.to_sym })
        end
      end

      context 'with an unclean failure' do
        include_examples 'unclean failure'

        it 'does not mark the import as finished' do
          expect { subject.perform(row.id) }.to raise_error(StandardError).and(not_change { import.reload.state.to_sym })
        end
      end

      context 'with a clean success' do
        include_examples 'clean success'

        it 'does not mark the import as finished' do
          expect { subject.perform(row.id) }.to_not(change { import.reload.state.to_sym })
        end
      end
    end

    context 'when this is the last row to process' do
      let(:import) { Fabricate(:bulk_import, total_items: 2, processed_items: 1, imported_items: 0, state: :in_progress) }

      context 'with a clean failure' do
        include_examples 'clean failure'

        it 'marks the import as finished' do
          expect { subject.perform(row.id) }.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished)
        end
      end

      # NOTE: sidekiq retry logic may be a bit too difficult to test, so leaving this blind spot for now
      it_behaves_like 'unclean failure'

      context 'with a clean success' do
        include_examples 'clean success'

        it 'marks the import as finished' do
          expect { subject.perform(row.id) }.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished)
        end
      end
    end
  end
end

M spec/workers/move_worker_spec.rb => spec/workers/move_worker_spec.rb +81 -30
@@ 5,22 5,28 @@ require 'rails_helper'
describe MoveWorker do
  subject { described_class.new }

  let(:local_follower)   { Fabricate(:account) }
  let(:local_follower)   { Fabricate(:account, domain: nil) }
  let(:blocking_account) { Fabricate(:account) }
  let(:muting_account)   { Fabricate(:account) }
  let(:source_account)   { Fabricate(:account, protocol: :activitypub, domain: 'example.com') }
  let(:target_account)   { Fabricate(:account, protocol: :activitypub, domain: 'example.com') }
  let(:source_account)   { Fabricate(:account, protocol: :activitypub, domain: 'example.com', uri: 'https://example.org/a', inbox_url: 'https://example.org/a/inbox') }
  let(:target_account)   { Fabricate(:account, protocol: :activitypub, domain: 'example.com', uri: 'https://example.org/b', inbox_url: 'https://example.org/b/inbox') }
  let(:local_user)       { Fabricate(:user) }
  let(:comment)          { 'old note prior to move' }
  let!(:account_note)    { Fabricate(:account_note, account: local_user.account, target_account: source_account, comment: comment) }
  let(:list)             { Fabricate(:list, account: local_follower) }

  let(:block_service) { double }

  before do
    stub_request(:post, 'https://example.org/a/inbox').to_return(status: 200)
    stub_request(:post, 'https://example.org/b/inbox').to_return(status: 200)

    local_follower.follow!(source_account)
    blocking_account.block!(source_account)
    muting_account.mute!(source_account)

    list.accounts << source_account

    allow(BlockService).to receive(:new).and_return(block_service)
    allow(block_service).to receive(:call)
  end


@@ 86,56 92,101 @@ describe MoveWorker do
    end
  end

  context 'both accounts are distant' do
    describe 'perform' do
      it 'calls UnfollowFollowWorker' do
        expect_push_bulk_to_match(UnfollowFollowWorker, [[local_follower.id, source_account.id, target_account.id, false]])
        subject.perform(source_account.id, target_account.id)
  shared_examples 'lists handling' do
    it 'puts the new account on the list' do
      subject.perform(source_account.id, target_account.id)
      expect(list.accounts.include?(target_account)).to be true
    end

    it 'does not create invalid list memberships' do
      subject.perform(source_account.id, target_account.id)
      expect(ListAccount.all).to all be_valid
    end
  end

  shared_examples 'common tests' do
    include_examples 'user note handling'
    include_examples 'block and mute handling'
    include_examples 'followers count handling'
    include_examples 'lists handling'

    context 'when a local user already follows both source and target' do
      before do
        local_follower.request_follow!(target_account)
      end

      include_examples 'user note handling'
      include_examples 'block and mute handling'
      include_examples 'followers count handling'
    end
  end
      include_examples 'lists handling'

  context 'target account is local' do
    let(:target_account) { Fabricate(:account) }
      context 'when the local user already has the target in a list' do
        before do
          list.accounts << target_account
        end

    describe 'perform' do
      it 'calls UnfollowFollowWorker' do
        expect_push_bulk_to_match(UnfollowFollowWorker, [[local_follower.id, source_account.id, target_account.id, true]])
        subject.perform(source_account.id, target_account.id)
        include_examples 'lists handling'
      end
    end

    context 'when a local follower already has a pending request to the target' do
      before do
        local_follower.follow!(target_account)
      end

      include_examples 'user note handling'
      include_examples 'block and mute handling'
      include_examples 'followers count handling'
      include_examples 'lists handling'

      context 'when the local user already has the target in a list' do
        before do
          list.accounts << target_account
        end

        include_examples 'lists handling'
      end
    end
  end

  context 'both target and source accounts are local' do
    let(:target_account) { Fabricate(:account) }
    let(:source_account) { Fabricate(:account) }
  describe '#perform' do
    context 'when both accounts are distant' do
      it 'calls UnfollowFollowWorker' do
        Sidekiq::Testing.fake! do
          subject.perform(source_account.id, target_account.id)
          expect(UnfollowFollowWorker).to have_enqueued_sidekiq_job(local_follower.id, source_account.id, target_account.id, false)
          Sidekiq::Worker.drain_all
        end
      end

      include_examples 'common tests'
    end

    describe 'perform' do
      it 'calls makes local followers follow the target account' do
        subject.perform(source_account.id, target_account.id)
        expect(local_follower.following?(target_account)).to be true
    context 'when target account is local' do
      let(:target_account) { Fabricate(:account) }

      it 'calls UnfollowFollowWorker' do
        Sidekiq::Testing.fake! do
          subject.perform(source_account.id, target_account.id)
          expect(UnfollowFollowWorker).to have_enqueued_sidekiq_job(local_follower.id, source_account.id, target_account.id, true)
          Sidekiq::Worker.clear_all
        end
      end

      include_examples 'user note handling'
      include_examples 'block and mute handling'
      include_examples 'followers count handling'
      include_examples 'common tests'
    end

    context 'when both target and source accounts are local' do
      let(:target_account) { Fabricate(:account) }
      let(:source_account) { Fabricate(:account) }

      it 'does not fail when a local user is already following both accounts' do
        double_follower = Fabricate(:account)
        double_follower.follow!(source_account)
        double_follower.follow!(target_account)
      it 'calls makes local followers follow the target account' do
        subject.perform(source_account.id, target_account.id)
        expect(local_follower.following?(target_account)).to be true
      end

      include_examples 'common tests'

      it 'does not allow the moved account to follow themselves' do
        source_account.follow!(target_account)
        subject.perform(source_account.id, target_account.id)

M spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb => spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +30 -3
@@ 75,8 75,8 @@ describe Scheduler::AccountsStatusesCleanupScheduler do
    end
  end

  describe '#get_budget' do
    context 'on a single thread' do
  describe '#compute_budget' do
    context 'with a single thread' do
      let(:process_set_stub) { [{ 'concurrency' => 1, 'queues' => %w(push default) }] }

      it 'returns a low value' do


@@ 84,7 84,7 @@ describe Scheduler::AccountsStatusesCleanupScheduler do
      end
    end

    context 'on a lot of threads' do
    context 'with a lot of threads' do
      let(:process_set_stub) do
        [
          { 'concurrency' => 2, 'queues' => %w(push default) },


@@ 130,6 130,33 @@ describe Scheduler::AccountsStatusesCleanupScheduler do
          .and change { account3.statuses.count }
          .and change { account5.statuses.count }
      end

      context 'when given a big budget' do
        let(:process_set_stub) { [{ 'concurrency' => 400, 'queues' => %w(push default) }] }

        before do
          stub_const 'Scheduler::AccountsStatusesCleanupScheduler::MAX_BUDGET', 400
        end

        it 'correctly handles looping in a single run' do
          expect(subject.compute_budget).to eq(400)
          expect { subject.perform }.to change { Status.count }.by(-30)
        end
      end

      context 'when there is no work to be done' do
        let(:process_set_stub) { [{ 'concurrency' => 400, 'queues' => %w(push default) }] }

        before do
          stub_const 'Scheduler::AccountsStatusesCleanupScheduler::MAX_BUDGET', 400
          subject.perform
        end

        it 'does not get stuck' do
          expect(subject.compute_budget).to eq(400)
          expect { subject.perform }.to_not change { Status.count }
        end
      end
    end
  end
end

M yarn.lock => yarn.lock +307 -296
@@ 31,38 31,38 @@
  dependencies:
    "@babel/highlight" "^7.18.6"

"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f"
  integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.5":
  version "7.21.7"
  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc"
  integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==

"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.21.4", "@babel/core@^7.7.2":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659"
  integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==
"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.21.8", "@babel/core@^7.7.2":
  version "7.21.8"
  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4"
  integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==
  dependencies:
    "@ampproject/remapping" "^2.2.0"
    "@babel/code-frame" "^7.21.4"
    "@babel/generator" "^7.21.4"
    "@babel/helper-compilation-targets" "^7.21.4"
    "@babel/helper-module-transforms" "^7.21.2"
    "@babel/helpers" "^7.21.0"
    "@babel/parser" "^7.21.4"
    "@babel/generator" "^7.21.5"
    "@babel/helper-compilation-targets" "^7.21.5"
    "@babel/helper-module-transforms" "^7.21.5"
    "@babel/helpers" "^7.21.5"
    "@babel/parser" "^7.21.8"
    "@babel/template" "^7.20.7"
    "@babel/traverse" "^7.21.4"
    "@babel/types" "^7.21.4"
    "@babel/traverse" "^7.21.5"
    "@babel/types" "^7.21.5"
    convert-source-map "^1.7.0"
    debug "^4.1.0"
    gensync "^1.0.0-beta.2"
    json5 "^2.2.2"
    semver "^6.3.0"

"@babel/generator@^7.21.4", "@babel/generator@^7.7.2":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc"
  integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==
"@babel/generator@^7.21.5", "@babel/generator@^7.7.2":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f"
  integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==
  dependencies:
    "@babel/types" "^7.21.4"
    "@babel/types" "^7.21.5"
    "@jridgewell/gen-mapping" "^0.3.2"
    "@jridgewell/trace-mapping" "^0.3.17"
    jsesc "^2.5.1"


@@ 90,12 90,12 @@
    "@babel/helper-annotate-as-pure" "^7.18.6"
    "@babel/types" "^7.19.0"

"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.4":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz#770cd1ce0889097ceacb99418ee6934ef0572656"
  integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==
"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366"
  integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==
  dependencies:
    "@babel/compat-data" "^7.21.4"
    "@babel/compat-data" "^7.21.5"
    "@babel/helper-validator-option" "^7.21.0"
    browserslist "^4.21.3"
    lru-cache "^5.1.1"


@@ 162,6 162,11 @@
  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
  integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==

"@babel/helper-environment-visitor@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba"
  integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==

"@babel/helper-explode-assignable-expression@^7.18.6":
  version "7.18.6"
  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"


@@ 213,19 218,19 @@
  dependencies:
    "@babel/types" "^7.21.4"

"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2":
  version "7.21.2"
  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2"
  integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==
"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420"
  integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==
  dependencies:
    "@babel/helper-environment-visitor" "^7.18.9"
    "@babel/helper-module-imports" "^7.18.6"
    "@babel/helper-simple-access" "^7.20.2"
    "@babel/helper-environment-visitor" "^7.21.5"
    "@babel/helper-module-imports" "^7.21.4"
    "@babel/helper-simple-access" "^7.21.5"
    "@babel/helper-split-export-declaration" "^7.18.6"
    "@babel/helper-validator-identifier" "^7.19.1"
    "@babel/template" "^7.20.7"
    "@babel/traverse" "^7.21.2"
    "@babel/types" "^7.21.2"
    "@babel/traverse" "^7.21.5"
    "@babel/types" "^7.21.5"

"@babel/helper-optimise-call-expression@^7.18.6":
  version "7.18.6"


@@ 234,10 239,10 @@
  dependencies:
    "@babel/types" "^7.18.6"

"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
  version "7.20.2"
  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629"
  integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.21.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56"
  integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==

"@babel/helper-remap-async-to-generator@^7.18.9":
  version "7.18.9"


@@ 261,12 266,12 @@
    "@babel/traverse" "^7.20.7"
    "@babel/types" "^7.20.7"

"@babel/helper-simple-access@^7.20.2":
  version "7.20.2"
  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9"
  integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==
"@babel/helper-simple-access@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee"
  integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==
  dependencies:
    "@babel/types" "^7.20.2"
    "@babel/types" "^7.21.5"

"@babel/helper-skip-transparent-expression-wrappers@^7.20.0":
  version "7.20.0"


@@ 282,10 287,10 @@
  dependencies:
    "@babel/types" "^7.18.6"

"@babel/helper-string-parser@^7.19.4":
  version "7.19.4"
  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
  integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
"@babel/helper-string-parser@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd"
  integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==

"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
  version "7.19.1"


@@ 307,14 312,14 @@
    "@babel/traverse" "^7.18.10"
    "@babel/types" "^7.18.10"

"@babel/helpers@^7.21.0":
  version "7.21.0"
  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e"
  integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==
"@babel/helpers@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08"
  integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==
  dependencies:
    "@babel/template" "^7.20.7"
    "@babel/traverse" "^7.21.0"
    "@babel/types" "^7.21.0"
    "@babel/traverse" "^7.21.5"
    "@babel/types" "^7.21.5"

"@babel/highlight@^7.18.6":
  version "7.18.6"


@@ 325,10 330,10 @@
    chalk "^2.0.0"
    js-tokens "^4.0.0"

"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17"
  integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8":
  version "7.21.8"
  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
  integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==

"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
  version "7.18.6"


@@ 524,7 529,7 @@
  dependencies:
    "@babel/helper-plugin-utils" "^7.19.0"

"@babel/plugin-syntax-import-meta@^7.8.3":
"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3":
  version "7.10.4"
  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==


@@ 615,12 620,12 @@
  dependencies:
    "@babel/helper-plugin-utils" "^7.14.5"

"@babel/plugin-transform-arrow-functions@^7.20.7":
  version "7.20.7"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551"
  integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==
"@babel/plugin-transform-arrow-functions@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz#9bb42a53de447936a57ba256fbf537fc312b6929"
  integrity sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==
  dependencies:
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/helper-plugin-utils" "^7.21.5"

"@babel/plugin-transform-async-to-generator@^7.20.7":
  version "7.20.7"


@@ 660,12 665,12 @@
    "@babel/helper-split-export-declaration" "^7.18.6"
    globals "^11.1.0"

"@babel/plugin-transform-computed-properties@^7.20.7":
  version "7.20.7"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa"
  integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==
"@babel/plugin-transform-computed-properties@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz#3a2d8bb771cd2ef1cd736435f6552fe502e11b44"
  integrity sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==
  dependencies:
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/helper-plugin-utils" "^7.21.5"
    "@babel/template" "^7.20.7"

"@babel/plugin-transform-destructuring@^7.21.3":


@@ 698,12 703,12 @@
    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6"
    "@babel/helper-plugin-utils" "^7.18.6"

"@babel/plugin-transform-for-of@^7.21.0":
  version "7.21.0"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e"
  integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==
"@babel/plugin-transform-for-of@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz#e890032b535f5a2e237a18535f56a9fdaa7b83fc"
  integrity sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==
  dependencies:
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/helper-plugin-utils" "^7.21.5"

"@babel/plugin-transform-function-name@^7.18.9":
  version "7.18.9"


@@ 736,14 741,14 @@
    "@babel/helper-module-transforms" "^7.20.11"
    "@babel/helper-plugin-utils" "^7.20.2"

"@babel/plugin-transform-modules-commonjs@^7.21.2":
  version "7.21.2"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7"
  integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==
"@babel/plugin-transform-modules-commonjs@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz#d69fb947eed51af91de82e4708f676864e5e47bc"
  integrity sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==
  dependencies:
    "@babel/helper-module-transforms" "^7.21.2"
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/helper-simple-access" "^7.20.2"
    "@babel/helper-module-transforms" "^7.21.5"
    "@babel/helper-plugin-utils" "^7.21.5"
    "@babel/helper-simple-access" "^7.21.5"

"@babel/plugin-transform-modules-systemjs@^7.20.11":
  version "7.20.11"


@@ 841,12 846,12 @@
    "@babel/helper-annotate-as-pure" "^7.18.6"
    "@babel/helper-plugin-utils" "^7.18.6"

"@babel/plugin-transform-regenerator@^7.20.5":
  version "7.20.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d"
  integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==
"@babel/plugin-transform-regenerator@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz#576c62f9923f94bcb1c855adc53561fd7913724e"
  integrity sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==
  dependencies:
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/helper-plugin-utils" "^7.21.5"
    regenerator-transform "^0.15.1"

"@babel/plugin-transform-reserved-words@^7.18.6":


@@ 914,12 919,12 @@
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/plugin-syntax-typescript" "^7.20.0"

"@babel/plugin-transform-unicode-escapes@^7.18.10":
  version "7.18.10"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246"
  integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==
"@babel/plugin-transform-unicode-escapes@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz#1e55ed6195259b0e9061d81f5ef45a9b009fb7f2"
  integrity sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==
  dependencies:
    "@babel/helper-plugin-utils" "^7.18.9"
    "@babel/helper-plugin-utils" "^7.21.5"

"@babel/plugin-transform-unicode-regex@^7.18.6":
  version "7.18.6"


@@ 929,14 934,14 @@
    "@babel/helper-create-regexp-features-plugin" "^7.18.6"
    "@babel/helper-plugin-utils" "^7.18.6"

"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.21.4":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.4.tgz#a952482e634a8dd8271a3fe5459a16eb10739c58"
  integrity sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.5.tgz#db2089d99efd2297716f018aeead815ac3decffb"
  integrity sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg==
  dependencies:
    "@babel/compat-data" "^7.21.4"
    "@babel/helper-compilation-targets" "^7.21.4"
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/compat-data" "^7.21.5"
    "@babel/helper-compilation-targets" "^7.21.5"
    "@babel/helper-plugin-utils" "^7.21.5"
    "@babel/helper-validator-option" "^7.21.0"
    "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6"
    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.20.7"


@@ 961,6 966,7 @@
    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
    "@babel/plugin-syntax-import-assertions" "^7.20.0"
    "@babel/plugin-syntax-import-meta" "^7.10.4"
    "@babel/plugin-syntax-json-strings" "^7.8.3"
    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"


@@ 970,22 976,22 @@
    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
    "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
    "@babel/plugin-syntax-top-level-await" "^7.14.5"
    "@babel/plugin-transform-arrow-functions" "^7.20.7"
    "@babel/plugin-transform-arrow-functions" "^7.21.5"
    "@babel/plugin-transform-async-to-generator" "^7.20.7"
    "@babel/plugin-transform-block-scoped-functions" "^7.18.6"
    "@babel/plugin-transform-block-scoping" "^7.21.0"
    "@babel/plugin-transform-classes" "^7.21.0"
    "@babel/plugin-transform-computed-properties" "^7.20.7"
    "@babel/plugin-transform-computed-properties" "^7.21.5"
    "@babel/plugin-transform-destructuring" "^7.21.3"
    "@babel/plugin-transform-dotall-regex" "^7.18.6"
    "@babel/plugin-transform-duplicate-keys" "^7.18.9"
    "@babel/plugin-transform-exponentiation-operator" "^7.18.6"
    "@babel/plugin-transform-for-of" "^7.21.0"
    "@babel/plugin-transform-for-of" "^7.21.5"
    "@babel/plugin-transform-function-name" "^7.18.9"
    "@babel/plugin-transform-literals" "^7.18.9"
    "@babel/plugin-transform-member-expression-literals" "^7.18.6"
    "@babel/plugin-transform-modules-amd" "^7.20.11"
    "@babel/plugin-transform-modules-commonjs" "^7.21.2"
    "@babel/plugin-transform-modules-commonjs" "^7.21.5"
    "@babel/plugin-transform-modules-systemjs" "^7.20.11"
    "@babel/plugin-transform-modules-umd" "^7.18.6"
    "@babel/plugin-transform-named-capturing-groups-regex" "^7.20.5"


@@ 993,17 999,17 @@
    "@babel/plugin-transform-object-super" "^7.18.6"
    "@babel/plugin-transform-parameters" "^7.21.3"
    "@babel/plugin-transform-property-literals" "^7.18.6"
    "@babel/plugin-transform-regenerator" "^7.20.5"
    "@babel/plugin-transform-regenerator" "^7.21.5"
    "@babel/plugin-transform-reserved-words" "^7.18.6"
    "@babel/plugin-transform-shorthand-properties" "^7.18.6"
    "@babel/plugin-transform-spread" "^7.20.7"
    "@babel/plugin-transform-sticky-regex" "^7.18.6"
    "@babel/plugin-transform-template-literals" "^7.18.9"
    "@babel/plugin-transform-typeof-symbol" "^7.18.9"
    "@babel/plugin-transform-unicode-escapes" "^7.18.10"
    "@babel/plugin-transform-unicode-escapes" "^7.21.5"
    "@babel/plugin-transform-unicode-regex" "^7.18.6"
    "@babel/preset-modules" "^0.1.5"
    "@babel/types" "^7.21.4"
    "@babel/types" "^7.21.5"
    babel-plugin-polyfill-corejs2 "^0.3.3"
    babel-plugin-polyfill-corejs3 "^0.6.0"
    babel-plugin-polyfill-regenerator "^0.4.1"


@@ 1033,15 1039,15 @@
    "@babel/plugin-transform-react-jsx-development" "^7.18.6"
    "@babel/plugin-transform-react-pure-annotations" "^7.18.6"

"@babel/preset-typescript@^7.21.4":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz#b913ac8e6aa8932e47c21b01b4368d8aa239a529"
  integrity sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==
"@babel/preset-typescript@^7.21.5":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz#68292c884b0e26070b4d66b202072d391358395f"
  integrity sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==
  dependencies:
    "@babel/helper-plugin-utils" "^7.20.2"
    "@babel/helper-plugin-utils" "^7.21.5"
    "@babel/helper-validator-option" "^7.21.0"
    "@babel/plugin-syntax-jsx" "^7.21.4"
    "@babel/plugin-transform-modules-commonjs" "^7.21.2"
    "@babel/plugin-transform-modules-commonjs" "^7.21.5"
    "@babel/plugin-transform-typescript" "^7.21.3"

"@babel/regjsgen@^0.8.0":


@@ 1064,10 1070,10 @@
  dependencies:
    regenerator-runtime "^0.12.0"

"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
  version "7.21.0"
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
  integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
  integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
  dependencies:
    regenerator-runtime "^0.13.11"



@@ 1080,28 1086,28 @@
    "@babel/parser" "^7.20.7"
    "@babel/types" "^7.20.7"

"@babel/traverse@^7.18.10", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.7.2":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36"
  integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==
"@babel/traverse@^7.18.10", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133"
  integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==
  dependencies:
    "@babel/code-frame" "^7.21.4"
    "@babel/generator" "^7.21.4"
    "@babel/helper-environment-visitor" "^7.18.9"
    "@babel/generator" "^7.21.5"
    "@babel/helper-environment-visitor" "^7.21.5"
    "@babel/helper-function-name" "^7.21.0"
    "@babel/helper-hoist-variables" "^7.18.6"
    "@babel/helper-split-export-declaration" "^7.18.6"
    "@babel/parser" "^7.21.4"
    "@babel/types" "^7.21.4"
    "@babel/parser" "^7.21.5"
    "@babel/types" "^7.21.5"
    debug "^4.1.0"
    globals "^11.1.0"

"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
  version "7.21.4"
  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4"
  integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
  version "7.21.5"
  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6"
  integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==
  dependencies:
    "@babel/helper-string-parser" "^7.19.4"
    "@babel/helper-string-parser" "^7.21.5"
    "@babel/helper-validator-identifier" "^7.19.1"
    to-fast-properties "^2.0.0"



@@ 1271,29 1277,29 @@
  dependencies:
    "@floating-ui/core" "^1.0.1"

"@formatjs/ecma402-abstract@1.14.3":
  version "1.14.3"
  resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz#6428f243538a11126180d121ce8d4b2f17465738"
  integrity sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==
"@formatjs/ecma402-abstract@1.15.0":
  version "1.15.0"
  resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.15.0.tgz#0a285a5dc69889e15d53803bd5036272e23e5a18"
  integrity sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==
  dependencies:
    "@formatjs/intl-localematcher" "0.2.32"
    tslib "^2.4.0"

"@formatjs/icu-messageformat-parser@2.3.1":
  version "2.3.1"
  resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.1.tgz#953080ea5c053bc73bdf55d0a524a3c3c133ae6b"
  integrity sha512-knF2AkAKN4Upv4oIiKY4Wd/dLH68TNMPgV/tJMu/T6FP9aQwbv8fpj7U3lkyniPaNVxvia56Gxax8MKOjtxLSQ==
"@formatjs/icu-messageformat-parser@2.4.0":
  version "2.4.0"
  resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.4.0.tgz#e165f3594c68416ce15f63793768251de2a85f88"
  integrity sha512-6Dh5Z/gp4F/HovXXu/vmd0If5NbYLB5dZrmhWVNb+BOGOEU3wt7Z/83KY1dtd7IDhAnYHasbmKE1RbTE0J+3hw==
  dependencies:
    "@formatjs/ecma402-abstract" "1.14.3"
    "@formatjs/icu-skeleton-parser" "1.3.18"
    "@formatjs/ecma402-abstract" "1.15.0"
    "@formatjs/icu-skeleton-parser" "1.4.0"
    tslib "^2.4.0"

"@formatjs/icu-skeleton-parser@1.3.18":
  version "1.3.18"
  resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz#7aed3d60e718c8ad6b0e64820be44daa1e29eeeb"
  integrity sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==
"@formatjs/icu-skeleton-parser@1.4.0":
  version "1.4.0"
  resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.4.0.tgz#96342eca7c4eef7a309875569e5da973db3465e6"
  integrity sha512-Qq347VM616rVLkvN6QsKJELazRyNlbCiN47LdH0Mc5U7E2xV0vatiVhGqd3KFgbc055BvtnUXR7XX60dCGFuWg==
  dependencies:
    "@formatjs/ecma402-abstract" "1.14.3"
    "@formatjs/ecma402-abstract" "1.15.0"
    tslib "^2.4.0"

"@formatjs/intl-localematcher@0.2.32":


@@ 1315,12 1321,12 @@
  resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.2.5.tgz#eaafd94df3d102ee13e54e80f992a33868a6b1e8"
  integrity sha512-p7gcmazKROteL4IECCp03Qrs790fZ8tbemUAjQu0+K0AaAlK49rI1SIFFq3LzDUAqXIshV95JJhRe/yXxkal5g==

"@formatjs/ts-transformer@3.13.0":
  version "3.13.0"
  resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.13.0.tgz#61185278fb153d61e56fabbeed6d4fc8a0ee3af5"
  integrity sha512-TshsXkt2loK2GWFJFYTrlNThfCd4ubcEpokl9FWzGoR5f5e2FOxDPs69nTqw+7jodlKtx4VaTSfpNMtPvD9ZfQ==
"@formatjs/ts-transformer@3.13.1":
  version "3.13.1"
  resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.13.1.tgz#37aa4992aa50740f808f1f888f112b8addb617c7"
  integrity sha512-U5BuLqFx5wre5Q0NrZhBh7itMJZISYuZGoj8HdN/UO1EKaTL2HQjE1G040GjTpY0k+AAyaHK3b8WPqgHLvIaIQ==
  dependencies:
    "@formatjs/icu-messageformat-parser" "2.3.1"
    "@formatjs/icu-messageformat-parser" "2.4.0"
    "@types/json-stable-stringify" "^1.0.32"
    "@types/node" "14 || 16 || 17"
    chalk "^4.0.0"


@@ 1721,6 1727,16 @@
  resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717"
  integrity sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==

"@reduxjs/toolkit@^1.9.5":
  version "1.9.5"
  resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4"
  integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==
  dependencies:
    immer "^9.0.21"
    redux "^4.2.1"
    redux-thunk "^2.4.2"
    reselect "^4.1.8"

"@restart/hooks@^0.4.7":
  version "0.4.7"
  resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39"


@@ 2459,15 2475,15 @@
  dependencies:
    "@types/yargs-parser" "*"

"@typescript-eslint/eslint-plugin@^5.59.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08"
  integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==
"@typescript-eslint/eslint-plugin@^5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd"
  integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==
  dependencies:
    "@eslint-community/regexpp" "^4.4.0"
    "@typescript-eslint/scope-manager" "5.59.1"
    "@typescript-eslint/type-utils" "5.59.1"
    "@typescript-eslint/utils" "5.59.1"
    "@typescript-eslint/scope-manager" "5.59.2"
    "@typescript-eslint/type-utils" "5.59.2"
    "@typescript-eslint/utils" "5.59.2"
    debug "^4.3.4"
    grapheme-splitter "^1.0.4"
    ignore "^5.2.0"


@@ 2475,98 2491,98 @@
    semver "^7.3.7"
    tsutils "^3.21.0"

"@typescript-eslint/parser@^5.59.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4"
  integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==
"@typescript-eslint/parser@^5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8"
  integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==
  dependencies:
    "@typescript-eslint/scope-manager" "5.59.1"
    "@typescript-eslint/types" "5.59.1"
    "@typescript-eslint/typescript-estree" "5.59.1"
    "@typescript-eslint/scope-manager" "5.59.2"
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/typescript-estree" "5.59.2"
    debug "^4.3.4"

"@typescript-eslint/scope-manager@5.59.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz#8a20222719cebc5198618a5d44113705b51fd7fe"
  integrity sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==
"@typescript-eslint/scope-manager@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c"
  integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==
  dependencies:
    "@typescript-eslint/types" "5.59.1"
    "@typescript-eslint/visitor-keys" "5.59.1"
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/visitor-keys" "5.59.2"

"@typescript-eslint/type-utils@5.59.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111"
  integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==
"@typescript-eslint/type-utils@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf"
  integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==
  dependencies:
    "@typescript-eslint/typescript-estree" "5.59.1"
    "@typescript-eslint/utils" "5.59.1"
    "@typescript-eslint/typescript-estree" "5.59.2"
    "@typescript-eslint/utils" "5.59.2"
    debug "^4.3.4"
    tsutils "^3.21.0"

"@typescript-eslint/types@5.45.0":
  version "5.45.0"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5"
  integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==
"@typescript-eslint/types@5.59.0":
  version "5.59.0"
  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.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d"
  integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==
"@typescript-eslint/types@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655"
  integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==

"@typescript-eslint/typescript-estree@5.45.0":
  version "5.45.0"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d"
  integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==
"@typescript-eslint/typescript-estree@5.59.0":
  version "5.59.0"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965"
  integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==
  dependencies:
    "@typescript-eslint/types" "5.45.0"
    "@typescript-eslint/visitor-keys" "5.45.0"
    "@typescript-eslint/types" "5.59.0"
    "@typescript-eslint/visitor-keys" "5.59.0"
    debug "^4.3.4"
    globby "^11.1.0"
    is-glob "^4.0.3"
    semver "^7.3.7"
    tsutils "^3.21.0"

"@typescript-eslint/typescript-estree@5.59.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c"
  integrity sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==
"@typescript-eslint/typescript-estree@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936"
  integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==
  dependencies:
    "@typescript-eslint/types" "5.59.1"
    "@typescript-eslint/visitor-keys" "5.59.1"
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/visitor-keys" "5.59.2"
    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.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473"
  integrity sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==
"@typescript-eslint/utils@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4"
  integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==
  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.1"
    "@typescript-eslint/types" "5.59.1"
    "@typescript-eslint/typescript-estree" "5.59.1"
    "@typescript-eslint/scope-manager" "5.59.2"
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/typescript-estree" "5.59.2"
    eslint-scope "^5.1.1"
    semver "^7.3.7"

"@typescript-eslint/visitor-keys@5.45.0":
  version "5.45.0"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528"
  integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==
"@typescript-eslint/visitor-keys@5.59.0":
  version "5.59.0"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365"
  integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==
  dependencies:
    "@typescript-eslint/types" "5.45.0"
    "@typescript-eslint/types" "5.59.0"
    eslint-visitor-keys "^3.3.0"

"@typescript-eslint/visitor-keys@5.59.1":
  version "5.59.1"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058"
  integrity sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==
"@typescript-eslint/visitor-keys@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750"
  integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==
  dependencies:
    "@typescript-eslint/types" "5.59.1"
    "@typescript-eslint/types" "5.59.2"
    eslint-visitor-keys "^3.3.0"

"@webassemblyjs/ast@1.9.0":


@@ 3163,10 3179,10 @@ axe-core@^4.6.2:
  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece"
  integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==

axios@^1.3.6:
  version "1.3.6"
  resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646"
  integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==
axios@^1.4.0:
  version "1.4.0"
  resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
  integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
  dependencies:
    follow-redirects "^1.15.0"
    form-data "^4.0.0"


@@ 4309,14 4325,14 @@ cssesc@^3.0.0:
  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==

cssnano-preset-default@^6.0.0:
  version "6.0.0"
  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.0.0.tgz#058726536bdc18711c01b1d328766cbc5691cf71"
  integrity sha512-BDxlaFzObRDXUiCCBQUNQcI+f1/aX2mgoNtXGjV6PG64POcHoDUoX+LgMWw+Q4609QhxwkcSnS65YFs42RA6qQ==
cssnano-preset-default@^6.0.1:
  version "6.0.1"
  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.0.1.tgz#2a93247140d214ddb9f46bc6a3562fa9177fe301"
  integrity sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ==
  dependencies:
    css-declaration-sorter "^6.3.1"
    cssnano-utils "^4.0.0"
    postcss-calc "^8.2.3"
    postcss-calc "^9.0.0"
    postcss-colormin "^6.0.0"
    postcss-convert-values "^6.0.0"
    postcss-discard-comments "^6.0.0"


@@ 4324,7 4340,7 @@ cssnano-preset-default@^6.0.0:
    postcss-discard-empty "^6.0.0"
    postcss-discard-overridden "^6.0.0"
    postcss-merge-longhand "^6.0.0"
    postcss-merge-rules "^6.0.0"
    postcss-merge-rules "^6.0.1"
    postcss-minify-font-values "^6.0.0"
    postcss-minify-gradients "^6.0.0"
    postcss-minify-params "^6.0.0"


@@ 4349,12 4365,12 @@ cssnano-utils@^4.0.0:
  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-4.0.0.tgz#d1da885ec04003ab19505ff0e62e029708d36b08"
  integrity sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==

cssnano@^6.0.0:
  version "6.0.0"
  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.0.0.tgz#53f4cb81101cccba0809fad779f006b5d44925ee"
  integrity sha512-RGlcbzGhzEBCHuQe3k+Udyj5M00z0pm9S+VurHXFEOXxH+y0sVrJH2sMzoyz2d8N1EScazg+DVvmgyx0lurwwA==
cssnano@^6.0.1:
  version "6.0.1"
  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.0.1.tgz#87c38c4cd47049c735ab756d7e77ac3ca855c008"
  integrity sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg==
  dependencies:
    cssnano-preset-default "^6.0.0"
    cssnano-preset-default "^6.0.1"
    lilconfig "^2.1.0"

csso@^5.0.5:


@@ 5035,20 5051,20 @@ eslint-module-utils@^2.7.4:
  dependencies:
    debug "^3.2.7"

eslint-plugin-formatjs@^4.9.0:
  version "4.10.0"
  resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.10.0.tgz#05da23f75b4ce507c90df93ff07be2b6e70ffbc5"
  integrity sha512-YvNF72NVMkIevgJrX5xTkIj4eBCeiweM2/61ppP2eEni3FP4pDXy9UFsOhtxJVISTBH0UEAqz3xhRjqi1q+qag==
eslint-plugin-formatjs@^4.10.1:
  version "4.10.1"
  resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.10.1.tgz#c67184ac54188dcad84d6541e6b5467248ab6550"
  integrity sha512-sD3GGdfDQqiaW8TgbUD4lrUR+raIgusPzW+0v+iN36QzkHvpg5L0UZGFQE9GWtgnWfXAndb57UpgB0i/CF2G7w==
  dependencies:
    "@formatjs/icu-messageformat-parser" "2.3.1"
    "@formatjs/ts-transformer" "3.13.0"
    "@formatjs/icu-messageformat-parser" "2.4.0"
    "@formatjs/ts-transformer" "3.13.1"
    "@types/eslint" "7 || 8"
    "@types/picomatch" "^2.3.0"
    "@typescript-eslint/typescript-estree" "5.45.0"
    "@typescript-eslint/typescript-estree" "5.59.0"
    emoji-regex "^10.2.1"
    magic-string "^0.29.0"
    magic-string "^0.30.0"
    picomatch "^2.3.1"
    tslib "2.4.0"
    tslib "2.5.0"
    typescript "^4.7 || 5"
    unicode-emoji-utils "^1.1.1"



@@ 6296,6 6312,11 @@ ignore@^5.2.0, ignore@^5.2.4:
  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
  integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==

immer@^9.0.21:
  version "9.0.21"
  resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
  integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==

immutable@^3.8.2:
  version "3.8.2"
  resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"


@@ 7409,10 7430,10 @@ jsdom@^20.0.0:
    ws "^8.11.0"
    xml-name-validator "^4.0.0"

jsdom@^21.1.1:
  version "21.1.1"
  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.1.tgz#ab796361e3f6c01bcfaeda1fea3c06197ac9d8ae"
  integrity sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==
jsdom@^21.1.2:
  version "21.1.2"
  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.2.tgz#6433f751b8718248d646af1cdf6662dc8a1ca7f9"
  integrity sha512-sCpFmK2jv+1sjff4u7fzft+pUh2KSUbUrEHYHyfSIbGTIcmnjyp83qg6qLwdJ/I3LpTXx33ACxeRL7Lsyc6lGQ==
  dependencies:
    abab "^2.0.6"
    acorn "^8.8.2"


@@ 7427,7 7448,7 @@ jsdom@^21.1.1:
    http-proxy-agent "^5.0.0"
    https-proxy-agent "^5.0.1"
    is-potential-custom-element-name "^1.0.1"
    nwsapi "^2.2.2"
    nwsapi "^2.2.4"
    parse5 "^7.1.2"
    rrweb-cssom "^0.6.0"
    saxes "^6.0.0"


@@ 7810,10 7831,10 @@ magic-string@^0.25.0, magic-string@^0.25.7:
  dependencies:
    sourcemap-codec "^1.4.8"

magic-string@^0.29.0:
  version "0.29.0"
  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.29.0.tgz#f034f79f8c43dba4ae1730ffb5e8c4e084b16cf3"
  integrity sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==
magic-string@^0.30.0:
  version "0.30.0"
  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529"
  integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
  dependencies:
    "@jridgewell/sourcemap-codec" "^1.4.13"



@@ 8364,10 8385,10 @@ nth-check@^2.0.1:
  dependencies:
    boolbase "^1.0.0"

nwsapi@^2.2.2:
  version "2.2.2"
  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0"
  integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==
nwsapi@^2.2.2, nwsapi@^2.2.4:
  version "2.2.4"
  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5"
  integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==

object-assign@^4.0.1, object-assign@^4.1.1:
  version "4.1.1"


@@ 8920,12 8941,12 @@ posix-character-classes@^0.1.0:
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=

postcss-calc@^8.2.3:
  version "8.2.4"
  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5"
  integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==
postcss-calc@^9.0.0:
  version "9.0.0"
  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.0.tgz#cd9b2b155e57c823687eb67c9afcbe97c98ecaa4"
  integrity sha512-B9BNW/SVh4SMJfoCQ6D9h1Wo7Yjqks7UdbiARJ16J5TIsQn5NEqwMF5joSgOYb26oJPUR5Uv3fCQ/4PvmZWeJQ==
  dependencies:
    postcss-selector-parser "^6.0.9"
    postcss-selector-parser "^6.0.11"
    postcss-value-parser "^4.2.0"

postcss-colormin@^6.0.0:


@@ 8990,10 9011,10 @@ postcss-merge-longhand@^6.0.0:
    postcss-value-parser "^4.2.0"
    stylehacks "^6.0.0"

postcss-merge-rules@^6.0.0:
  version "6.0.0"
  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.0.0.tgz#0d95bc73541156b8b4e763bd0de2c3f9d0ecf013"
  integrity sha512-rCXkklftzEkniyv3f4mRCQzxD6oE4Quyh61uyWTUbCJ26Pv2hoz+fivJSsSBWxDBeScR4fKCfF3HHTcD7Ybqnw==
postcss-merge-rules@^6.0.1:
  version "6.0.1"
  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.0.1.tgz#39f165746404e646c0f5c510222ccde4824a86aa"
  integrity sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw==
  dependencies:
    browserslist "^4.21.4"
    caniuse-api "^3.0.0"


@@ 9160,10 9181,10 @@ postcss-scss@^4.0.6:
  resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd"
  integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==

postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
  version "6.0.11"
  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
  integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5:
  version "6.0.12"
  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz#2efae5ffab3c8bfb2b7fbf0c426e3bca616c4abb"
  integrity sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==
  dependencies:
    cssesc "^3.0.0"
    util-deprecate "^1.0.2"


@@ 9188,7 9209,7 @@ 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.22, postcss@^8.4.23:
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==


@@ 9612,10 9633,10 @@ react-router@^4.3.1:
    prop-types "^15.6.1"
    warning "^4.0.1"

react-select@*, react-select@^5.7.2:
  version "5.7.2"
  resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.2.tgz#ccd40071b9429277983bf15526e7a5773a060e09"
  integrity sha512-cTlJkQ8YjV6T/js8wW0owTzht0hHGABh29vjLscY4HfZGkv7hc3FFTmRp9NzY/Ib1uQ36GieAKEjxpHdpCFpcA==
react-select@*, react-select@^5.7.3:
  version "5.7.3"
  resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.3.tgz#fa0dc9a23cad6ff3871ad3829f6083a4b54961a2"
  integrity sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==
  dependencies:
    "@babel/runtime" "^7.12.0"
    "@emotion/cache" "^11.4.0"


@@ 10949,10 10970,10 @@ stylelint-scss@^4.6.0:
    postcss-selector-parser "^6.0.11"
    postcss-value-parser "^4.2.0"

stylelint@^15.6.0:
  version "15.6.0"
  resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.0.tgz#1d76176dd8b6307bc4645e428ad18ddd15edbafc"
  integrity sha512-Cqzpc8tvJm77KaM8qUbhpJ/UYK55Ia0whQXj4b9IId9dlPICO7J8Lyo15SZWiHxKjlvy3p5FQor/3n6i8ignXg==
stylelint@^15.6.1:
  version "15.6.1"
  resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.1.tgz#e4cd33a3af88587b99a5d1328aedd8c298b6dc81"
  integrity sha512-d8icFBlVl93Elf3Z5ABQNOCe4nx69is3D/NZhDLAie1eyYnpxfeKe7pCfqzT5W4F8vxHCLSDfV8nKNJzogvV2Q==
  dependencies:
    "@csstools/css-parser-algorithms" "^2.1.1"
    "@csstools/css-tokenizer" "^2.1.1"


@@ 10981,11 11002,11 @@ stylelint@^15.6.0:
    micromatch "^4.0.5"
    normalize-path "^3.0.0"
    picocolors "^1.0.0"
    postcss "^8.4.22"
    postcss "^8.4.23"
    postcss-media-query-parser "^0.2.3"
    postcss-resolve-nested-selector "^0.1.1"
    postcss-safe-parser "^6.0.0"
    postcss-selector-parser "^6.0.11"
    postcss-selector-parser "^6.0.12"
    postcss-value-parser "^4.2.0"
    resolve-from "^5.0.0"
    string-width "^4.2.3"


@@ 10995,7 11016,7 @@ stylelint@^15.6.0:
    svg-tags "^1.0.0"
    table "^6.8.1"
    v8-compile-cache "^2.3.0"
    write-file-atomic "^5.0.0"
    write-file-atomic "^5.0.1"

stylis@4.0.13:
  version "4.0.13"


@@ 11316,26 11337,16 @@ tsconfig-paths@^3.14.1:
    minimist "^1.2.6"
    strip-bom "^3.0.0"

tslib@2.4.0:
  version "2.4.0"
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
  integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tslib@2.5.0, tslib@^2.1.0, tslib@^2.4.0:
  version "2.5.0"
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
  integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==

tslib@^1.8.1:
tslib@^1.8.1, tslib@^1.9.0:
  version "1.14.1"
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==

tslib@^1.9.0:
  version "1.13.0"
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==

tslib@^2.1.0, tslib@^2.4.0:
  version "2.5.0"
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
  integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==

tsutils@^3.21.0:
  version "3.21.0"
  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"


@@ 12256,13 12267,13 @@ write-file-atomic@^4.0.2:
    imurmurhash "^0.1.4"
    signal-exit "^3.0.7"

write-file-atomic@^5.0.0:
  version "5.0.0"
  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.0.tgz#54303f117e109bf3d540261125c8ea5a7320fab0"
  integrity sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w==
write-file-atomic@^5.0.1:
  version "5.0.1"
  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7"
  integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==
  dependencies:
    imurmurhash "^0.1.4"
    signal-exit "^3.0.7"
    signal-exit "^4.0.1"

ws@^6.2.1:
  version "6.2.1"


@@ 12360,10 12371,10 @@ yargs@^13.3.2:
    y18n "^4.0.0"
    yargs-parser "^13.1.2"

yargs@^17.3.1, yargs@^17.7.1:
  version "17.7.1"
  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"
  integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==
yargs@^17.3.1, yargs@^17.7.2:
  version "17.7.2"
  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
  dependencies:
    cliui "^8.0.1"
    escalade "^3.1.1"