about summary refs log tree commit diff
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-05-03 21:20:42 +0200
committerThibaut Girka <thib@sitedethib.com>2020-05-03 21:23:49 +0200
commita22e6a368333f3563f8d8d56d8e98d02088e82dc (patch)
tree4146f9e8afe4257c6f33bc695a3cfbfb89aa81b6
parent9c61dadc0db7009853c6b2345a02c3b219022929 (diff)
parente223fd8c6190661237ea43e7773e47513c48fd46 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/statuses_controller.rb`:
  Upstream disabled the embed controller for reblogs.
  Not a real conflict, but glitch-soc has an extra line to deal
  with its theming system.
  Ported upstream changes.
- `app/javascript/packs/public.js`:
  Upstream made changes to get rid of most inline CSS, this changes
  javascript for public pages, which in glitch are split between
  different files. Ported those changes.
- `app/models/status.rb`:
  Upstream changed the block check in `Status#permitted_for` to
  include domain-block checks. Not a real conflict with glitch-soc,
  but our scope is slightly different, as our scope for
  unauthenticated access do not include instance-local toots.
  Ported upstream changes.
- `app/serializers/rest/instance_serializer.rb`:
  Not a real conflict, upstream added a new field to the instance
  serializer, the conflict is one line above since we added more of
  that.
  Ported upstream changes.
- `app/views/settings/profiles/show.html.haml`:
  Upstream got rid of most inline CSS and moved hidden elements
  to data attributes in the process, in fields were we have
  different values.
  Ported upstream changes while keeping our glitch-specific
  values.
- `app/views/statuses/_simple_status.html.haml`:
  Upstream got rid of inline CSS on an HAML line we treat
  differently, stripping empty text nodes.
  Ported upstream changes to the style attribute, keeping
  the empty text node stripping behavior.
-rw-r--r--.circleci/config.yml124
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock21
-rw-r--r--app/controllers/accounts_controller.rb14
-rw-r--r--app/controllers/activitypub/collections_controller.rb17
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb6
-rw-r--r--app/controllers/activitypub/replies_controller.rb21
-rw-r--r--app/controllers/api/v1/polls/votes_controller.rb2
-rw-r--r--app/controllers/api/v1/polls_controller.rb2
-rw-r--r--app/controllers/api/v1/push/subscriptions_controller.rb11
-rw-r--r--app/controllers/api/v1/statuses/mutes_controller.rb3
-rw-r--r--app/controllers/api/v1/statuses_controller.rb2
-rw-r--r--app/controllers/media_controller.rb2
-rw-r--r--app/controllers/remote_interaction_controller.rb2
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/javascript/core/settings.js2
-rw-r--r--app/javascript/flavours/glitch/packs/public.js14
-rw-r--r--app/javascript/mastodon/actions/timelines.js2
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js2
-rw-r--r--app/javascript/mastodon/reducers/statuses.js2
-rw-r--r--app/javascript/mastodon/reducers/timelines.js10
-rw-r--r--app/javascript/packs/public.js14
-rw-r--r--app/javascript/styles/mastodon/about.scss5
-rw-r--r--app/javascript/styles/mastodon/admin.scss20
-rw-r--r--app/javascript/styles/mastodon/basics.scss16
-rw-r--r--app/javascript/styles/mastodon/components.scss12
-rw-r--r--app/javascript/styles/mastodon/forms.scss21
-rw-r--r--app/javascript/styles/mastodon/polls.scss30
-rw-r--r--app/javascript/styles/mastodon/statuses.scss17
-rw-r--r--app/models/account.rb90
-rw-r--r--app/models/concerns/omniauthable.rb2
-rw-r--r--app/models/custom_emoji.rb29
-rw-r--r--app/models/media_attachment.rb35
-rw-r--r--app/models/preview_card.rb43
-rw-r--r--app/models/status.rb8
-rw-r--r--app/serializers/rest/instance_serializer.rb6
-rw-r--r--app/services/fetch_resource_service.rb13
-rw-r--r--app/views/about/show.html.haml6
-rw-r--r--app/views/accounts/_moved.html.haml6
-rw-r--r--app/views/admin/accounts/_account.html.haml2
-rw-r--r--app/views/admin/accounts/show.html.haml26
-rw-r--r--app/views/admin/instances/index.html.haml2
-rw-r--r--app/views/admin/instances/show.html.haml6
-rw-r--r--app/views/admin/pending_accounts/index.html.haml8
-rw-r--r--app/views/admin/relationships/index.html.haml2
-rw-r--r--app/views/admin/reports/show.html.haml6
-rw-r--r--app/views/admin/statuses/index.html.haml2
-rw-r--r--app/views/admin/statuses/show.html.haml2
-rw-r--r--app/views/admin/tags/index.html.haml8
-rw-r--r--app/views/application/_card.html.haml1
-rw-r--r--app/views/auth/registrations/new.html.haml4
-rw-r--r--app/views/auth/sessions/two_factor.html.haml2
-rw-r--r--app/views/directories/index.html.haml1
-rwxr-xr-xapp/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/embedded.html.haml2
-rw-r--r--app/views/public_timelines/show.html.haml2
-rw-r--r--app/views/settings/preferences/appearance/show.html.haml4
-rw-r--r--app/views/settings/profiles/show.html.haml2
-rw-r--r--app/views/statuses/_detailed_status.html.haml6
-rw-r--r--app/views/statuses/_poll.html.haml8
-rw-r--r--app/views/statuses/_simple_status.html.haml12
-rw-r--r--config/initializers/paperclip.rb22
-rw-r--r--db/migrate/20200417125749_add_storage_schema_version.rb9
-rw-r--r--db/schema.rb7
-rw-r--r--ide-helper.js12
-rw-r--r--lib/cli.rb4
-rw-r--r--lib/mastodon/cli_helper.rb4
-rw-r--r--lib/mastodon/emoji_cli.rb44
-rw-r--r--lib/mastodon/media_cli.rb24
-rw-r--r--lib/mastodon/upgrade_cli.rb148
-rw-r--r--lib/paperclip/attachment_extensions.rb9
-rw-r--r--spec/controllers/activitypub/collections_controller_spec.rb132
-rw-r--r--spec/controllers/activitypub/inboxes_controller_spec.rb28
-rw-r--r--spec/controllers/activitypub/outboxes_controller_spec.rb170
-rw-r--r--spec/controllers/activitypub/replies_controller_spec.rb196
-rw-r--r--spec/controllers/statuses_controller_spec.rb843
-rw-r--r--spec/fabricators/status_pin_fabricator.rb2
-rw-r--r--spec/models/status_spec.rb12
-rw-r--r--spec/services/fetch_resource_service_spec.rb12
-rw-r--r--yarn.lock367
81 files changed, 2170 insertions, 624 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index dd943e327..9f43a0573 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,12 +5,13 @@ aliases:
     docker:
       - image: circleci/ruby:2.7-buster-node
         environment: &ruby_environment
+          BUNDLE_JOBS: 3
+          BUNDLE_RETRY: 3
           BUNDLE_APP_CONFIG: ./.bundle/
           BUNDLE_PATH: ./vendor/bundle/
           DB_HOST: localhost
           DB_USER: root
           RAILS_ENV: test
-          PARALLEL_TEST_PROCESSORS: 4
           ALLOW_NOPAM: true
           CONTINUOUS_INTEGRATION: true
           DISABLE_SIMPLECOV: true
@@ -32,9 +33,9 @@ aliases:
   - &restore_ruby_dependencies
     restore_cache:
       keys:
-        - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
-        - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
-        - v2-ruby-dependencies-
+        - v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
+        - v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
+        - v3-ruby-dependencies-
 
   - &install_steps
     steps:
@@ -42,11 +43,13 @@ aliases:
       - *attach_workspace
       - restore_cache:
           keys:
-            - v1-node-dependencies-{{ checksum "yarn.lock" }}
-            - v1-node-dependencies-
-      - run: yarn install --frozen-lockfile
+            - v2-node-dependencies-{{ checksum "yarn.lock" }}
+            - v2-node-dependencies-
+      - run:
+          name: Install yarn dependencies
+          command: yarn install --frozen-lockfile
       - save_cache:
-          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
+          key: v2-node-dependencies-{{ checksum "yarn.lock" }}
           paths:
             - ./node_modules/
       - *persist_to_workspace
@@ -57,27 +60,28 @@ aliases:
         command: |
           sudo apt-get update
           sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
-          
-          ## TODO: FIX THESE BUSTER DEPENDANCES
-          sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb
-          sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb
-          sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb
-          sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb
 
   - &install_ruby_dependencies
       steps:
         - *attach_workspace
         - *install_system_dependencies
-        - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+        - run:
+            name: Set Ruby version
+            command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
         - *restore_ruby_dependencies
-        - run: bundle config set clean 'true'
-        - run: bundle config set deployment 'true'
-        - run: bundle config set with 'pam_authentication'
-        - run: bundle config set without 'development production'
-        - run: bundle config set frozen 'true'
-        - run: bundle install --jobs 16 --retry 3 && bundle clean
+        - run:
+            name: Set bundler settings
+            command: |
+              bundle config clean 'true'
+              bundle config deployment 'true'
+              bundle config with 'pam_authentication'
+              bundle config without 'development production'
+              bundle config frozen 'true'
+        - run:
+            name: Install bundler dependencies
+            command: bundle check || (bundle install && bundle clean)
         - save_cache:
-            key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
+            key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
             paths:
               - ./.bundle/
               - ./vendor/bundle/
@@ -88,17 +92,26 @@ aliases:
                 - ./mastodon/vendor/bundle/
 
   - &test_steps
+      parallelism: 4
       steps:
         - *attach_workspace
         - *install_system_dependencies
-        - run: sudo apt-get install -y ffmpeg
         - run:
-            name: Prepare Tests
-            command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
+            name: Install FFMPEG
+            command: sudo apt-get install -y ffmpeg
         - run:
-            name: Run Tests
-            command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec
-
+            name: Load database schema
+            command: ./bin/rails db:create db:schema:load db:seed
+        - run:
+            name: Run rspec in parallel
+            command: |
+              bundle exec rspec --profile 10 \
+                                --format RspecJunitFormatter \
+                                --out test_results/rspec.xml \
+                                --format progress \
+                                $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
+        - store_test_results:
+            path: test_results
 jobs:
   install:
     <<: *defaults
@@ -115,19 +128,14 @@ jobs:
         environment: *ruby_environment
     <<: *install_ruby_dependencies
 
-  install-ruby2.5:
-    <<: *defaults
-    docker:
-      - image: circleci/ruby:2.5-buster-node
-        environment: *ruby_environment
-    <<: *install_ruby_dependencies
-
   build:
     <<: *defaults
     steps:
       - *attach_workspace
       - *install_system_dependencies
-      - run: ./bin/rails assets:precompile
+      - run:
+          name: Precompile assets
+          command: ./bin/rails assets:precompile
       - persist_to_workspace:
           root: ~/projects/
           paths:
@@ -149,10 +157,10 @@ jobs:
       - *install_system_dependencies
       - run:
           name: Create database
-          command: ./bin/rails parallel:create
+          command: ./bin/rails db:create
       - run:
           name: Run migrations
-          command: ./bin/rails parallel:migrate
+          command: ./bin/rails db:migrate
 
   test-ruby2.7:
     <<: *defaults
@@ -178,35 +186,33 @@ jobs:
       - image: circleci/redis:5-alpine
     <<: *test_steps
 
-  test-ruby2.5:
-    <<: *defaults
-    docker:
-      - image: circleci/ruby:2.5-buster-node
-        environment: *ruby_environment
-      - image: circleci/postgres:12.2
-        environment:
-          POSTGRES_USER: root
-          POSTGRES_HOST_AUTH_METHOD: trust
-      - image: circleci/redis:5-alpine
-    <<: *test_steps
-
   test-webui:
     <<: *defaults
     docker:
       - image: circleci/node:12-buster
     steps:
       - *attach_workspace
-      - run: ./bin/retry yarn test:jest
+      - run:
+          name: Run jest
+          command: yarn test:jest
 
   check-i18n:
     <<: *defaults
     steps:
       - *attach_workspace
       - *install_system_dependencies
-      - run: bundle exec i18n-tasks check-normalized
-      - run: bundle exec i18n-tasks unused -l en
-      - run: bundle exec i18n-tasks check-consistent-interpolations
-      - run: bundle exec rake repo:check_locales_files
+      - run:
+          name: Check locale file normalization
+          command: bundle exec i18n-tasks check-normalized
+      - run:
+          name: Check for unused strings
+          command: bundle exec i18n-tasks unused -l en
+      - run:
+          name: Check for wrong string interpolations
+          command: bundle exec i18n-tasks check-consistent-interpolations
+      - run:
+          name: Check that all required locale files exist
+          command: bundle exec rake repo:check_locales_files
 
 workflows:
   version: 2
@@ -220,10 +226,6 @@ workflows:
           requires:
             - install
             - install-ruby2.7
-      - install-ruby2.5:
-          requires:
-            - install
-            - install-ruby2.7
       - build:
           requires:
             - install-ruby2.7
@@ -238,10 +240,6 @@ workflows:
           requires:
             - install-ruby2.6
             - build
-      - test-ruby2.5:
-          requires:
-            - install-ruby2.5
-            - build
       - test-webui:
           requires:
             - install
diff --git a/Gemfile b/Gemfile
index d59aa9d12..c5b957114 100644
--- a/Gemfile
+++ b/Gemfile
@@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
 gem 'pghero', '~> 2.4'
 gem 'dotenv-rails', '~> 2.7'
 
-gem 'aws-sdk-s3', '~> 1.61', require: false
+gem 'aws-sdk-s3', '~> 1.63', require: false
 gem 'fog-core', '<= 2.1.0'
 gem 'fog-openstack', '~> 0.3', require: false
 gem 'paperclip', '~> 6.0'
@@ -129,6 +129,7 @@ group :test do
   gem 'simplecov', '~> 0.18', require: false
   gem 'webmock', '~> 3.8'
   gem 'parallel_tests', '~> 2.32'
+  gem 'rspec_junit_formatter', '~> 0.4'
 end
 
 group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 13f6bed59..7525911ff 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -92,7 +92,7 @@ GEM
     av (0.9.0)
       cocaine (~> 0.5.3)
     aws-eventstream (1.1.0)
-    aws-partitions (1.296.0)
+    aws-partitions (1.303.0)
     aws-sdk-core (3.94.0)
       aws-eventstream (~> 1, >= 1.0.2)
       aws-partitions (~> 1, >= 1.239.0)
@@ -101,7 +101,7 @@ GEM
     aws-sdk-kms (1.30.0)
       aws-sdk-core (~> 3, >= 3.71.0)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.61.2)
+    aws-sdk-s3 (1.63.0)
       aws-sdk-core (~> 3, >= 3.83.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.1)
@@ -277,7 +277,7 @@ GEM
       http-parser (~> 1.2.0)
     http-cookie (1.0.3)
       domain_name (~> 0.5)
-    http-form_data (2.2.0)
+    http-form_data (2.3.0)
     http-parser (1.2.1)
       ffi-compiler (>= 1.0, < 2.0)
     http_accept_language (2.1.1)
@@ -303,7 +303,7 @@ GEM
     jmespath (1.4.0)
     json (2.3.0)
     json-canonicalization (0.2.0)
-    json-ld (3.1.2)
+    json-ld (3.1.3)
       htmlentities (~> 4.3)
       json-canonicalization (~> 0.2)
       link_header (~> 0.0, >= 0.0.8)
@@ -359,7 +359,7 @@ GEM
       nokogiri (~> 1.10)
     mime-types (3.3.1)
       mime-types-data (~> 3.2015)
-    mime-types-data (3.2019.1009)
+    mime-types-data (3.2020.0425)
     mimemagic (0.3.4)
     mini_mime (1.0.2)
     mini_portile2 (2.4.0)
@@ -382,7 +382,7 @@ GEM
       concurrent-ruby (~> 1.0, >= 1.0.2)
       sidekiq (>= 3.5)
       statsd-ruby (~> 1.4, >= 1.4.0)
-    oj (3.10.5)
+    oj (3.10.6)
     omniauth (1.9.1)
       hashie (>= 3.4.6)
       rack (>= 1.6.2, < 3)
@@ -409,7 +409,7 @@ GEM
       parallel
     parser (2.7.1.1)
       ast (~> 2.4.0)
-    parslet (1.8.2)
+    parslet (2.0.0)
     pastel (0.7.3)
       equatable (~> 0.6)
       tty-color (~> 0.5)
@@ -542,6 +542,8 @@ GEM
       rspec-core (~> 3.0, >= 3.0.0)
       sidekiq (>= 2.4.0)
     rspec-support (3.9.2)
+    rspec_junit_formatter (0.4.1)
+      rspec-core (>= 2, < 4, != 2.12.0)
     rubocop (0.79.0)
       jaro_winkler (~> 1.5.1)
       parallel (~> 1.10)
@@ -554,7 +556,7 @@ GEM
       rack (>= 1.1)
       rubocop (>= 0.72.0)
     ruby-progressbar (1.10.1)
-    ruby-saml (1.9.0)
+    ruby-saml (1.11.0)
       nokogiri (>= 1.5.10)
     rufus-scheduler (3.6.0)
       fugit (~> 1.1, >= 1.1.6)
@@ -668,7 +670,7 @@ DEPENDENCIES
   active_record_query_trace (~> 1.7)
   addressable (~> 2.7)
   annotate (~> 3.1)
-  aws-sdk-s3 (~> 1.61)
+  aws-sdk-s3 (~> 1.63)
   better_errors (~> 2.6)
   binding_of_caller (~> 0.7)
   blurhash (~> 0.1)
@@ -765,6 +767,7 @@ DEPENDENCIES
   rqrcode (~> 1.1)
   rspec-rails (~> 4.0)
   rspec-sidekiq (~> 3.0)
+  rspec_junit_formatter (~> 0.4)
   rubocop (~> 0.79)
   rubocop-rails (~> 2.5)
   ruby-progressbar (~> 1.10)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index ee48da177..52d09cff8 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -28,7 +28,7 @@ class AccountsController < ApplicationController
         end
 
         @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
-        @statuses        = filtered_status_page(params)
+        @statuses        = filtered_status_page
         @statuses        = cache_collection(@statuses, Status)
         @rss_url         = rss_url
 
@@ -141,12 +141,12 @@ class AccountsController < ApplicationController
     request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
   end
 
-  def filtered_status_page(params)
-    if params[:min_id].present?
-      filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
-    else
-      filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
-    end
+  def filtered_status_page
+    filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
+  end
+
+  def params_slice(*keys)
+    params.slice(*keys).permit(*keys)
   end
 
   def restrict_fields_to
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index 910fefb1c..c1e7aa550 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -24,20 +24,23 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
   def set_size
     case params[:id]
     when 'featured'
-      @account.pinned_statuses.count
+      @size = @account.pinned_statuses.count
     else
-      raise ActiveRecord::RecordNotFound
+      not_found
     end
   end
 
   def scope_for_collection
     case params[:id]
     when 'featured'
-      return Status.none if @account.blocking?(signed_request_account)
-
-      @account.pinned_statuses
-    else
-      raise ActiveRecord::RecordNotFound
+      # Because in public fetch mode we cache the response, there would be no
+      # benefit from performing the check below, since a blocked account or domain
+      # would likely be served the cache from the reverse proxy anyway
+      if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
+        Status.none
+      else
+        @account.pinned_statuses
+      end
     end
   end
 
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index 891756b7e..e25a4bc07 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -11,7 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
   before_action :set_cache_headers
 
   def show
-    expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
+    expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
     render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
   end
 
@@ -50,12 +50,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
     return unless page_requested?
 
     @statuses = @account.statuses.permitted_for(@account, signed_request_account)
-    @statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
+    @statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
     @statuses = cache_collection(@statuses, Status)
   end
 
   def page_requested?
-    params[:page] == 'true'
+    truthy_param?(:page)
   end
 
   def page_params
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index c62061555..43bf4e657 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ActivityPub::RepliesController < ActivityPub::BaseController
-  include SignatureAuthentication
+  include SignatureVerification
   include Authorization
   include AccountOwnedConcern
 
@@ -19,15 +19,19 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
 
   private
 
+  def pundit_user
+    signed_request_account
+  end
+
   def set_status
     @status = @account.statuses.find(params[:status_id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_replies
-    @replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
+    @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
     @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
     @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
   end
@@ -38,7 +42,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
       type: :unordered,
       part_of: account_status_replies_url(@account, @status),
       next: next_page,
-      items: @replies.map { |status| status.local ? status : status.uri }
+      items: @replies.map { |status| status.local? ? status : status.uri }
     )
 
     return page if page_requested?
@@ -51,16 +55,21 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
   end
 
   def page_requested?
-    params[:page] == 'true'
+    truthy_param?(:page)
+  end
+
+  def only_other_accounts?
+    truthy_param?(:only_other_accounts)
   end
 
   def next_page
     only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
+
     account_status_replies_url(
       @account,
       @status,
       page: true,
-      min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
+      min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
       only_other_accounts: only_other_accounts
     )
   end
diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb
index e1d26106a..513b937ef 100644
--- a/app/controllers/api/v1/polls/votes_controller.rb
+++ b/app/controllers/api/v1/polls/votes_controller.rb
@@ -18,7 +18,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
     @poll = Poll.attached.find(params[:poll_id])
     authorize @poll.status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def vote_params
diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb
index 744baf7bb..6435e9f0d 100644
--- a/app/controllers/api/v1/polls_controller.rb
+++ b/app/controllers/api/v1/polls_controller.rb
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
     @poll = Poll.attached.find(params[:id])
     authorize @poll.status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def refresh_poll
diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb
index 1cbc92b93..d34b333eb 100644
--- a/app/controllers/api/v1/push/subscriptions_controller.rb
+++ b/app/controllers/api/v1/push/subscriptions_controller.rb
@@ -4,6 +4,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :push }
   before_action :require_user!
   before_action :set_web_push_subscription
+  before_action :check_web_push_subscription, only: [:show, :update]
 
   def create
     @web_subscription&.destroy!
@@ -21,16 +22,11 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
   end
 
   def show
-    raise ActiveRecord::RecordNotFound if @web_subscription.nil?
-
     render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
   def update
-    raise ActiveRecord::RecordNotFound if @web_subscription.nil?
-
     @web_subscription.update!(data: data_params)
-
     render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
@@ -45,12 +41,17 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
     @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
   end
 
+  def check_web_push_subscription
+    not_found if @web_subscription.nil?
+  end
+
   def subscription_params
     params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
   end
 
   def data_params
     return {} if params[:data].blank?
+
     params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
   end
 end
diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb
index 43c7a525a..87071a2b9 100644
--- a/app/controllers/api/v1/statuses/mutes_controller.rb
+++ b/app/controllers/api/v1/statuses/mutes_controller.rb
@@ -28,8 +28,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController
     @status = Status.find(params[:status_id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    # Reraise in order to get a 404 instead of a 403 error code
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_conversation
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 29ae91762..b3edce676 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -68,7 +68,7 @@ class Api::V1::StatusesController < Api::BaseController
     @status = Status.find(params[:id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_thread
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 05cf09c28..1d166d6e7 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -33,7 +33,7 @@ class MediaController < ApplicationController
   def verify_permitted_status!
     authorize @media_attachment.status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def check_playable
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index e058d0ed5..51bb9bdea 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -42,7 +42,7 @@ class RemoteInteractionController < ApplicationController
     @status = Status.find(params[:id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    raise ActiveRecord::RecordNotFound
+    not_found
   end
 
   def set_body_classes
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 588063d01..a1b7f4320 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -49,7 +49,7 @@ class StatusesController < ApplicationController
 
   def embed
     use_pack 'embed'
-    return not_found if @status.hidden?
+    return not_found if @status.hidden? || @status.reblog?
 
     expires_in 180, public: true
     response.headers['X-Frame-Options'] = 'ALLOWALL'
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index e02c91cc7..9fe03f90c 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -10,7 +10,7 @@ delegate(document, '#account_display_name', 'input', ({ target }) => {
     if (target.value) {
       name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
     } else {
-      name.textContent = document.querySelector('#default_account_display_name').textContent;
+      name.textContent = name.textContent = target.dataset.default;
     }
   }
 });
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index e5a567205..58febcf5b 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -99,15 +99,13 @@ function main() {
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
 
     delegate(document, '.status__content__spoiler-link', 'click', function() {
-      const contentEl = this.parentNode.parentNode.querySelector('.e-content');
+      const statusEl = this.parentNode.parentNode;
 
-      if (contentEl.style.display === 'block') {
-        contentEl.style.display = 'none';
-        this.parentNode.style.marginBottom = 0;
+      if (statusEl.dataset.spoiler === 'expanded') {
+        statusEl.dataset.spoiler = 'folded';
         this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
       } else {
-        contentEl.style.display = 'block';
-        this.parentNode.style.marginBottom = null;
+        statusEl.dataset.spoiler = 'expanded';
         this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
       }
 
@@ -115,8 +113,8 @@ function main() {
     });
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
-      const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
-      const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const statusEl = spoilerLink.parentNode.parentNode;
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 50840cacc..861827d33 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -42,7 +42,7 @@ export function updateTimeline(timeline, status, accept) {
 export function deleteFromTimelines(id) {
   return (dispatch, getState) => {
     const accountId  = getState().getIn(['statuses', id, 'account']);
-    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
+    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
     const reblogOf   = getState().getIn(['statuses', id, 'reblog'], null);
 
     dispatch({
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 31c02d735..4734e0f3f 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent {
     document.addEventListener('keydown', this.handleKeyDown, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
     if (this.focusedItem && this.props.openedViaKeyboard) {
-      this.focusedItem.focus();
+      this.focusedItem.focus({ preventScroll: true });
     }
     this.setState({ mounted: true });
   }
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 57588fe96..96028e042 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -100,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
-    if (this.focusedItem) this.focusedItem.focus();
+    if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
     this.setState({ mounted: true });
   }
 
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 2554c008d..53dec9585 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -25,7 +25,7 @@ const importStatuses = (state, statuses) =>
 
 const deleteStatus = (state, id, references) => {
   references.forEach(ref => {
-    state = deleteStatus(state, ref[0], []);
+    state = deleteStatus(state, ref, []);
   });
 
   return state.delete(id);
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 63b76773d..9156db021 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -89,7 +89,7 @@ const updateTimeline = (state, timeline, status, usePendingItems) => {
   }));
 };
 
-const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
+const deleteStatus = (state, id, references, exclude_account = null) => {
   state.keySeq().forEach(timeline => {
     if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
       const helper = list => list.filterNot(item => item === id);
@@ -99,7 +99,7 @@ const deleteStatus = (state, id, accountId, references, exclude_account = null)
 
   // Remove reblogs of deleted status
   references.forEach(ref => {
-    state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
+    state = deleteStatus(state, ref, [], exclude_account);
   });
 
   return state;
@@ -117,8 +117,8 @@ const filterTimelines = (state, relationship, statuses) => {
       return;
     }
 
-    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
-    state      = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
+    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id'));
+    state      = deleteStatus(state, status.get('id'), references, relationship.id);
   });
 
   return state;
@@ -150,7 +150,7 @@ export default function timelines(state = initialState, action) {
   case TIMELINE_UPDATE:
     return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
   case TIMELINE_DELETE:
-    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
+    return deleteStatus(state, action.id, action.references, action.reblogOf);
   case TIMELINE_CLEAR:
     return clearTimeline(state, action.timeline);
   case ACCOUNT_BLOCK_SUCCESS:
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 5b699e767..3d190d2da 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -103,15 +103,13 @@ function main() {
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
 
     delegate(document, '.status__content__spoiler-link', 'click', function() {
-      const contentEl = this.parentNode.parentNode.querySelector('.e-content');
+      const statusEl = this.parentNode.parentNode;
 
-      if (contentEl.style.display === 'block') {
-        contentEl.style.display = 'none';
-        this.parentNode.style.marginBottom = 0;
+      if (statusEl.dataset.spoiler === 'expanded') {
+        statusEl.dataset.spoiler = 'folded';
         this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
       } else {
-        contentEl.style.display = 'block';
-        this.parentNode.style.marginBottom = null;
+        statusEl.dataset.spoiler = 'expanded';
         this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
       }
 
@@ -119,8 +117,8 @@ function main() {
     });
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
-      const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
-      const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const statusEl = spoilerLink.parentNode.parentNode;
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index cf16b54ac..711f34965 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -757,8 +757,13 @@ $small-breakpoint: 960px;
       }
     }
 
+    &__counters__wrapper {
+      display: flex;
+    }
+
     &__counter {
       padding: 10px;
+      width: 50%;
 
       strong {
         font-family: $font-display, sans-serif;
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 7bff2daa1..78dea92b9 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -583,6 +583,18 @@ body,
   }
 }
 
+.special-action-button,
+.back-link {
+  text-align: right;
+  flex: 1 1 auto;
+}
+
+.action-buttons {
+  display: flex;
+  overflow: hidden;
+  justify-content: space-between;
+}
+
 .spacer {
   flex: 1 1 auto;
 }
@@ -920,3 +932,11 @@ a.name-tag,
     }
   }
 }
+
+.account-badges {
+  margin: -2px 0;
+}
+
+.dashboard__counters.admin-account-counters {
+  margin-top: 10px;
+}
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 2b10b5ad3..a5dbe75fb 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -229,3 +229,19 @@ button {
     }
   }
 }
+
+.logo-resources {
+  display: none;
+}
+
+// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements,
+// to set the z-index to a high value, which messes with modals and dropdowns.
+// Blocked elements can in theory only be media and frames/embeds, so they
+// should only appear in statuses, under divs and articles.
+body,
+div,
+article {
+  .__ns__pop2top {
+    z-index: unset !important;
+  }
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c7835b878..6c33b709d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1362,6 +1362,12 @@ a .account__avatar {
   &-base {
     @include avatar-radius;
     @include avatar-size(36px);
+
+    img {
+      @include avatar-radius;
+      width: 100%;
+      height: 100%;
+    }
   }
 
   &-overlay {
@@ -1372,6 +1378,12 @@ a .account__avatar {
     bottom: 0;
     right: 0;
     z-index: 1;
+
+    img {
+      @include avatar-radius;
+      width: 100%;
+      height: 100%;
+    }
   }
 }
 
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index c9ad68f94..0e5b00e8f 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -142,6 +142,10 @@ code {
     }
   }
 
+  .otp-hint {
+    margin-bottom: 25px;
+  }
+
   .card {
     margin-bottom: 15px;
   }
@@ -285,6 +289,14 @@ code {
         margin-bottom: 25px;
       }
     }
+
+    .fields-group.invited-by {
+      margin-bottom: 30px;
+
+      .hint {
+        text-align: center;
+      }
+    }
   }
 
   .input.radio_buttons .radio label {
@@ -635,6 +647,15 @@ code {
   @media screen and (max-width: 740px) and (min-width: 441px) {
     margin-top: 40px;
   }
+
+  &.translation-prompt {
+    text-align: unset;
+    color: unset;
+
+    a {
+      text-decoration: underline;
+    }
+  }
 }
 
 .form-footer {
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index 1ecc8434d..ad7088982 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -19,6 +19,36 @@
     }
   }
 
+  progress {
+    border: 0;
+    display: block;
+    width: 100%;
+    height: 5px;
+    appearance: none;
+    background: transparent;
+
+    &::-webkit-progress-bar {
+      background: transparent;
+    }
+
+    // Those rules need to be entirely separate or they won't work, hence the
+    // duplication
+    &::-moz-progress-bar {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+
+    &::-ms-fill {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+
+    &::-webkit-progress-value {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+  }
+
   &__option {
     position: relative;
     display: flex;
diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss
index 19ce0ab8f..0b7be7afd 100644
--- a/app/javascript/styles/mastodon/statuses.scss
+++ b/app/javascript/styles/mastodon/statuses.scss
@@ -128,6 +128,16 @@
 
 .embed,
 .public-layout {
+  .status__content[data-spoiler=folded] {
+    .e-content {
+      display: none;
+    }
+
+    p:first-child {
+      margin-bottom: 0;
+    }
+  }
+
   .detailed-status {
     padding: 15px;
   }
@@ -159,5 +169,12 @@
     .video-player {
       margin-top: 10px;
     }
+
+    &__action-bar-button {
+      font-size: 18px;
+      width: 23.1429px;
+      height: 23.1429px;
+      line-height: 23.15px;
+    }
   }
 }
diff --git a/app/models/account.rb b/app/models/account.rb
index e56db3126..5038d4768 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -3,50 +3,52 @@
 #
 # Table name: accounts
 #
-#  id                      :bigint(8)        not null, primary key
-#  username                :string           default(""), not null
-#  domain                  :string
-#  secret                  :string           default(""), not null
-#  private_key             :text
-#  public_key              :text             default(""), not null
-#  remote_url              :string           default(""), not null
-#  salmon_url              :string           default(""), not null
-#  hub_url                 :string           default(""), not null
-#  created_at              :datetime         not null
-#  updated_at              :datetime         not null
-#  note                    :text             default(""), not null
-#  display_name            :string           default(""), not null
-#  uri                     :string           default(""), not null
-#  url                     :string
-#  avatar_file_name        :string
-#  avatar_content_type     :string
-#  avatar_file_size        :integer
-#  avatar_updated_at       :datetime
-#  header_file_name        :string
-#  header_content_type     :string
-#  header_file_size        :integer
-#  header_updated_at       :datetime
-#  avatar_remote_url       :string
-#  subscription_expires_at :datetime
-#  locked                  :boolean          default(FALSE), not null
-#  header_remote_url       :string           default(""), not null
-#  last_webfingered_at     :datetime
-#  inbox_url               :string           default(""), not null
-#  outbox_url              :string           default(""), not null
-#  shared_inbox_url        :string           default(""), not null
-#  followers_url           :string           default(""), not null
-#  protocol                :integer          default("ostatus"), not null
-#  memorial                :boolean          default(FALSE), not null
-#  moved_to_account_id     :bigint(8)
-#  featured_collection_url :string
-#  fields                  :jsonb
-#  actor_type              :string
-#  discoverable            :boolean
-#  also_known_as           :string           is an Array
-#  silenced_at             :datetime
-#  suspended_at            :datetime
-#  trust_level             :integer
-#  hide_collections        :boolean
+#  id                            :bigint(8)        not null, primary key
+#  username                      :string           default(""), not null
+#  domain                        :string
+#  secret                        :string           default(""), not null
+#  private_key                   :text
+#  public_key                    :text             default(""), not null
+#  remote_url                    :string           default(""), not null
+#  salmon_url                    :string           default(""), not null
+#  hub_url                       :string           default(""), not null
+#  created_at                    :datetime         not null
+#  updated_at                    :datetime         not null
+#  note                          :text             default(""), not null
+#  display_name                  :string           default(""), not null
+#  uri                           :string           default(""), not null
+#  url                           :string
+#  avatar_file_name              :string
+#  avatar_content_type           :string
+#  avatar_file_size              :integer
+#  avatar_updated_at             :datetime
+#  header_file_name              :string
+#  header_content_type           :string
+#  header_file_size              :integer
+#  header_updated_at             :datetime
+#  avatar_remote_url             :string
+#  subscription_expires_at       :datetime
+#  locked                        :boolean          default(FALSE), not null
+#  header_remote_url             :string           default(""), not null
+#  last_webfingered_at           :datetime
+#  inbox_url                     :string           default(""), not null
+#  outbox_url                    :string           default(""), not null
+#  shared_inbox_url              :string           default(""), not null
+#  followers_url                 :string           default(""), not null
+#  protocol                      :integer          default("ostatus"), not null
+#  memorial                      :boolean          default(FALSE), not null
+#  moved_to_account_id           :bigint(8)
+#  featured_collection_url       :string
+#  fields                        :jsonb
+#  actor_type                    :string
+#  discoverable                  :boolean
+#  also_known_as                 :string           is an Array
+#  silenced_at                   :datetime
+#  suspended_at                  :datetime
+#  trust_level                   :integer
+#  hide_collections              :boolean
+#  avatar_storage_schema_version :integer
+#  header_storage_schema_version :integer
 #
 
 class Account < ApplicationRecord
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
index 960784222..736da6c1d 100644
--- a/app/models/concerns/omniauthable.rb
+++ b/app/models/concerns/omniauthable.rb
@@ -82,7 +82,7 @@ module Omniauthable
       username = starting_username
       i        = 0
 
-      while Account.exists?(username: username)
+      while Account.exists?(username: username, domain: nil)
         i       += 1
         username = "#{starting_username}_#{i}"
       end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index d177cf281..7cb03b819 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -3,20 +3,21 @@
 #
 # Table name: custom_emojis
 #
-#  id                 :bigint(8)        not null, primary key
-#  shortcode          :string           default(""), not null
-#  domain             :string
-#  image_file_name    :string
-#  image_content_type :string
-#  image_file_size    :integer
-#  image_updated_at   :datetime
-#  created_at         :datetime         not null
-#  updated_at         :datetime         not null
-#  disabled           :boolean          default(FALSE), not null
-#  uri                :string
-#  image_remote_url   :string
-#  visible_in_picker  :boolean          default(TRUE), not null
-#  category_id        :bigint(8)
+#  id                           :bigint(8)        not null, primary key
+#  shortcode                    :string           default(""), not null
+#  domain                       :string
+#  image_file_name              :string
+#  image_content_type           :string
+#  image_file_size              :integer
+#  image_updated_at             :datetime
+#  created_at                   :datetime         not null
+#  updated_at                   :datetime         not null
+#  disabled                     :boolean          default(FALSE), not null
+#  uri                          :string
+#  image_remote_url             :string
+#  visible_in_picker            :boolean          default(TRUE), not null
+#  category_id                  :bigint(8)
+#  image_storage_schema_version :integer
 #
 
 class CustomEmoji < ApplicationRecord
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 40624c73c..f789bdc55 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -3,23 +3,24 @@
 #
 # Table name: media_attachments
 #
-#  id                  :bigint(8)        not null, primary key
-#  status_id           :bigint(8)
-#  file_file_name      :string
-#  file_content_type   :string
-#  file_file_size      :integer
-#  file_updated_at     :datetime
-#  remote_url          :string           default(""), not null
-#  created_at          :datetime         not null
-#  updated_at          :datetime         not null
-#  shortcode           :string
-#  type                :integer          default("image"), not null
-#  file_meta           :json
-#  account_id          :bigint(8)
-#  description         :text
-#  scheduled_status_id :bigint(8)
-#  blurhash            :string
-#  processing          :integer
+#  id                          :bigint(8)        not null, primary key
+#  status_id                   :bigint(8)
+#  file_file_name              :string
+#  file_content_type           :string
+#  file_file_size              :integer
+#  file_updated_at             :datetime
+#  remote_url                  :string           default(""), not null
+#  created_at                  :datetime         not null
+#  updated_at                  :datetime         not null
+#  shortcode                   :string
+#  type                        :integer          default("image"), not null
+#  file_meta                   :json
+#  account_id                  :bigint(8)
+#  description                 :text
+#  scheduled_status_id         :bigint(8)
+#  blurhash                    :string
+#  processing                  :integer
+#  file_storage_schema_version :integer
 #
 
 class MediaAttachment < ApplicationRecord
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 4e89fbf85..2802f4667 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -3,25 +3,26 @@
 #
 # Table name: preview_cards
 #
-#  id                 :bigint(8)        not null, primary key
-#  url                :string           default(""), not null
-#  title              :string           default(""), not null
-#  description        :string           default(""), not null
-#  image_file_name    :string
-#  image_content_type :string
-#  image_file_size    :integer
-#  image_updated_at   :datetime
-#  type               :integer          default("link"), not null
-#  html               :text             default(""), not null
-#  author_name        :string           default(""), not null
-#  author_url         :string           default(""), not null
-#  provider_name      :string           default(""), not null
-#  provider_url       :string           default(""), not null
-#  width              :integer          default(0), not null
-#  height             :integer          default(0), not null
-#  created_at         :datetime         not null
-#  updated_at         :datetime         not null
-#  embed_url          :string           default(""), not null
+#  id                           :bigint(8)        not null, primary key
+#  url                          :string           default(""), not null
+#  title                        :string           default(""), not null
+#  description                  :string           default(""), not null
+#  image_file_name              :string
+#  image_content_type           :string
+#  image_file_size              :integer
+#  image_updated_at             :datetime
+#  type                         :integer          default("link"), not null
+#  html                         :text             default(""), not null
+#  author_name                  :string           default(""), not null
+#  author_url                   :string           default(""), not null
+#  provider_name                :string           default(""), not null
+#  provider_url                 :string           default(""), not null
+#  width                        :integer          default(0), not null
+#  height                       :integer          default(0), not null
+#  created_at                   :datetime         not null
+#  updated_at                   :datetime         not null
+#  embed_url                    :string           default(""), not null
+#  image_storage_schema_version :integer
 #
 
 class PreviewCard < ApplicationRecord
@@ -47,6 +48,10 @@ class PreviewCard < ApplicationRecord
 
   before_save :extract_dimensions, if: :link?
 
+  def local?
+    false
+  end
+
   def missing_image?
     width.present? && height.present? && image_file_name.blank?
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 31e77770d..34fa00912 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -206,12 +206,8 @@ class Status < ApplicationRecord
   def title
     if destroyed?
       "#{account.acct} deleted status"
-    elsif reblog?
-      preview = sensitive ? '<sensitive>' : text.slice(0, 10).split("\n")[0]
-      "#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
     else
-      preview = sensitive ? '<sensitive>' : text.slice(0, 20).split("\n")[0]
-      "#{account.acct}: #{preview}"
+      reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
     end
   end
 
@@ -404,7 +400,7 @@ class Status < ApplicationRecord
 
       if account.nil?
         where(visibility: visibility).not_local_only
-      elsif target_account.blocking?(account) # get rid of blocked peeps
+      elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
         none
       elsif account.id == target_account.id # author can see own stuff
         all
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index e913f0c64..54e7c450c 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 
   attributes :uri, :title, :short_description, :description, :email,
              :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
-             :languages, :registrations, :approval_required
+             :languages, :registrations, :approval_required, :invites_enabled
 
   has_one :contact_account, serializer: REST::AccountSerializer
 
@@ -76,6 +76,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
     Setting.registrations_mode == 'approved'
   end
 
+  def invites_enabled
+    Setting.min_invite_role == 'user'
+  end
+
   private
 
   def instance_presenter
diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb
index 880cdde92..6c0093cd4 100644
--- a/app/services/fetch_resource_service.rb
+++ b/app/services/fetch_resource_service.rb
@@ -25,7 +25,18 @@ class FetchResourceService < BaseService
   end
 
   def perform_request(&block)
-    Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).on_behalf_of(Account.representative).perform(&block)
+    Request.new(:get, @url).tap do |request|
+      request.add_headers('Accept' => ACCEPT_HEADER)
+
+      # In a real setting we want to sign all outgoing requests,
+      # in case the remote server has secure mode enabled and requires
+      # authentication on all resources. However, during development,
+      # sending request signatures with an inaccessible host is useless
+      # and prevents even public resources from being fetched, so
+      # don't do it
+
+      request.on_behalf_of(Account.representative) unless Rails.env.development?
+    end.perform(&block)
   end
 
   def process_response(response, terminal = false)
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index e0ec98ec9..07e06100a 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -68,11 +68,11 @@
           .hero-widget__footer__column
             %h4= t 'about.server_stats'
 
-            %div{ style: 'display: flex' }
-              .hero-widget__counter{ style: 'width: 50%' }
+            .hero-widget__counters__wrapper
+              .hero-widget__counter
                 %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
                 %span= t 'about.user_count_after', count: @instance_presenter.user_count
-              .hero-widget__counter{ style: 'width: 50%' }
+              .hero-widget__counter
                 %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
                 %span
                   = t 'about.active_count_after'
diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml
index a82f277b1..4f71b062d 100644
--- a/app/views/accounts/_moved.html.haml
+++ b/app/views/accounts/_moved.html.haml
@@ -9,8 +9,10 @@
     = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
       .detailed-status__display-avatar
         .account__avatar-overlay
-          .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
-          .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" }
+          .account__avatar-overlay-base
+            = image_tag moved_to_account.avatar_static_url
+          .account__avatar-overlay-overlay
+            = image_tag account.avatar_static_url
 
       %span.display-name
         %bdi
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 44b10af6e..c9bd8c686 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -2,7 +2,7 @@
   %td
     = admin_account_link_to(account)
   %td
-    %div{ style: 'margin: -2px 0' }= account_badge(account, all: true)
+    %div.account-badges= account_badge(account, all: true)
   %td
     - if account.user_current_sign_in_ip
       %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 408f94eed..e6461aad0 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -31,7 +31,7 @@
       %div
         .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
 
-.dashboard__counters{ style: 'margin-top: 10px' }
+.dashboard__counters.admin-account-counters
   %div
     = link_to admin_account_statuses_path(@account.id) do
       .dashboard__counters__num= number_with_delimiter @account.statuses_count
@@ -178,18 +178,8 @@
               = @account.shared_inbox_url
               = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times'
 
-  %div{ style: 'overflow: hidden' }
-    %div{ style: 'float: right' }
-      - if @account.local?
-        = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
-        - if @account.user&.otp_required_for_login?
-          = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
-        - if !@account.memorial? && @account.user_approved?
-          = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
-      - else
-        = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
-
-    %div{ style: 'float: left' }
+  %div.action-buttons
+    %div
       - if @account.local? && @account.user_approved?
         = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
       - if @account.silenced?
@@ -216,6 +206,16 @@
         - else
           = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
 
+    %div
+      - if @account.local?
+        = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
+        - if @account.user&.otp_required_for_login?
+          = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
+        - if !@account.memorial? && @account.user_approved?
+          = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
+      - else
+        = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
+
   %hr.spacer/
 
   - unless @warnings.empty?
diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml
index 0b299acc5..bd67eb4fc 100644
--- a/app/views/admin/instances/index.html.haml
+++ b/app/views/admin/instances/index.html.haml
@@ -10,7 +10,7 @@
       - unless whitelist_mode?
         %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
 
-  %div{ style: 'flex: 1 1 auto; text-align: right' }
+  %div.special-action-button
     - if whitelist_mode?
       = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button'
     - else
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index 49a666a5a..92e14c0df 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -45,11 +45,11 @@
 
 %hr.spacer/
 
-%div{ style: 'overflow: hidden' }
-  %div{ style: 'float: left' }
+%div.action-buttons
+  %div
     = link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button'
 
-  %div{ style: 'float: right' }
+  %div
     - if @domain_allow
       = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
     - elsif @domain_block
diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml
index 171976e33..8101d7f99 100644
--- a/app/views/admin/pending_accounts/index.html.haml
+++ b/app/views/admin/pending_accounts/index.html.haml
@@ -22,9 +22,9 @@
 
 %hr.spacer/
 
-%div{ style: 'overflow: hidden' }
-  %div{ style: 'float: right' }
-    = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
-
+%div.action-buttons
   %div
     = link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
+
+  %div
+    = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
diff --git a/app/views/admin/relationships/index.html.haml b/app/views/admin/relationships/index.html.haml
index 3afaff615..907477f24 100644
--- a/app/views/admin/relationships/index.html.haml
+++ b/app/views/admin/relationships/index.html.haml
@@ -17,7 +17,7 @@
       %li= filter_link_to t('admin.accounts.location.local'), location: 'local'
       %li= filter_link_to t('admin.accounts.location.remote'), location: 'remote'
 
-  .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+  .back-link
     = link_to admin_account_path(@account.id) do
       = fa_icon 'chevron-left fw'
       = t('admin.statuses.back_to_account')
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index b12ea4270..4ecc8dc93 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -65,9 +65,11 @@
 
 %hr.spacer
 
-%div{ style: 'overflow: hidden; margin-bottom: 20px; clear: both' }
+%div.action-buttons
+  %div
+
   - if @report.unresolved?
-    %div{ style: 'float: right' }
+    %div
       - if @report.target_account.local?
         = link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
         = link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml
index 55926f3b3..5414d69d5 100644
--- a/app/views/admin/statuses/index.html.haml
+++ b/app/views/admin/statuses/index.html.haml
@@ -9,7 +9,7 @@
     %ul
       %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected'
       %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
-  .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+  .back-link
     = link_to admin_account_path(@account.id) do
       = fa_icon 'chevron-left fw'
       = t('admin.statuses.back_to_account')
diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml
index a7a392272..e2470198d 100644
--- a/app/views/admin/statuses/show.html.haml
+++ b/app/views/admin/statuses/show.html.haml
@@ -4,7 +4,7 @@
   = "@#{@account.acct}"
 
 .filters
-  .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+  .back-link
     = link_to admin_account_path(@account.id) do
       %i.fa.fa-chevron-left.fa-fw
       = t('admin.statuses.back_to_account')
diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml
index d20ed80f8..e64802275 100644
--- a/app/views/admin/tags/index.html.haml
+++ b/app/views/admin/tags/index.html.haml
@@ -68,9 +68,9 @@
 - if params[:pending_review] == '1' || params[:unreviewed] == '1'
   %hr.spacer/
 
-  %div{ style: 'overflow: hidden' }
-    %div{ style: 'float: right' }
-      = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
-
+  %div.action-buttons
     %div
       = link_to t('admin.accounts.approve_all'), approve_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
+
+    %div
+      = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml
index 808dce514..e7ecfecd9 100644
--- a/app/views/application/_card.html.haml
+++ b/app/views/application/_card.html.haml
@@ -9,7 +9,6 @@
         = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 
       .display-name
-        %span{ id: "default_account_display_name", style: "display: none" }= account.username
         %bdi
           %strong.emojify.p-name= display_name(account, custom_emojify: true)
         %span
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index bcd66fb8a..457bc1d23 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -8,8 +8,8 @@
   = render 'shared/error_messages', object: resource
 
   - if @invite.present? && @invite.autofollow?
-    .fields-group{ style: 'margin-bottom: 30px' }
-      %p.hint{ style: 'text-align: center' }= t('invites.invited_by')
+    .fields-group.invited-by
+      %p.hint= t('invites.invited_by')
       = render 'application/card', account: @invite.user.account
 
   = f.simple_fields_for :account do |ff|
diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml
index 4e6bbd7a9..b2e36f6bc 100644
--- a/app/views/auth/sessions/two_factor.html.haml
+++ b/app/views/auth/sessions/two_factor.html.haml
@@ -2,7 +2,7 @@
   = t('auth.login')
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
-  %p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp')
+  %p.hint.otp-hint= t('simple_form.hints.sessions.otp')
 
   .fields-group
     = f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index ecf12b649..1170332ff 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -28,7 +28,6 @@
               = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 
             .display-name
-              %span{ id: "default_account_display_name", style: "display: none" }= account.username
               %bdi
                 %strong.emojify.p-name= display_name(account, custom_emojify: true)
               %span= acct(account)
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 2be9427c5..99ab3729e 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -40,6 +40,6 @@
   %body{ class: body_classes }
     = content_for?(:content) ? yield(:content) : yield
 
-    %div{ style: 'display: none'}
+    .logo-resources
       = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')
       = render file: Rails.root.join('app', 'javascript', 'images', 'logo_full.svg')
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 6695b12dd..75441b452 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -23,5 +23,5 @@
   %body.embed
     = yield
 
-    %div{ style: 'display: none'}
+    .logo-resources
       = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')
diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml
index 063089a7f..e32bd49ec 100644
--- a/app/views/public_timelines/show.html.haml
+++ b/app/views/public_timelines/show.html.haml
@@ -12,5 +12,5 @@
   - else
     %p= t('about.browse_local_posts')
 
-#mastodon-timeline{ data: { props: Oj.dump(default_props) }}
+#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(local: !Setting.show_known_fediverse_at_about_page)) }}
 #modal-container
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index 5453177fd..5fc865814 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -9,8 +9,8 @@
     = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false
 
   - unless I18n.locale == :en
-    .flash-message{ style: "text-align: unset; color: unset" }
-      #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")}
+    .flash-message.translation-prompt
+      #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener")}
 
   %h4= t 'appearance.advanced_web_interface'
 
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 841c01fd7..6061e9cfd 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -9,7 +9,7 @@
 
   .fields-row
     .fields-row__column.fields-group.fields-row__column-6
-      = f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH }, hint: false
+      = f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH, data: { default: @account.username } }, hint: false
       = f.input :note, wrapper: :with_label, input_html: { maxlength: Account::MAX_NOTE_LENGTH }, hint: false
 
   .fields-row
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 021390e47..544b92330 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -15,12 +15,12 @@
 
   = account_action_button(status.account)
 
-  .status__content.emojify<
+  .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
     - if status.spoiler_text?
-      %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
+      %p<
         %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
         %button.status__content__spoiler-link= t('statuses.show_more')
-    .e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
+    .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
       = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
       - if status.preloadable_poll
         = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml
index de5357e6d..64e62e97c 100644
--- a/app/views/statuses/_poll.html.haml
+++ b/app/views/statuses/_poll.html.haml
@@ -10,13 +10,15 @@
           - percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0
           %label.poll__option><
             %span.poll__number><
-              - if own_votes.include?(index)
-                %i.poll__voted__mark.fa.fa-check
               = "#{percent.round}%"
             %span.poll__option__text
               = Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
+            - if own_votes.include?(index)
+              %span.poll__voted
+                %i.poll__voted__mark.fa.fa-check
 
-          %span.poll__chart{ style: "width: #{percent}%" }
+          %progress{ max: 100, value: percent < 1 ? 1 : percent, 'aria-hidden': 'true' }
+            %span.poll__chart
         - else
           %label.poll__option><
             %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index 8a418a1d5..f959056cd 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -19,12 +19,12 @@
           %span.display-name__account
             = acct(status.account)
             = fa_icon('lock') if status.account.locked?
-  .status__content.emojify<
+  .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
     - if status.spoiler_text?
-      %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
+      %p<
         %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
         %button.status__content__spoiler-link= t('statuses.show_more')
-    .e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
+    .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }<
       = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
       - if status.preloadable_poll
         = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
@@ -51,18 +51,18 @@
 
   .status__action-bar
     .status__action-bar__counter
-      = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+      = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button' do
         - if status.in_reply_to_id.nil?
           = fa_icon 'reply fw'
         - else
           = fa_icon 'reply-all fw'
       .status__action-bar__counter__label= obscured_counter status.replies_count
-    = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+    = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
       - if status.distributable?
         = fa_icon 'retweet fw'
       - elsif status.private_visibility? || status.limited_visibility?
         = fa_icon 'lock fw'
       - else
         = fa_icon 'envelope fw'
-    = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+    = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
       = fa_icon 'star fw'
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 8909678d6..0dd7f8cf8 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -10,9 +10,25 @@ Paperclip.interpolates :filename do |attachment, style|
   end
 end
 
+Paperclip.interpolates :prefix_path do |attachment, style|
+  if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local?
+    'cache' + File::SEPARATOR
+  else
+    ''
+  end
+end
+
+Paperclip.interpolates :prefix_url do |attachment, style|
+  if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local?
+    'cache/'
+  else
+    ''
+  end
+end
+
 Paperclip::Attachment.default_options.merge!(
   use_timestamp: false,
-  path: ':class/:attachment/:id_partition/:style/:filename',
+  path: ':prefix_url:class/:attachment/:id_partition/:style/:filename',
   storage: :fog
 )
 
@@ -91,7 +107,7 @@ else
   Paperclip::Attachment.default_options.merge!(
     storage: :filesystem,
     use_timestamp: true,
-    path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':class', ':attachment', ':id_partition', ':style', ':filename'),
-    url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:class/:attachment/:id_partition/:style/:filename',
+    path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':prefix_path:class', ':attachment', ':id_partition', ':style', ':filename'),
+    url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:prefix_url:class/:attachment/:id_partition/:style/:filename',
   )
 end
diff --git a/db/migrate/20200417125749_add_storage_schema_version.rb b/db/migrate/20200417125749_add_storage_schema_version.rb
new file mode 100644
index 000000000..7438f97ba
--- /dev/null
+++ b/db/migrate/20200417125749_add_storage_schema_version.rb
@@ -0,0 +1,9 @@
+class AddStorageSchemaVersion < ActiveRecord::Migration[5.2]
+  def change
+    add_column :preview_cards, :image_storage_schema_version, :integer
+    add_column :accounts, :avatar_storage_schema_version, :integer
+    add_column :accounts, :header_storage_schema_version, :integer
+    add_column :media_attachments, :file_storage_schema_version, :integer
+    add_column :custom_emojis, :image_storage_schema_version, :integer
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 36e9017b5..2b27a1877 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2020_04_07_202420) do
+ActiveRecord::Schema.define(version: 2020_04_17_125749) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -172,6 +172,8 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
     t.datetime "suspended_at"
     t.integer "trust_level"
     t.boolean "hide_collections"
+    t.integer "avatar_storage_schema_version"
+    t.integer "header_storage_schema_version"
     t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
     t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
     t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
@@ -299,6 +301,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
     t.string "image_remote_url"
     t.boolean "visible_in_picker", default: true, null: false
     t.bigint "category_id"
+    t.integer "image_storage_schema_version"
     t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
   end
 
@@ -465,6 +468,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
     t.bigint "scheduled_status_id"
     t.string "blurhash"
     t.integer "processing"
+    t.integer "file_storage_schema_version"
     t.index ["account_id"], name: "index_media_attachments_on_account_id"
     t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
     t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
@@ -605,6 +609,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
     t.string "embed_url", default: "", null: false
+    t.integer "image_storage_schema_version"
     t.index ["url"], name: "index_preview_cards_on_url", unique: true
   end
 
diff --git a/ide-helper.js b/ide-helper.js
new file mode 100644
index 000000000..9e645cb0e
--- /dev/null
+++ b/ide-helper.js
@@ -0,0 +1,12 @@
+/* global path */
+/*
+Preferences | Languages & Frameworks | JavaScript | Webpack | webpack configuration file
+jetbrains://WebStorm/settings?name=Languages+%26+Frameworks--JavaScript--Webpack
+*/
+module.exports = {
+  resolve: {
+    alias: {
+      'mastodon': path.resolve(__dirname, 'app/javascript/mastodon'),
+    },
+  },
+};
diff --git a/lib/cli.rb b/lib/cli.rb
index 19cc5d6b5..313a36a3d 100644
--- a/lib/cli.rb
+++ b/lib/cli.rb
@@ -11,6 +11,7 @@ require_relative 'mastodon/statuses_cli'
 require_relative 'mastodon/domains_cli'
 require_relative 'mastodon/preview_cards_cli'
 require_relative 'mastodon/cache_cli'
+require_relative 'mastodon/upgrade_cli'
 require_relative 'mastodon/version'
 
 module Mastodon
@@ -49,6 +50,9 @@ module Mastodon
     desc 'cache SUBCOMMAND ...ARGS', 'Manage cache'
     subcommand 'cache', Mastodon::CacheCLI
 
+    desc 'upgrade SUBCOMMAND ...ARGS', 'Various version upgrade utilities'
+    subcommand 'upgrade', Mastodon::UpgradeCLI
+
     option :dry_run, type: :boolean
     desc 'self-destruct', 'Erase the server from the federation'
     long_desc <<~LONG_DESC
diff --git a/lib/mastodon/cli_helper.rb b/lib/mastodon/cli_helper.rb
index ec4d9a81e..4a20fa8d6 100644
--- a/lib/mastodon/cli_helper.rb
+++ b/lib/mastodon/cli_helper.rb
@@ -10,6 +10,10 @@ Paperclip.options[:log]      = false
 
 module Mastodon
   module CLIHelper
+    def dry_run?
+      options[:dry_run]
+    end
+
     def create_progress_bar(total = nil)
       ProgressBar.create(total: total, format: '%c/%u |%b%i| %e')
     end
diff --git a/lib/mastodon/emoji_cli.rb b/lib/mastodon/emoji_cli.rb
index dbaf12018..da8fd6a0d 100644
--- a/lib/mastodon/emoji_cli.rb
+++ b/lib/mastodon/emoji_cli.rb
@@ -23,7 +23,7 @@ module Mastodon
       Existing emoji will be skipped unless the --overwrite option
       is provided, in which case they will be overwritten.
 
-      You can specifiy a --category under which the emojis will be
+      You can specify a --category under which the emojis will be
       grouped together.
 
       With the --prefix option, a prefix can be added to all
@@ -72,6 +72,48 @@ module Mastodon
       say("Imported #{imported}, skipped #{skipped}, failed to import #{failed}", color(imported, skipped, failed))
     end
 
+    option :category
+    option :overwrite, type: :boolean
+    desc 'export PATH', 'Export emoji to a TAR GZIP archive at PATH'
+    long_desc <<-LONG_DESC
+      Exports custom emoji to 'export.tar.gz' at PATH.
+
+      The --category option dumps only the specified category.
+      If this option is not specified, all emoji will be exported.
+
+      The --overwrite option will overwrite an existing archive.
+    LONG_DESC
+    def export(path)
+      exported         = 0
+      category         = CustomEmojiCategory.find_by(name: options[:category])
+      export_file_name = File.join(path, 'export.tar.gz')
+
+      if File.file?(export_file_name) && !options[:overwrite]
+        say("Archive already exists! Use '--overwrite' to overwrite it!")
+        exit 1
+      end
+      if category.nil? && options[:category]
+        say("Unable to find category '#{options[:category]}'!")
+        exit 1
+      end
+
+      File.open(export_file_name, 'wb') do |file|
+        Zlib::GzipWriter.wrap(file) do |gzip|
+          Gem::Package::TarWriter.new(gzip) do |tar|
+            scope = !options[:category] || category.nil? ? CustomEmoji.local : category.emojis
+            scope.find_each do |emoji|
+              say("Adding '#{emoji.shortcode}'...")
+              tar.add_file_simple(emoji.shortcode + File.extname(emoji.image_file_name), 0o644, emoji.image_file_size) do |io|
+                io.write Paperclip.io_adapters.for(emoji.image).read
+                exported += 1
+              end
+            end
+          end
+        end
+      end
+      say("Exported #{exported}")
+    end
+
     option :remote_only, type: :boolean
     desc 'purge', 'Remove all custom emoji'
     long_desc <<-LONG_DESC
diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb
index 0f211f272..424d65a5f 100644
--- a/lib/mastodon/media_cli.rb
+++ b/lib/mastodon/media_cli.rb
@@ -85,7 +85,9 @@ module Mastodon
           record_map = preload_records_from_mixed_objects(objects)
 
           objects.each do |object|
-            path_segments   = object.key.split('/')
+            path_segments = object.key.split('/')
+            path_segments.delete('cache')
+
             model_name      = path_segments.first.classify
             attachment_name = path_segments[1].singularize
             record_id       = path_segments[2..-2].join.to_i
@@ -120,8 +122,11 @@ module Mastodon
         Find.find(File.join(*[root_path, prefix].compact)) do |path|
           next if File.directory?(path)
 
-          key             = path.gsub("#{root_path}#{File::SEPARATOR}", '')
-          path_segments   = key.split(File::SEPARATOR)
+          key = path.gsub("#{root_path}#{File::SEPARATOR}", '')
+
+          path_segments = key.split(File::SEPARATOR)
+          path_segments.delete('cache')
+
           model_name      = path_segments.first.classify
           record_id       = path_segments[2..-2].join.to_i
           attachment_name = path_segments[1].singularize
@@ -229,10 +234,13 @@ module Mastodon
 
     desc 'lookup URL', 'Lookup where media is displayed by passing a media URL'
     def lookup(url)
-      path          = Addressable::URI.parse(url).path
+      path = Addressable::URI.parse(url).path
+
       path_segments = path.split('/')[2..-1]
-      model_name    = path_segments.first.classify
-      record_id     = path_segments[2..-2].join.to_i
+      path_segments.delete('cache')
+
+      model_name = path_segments.first.classify
+      record_id  = path_segments[2..-2].join.to_i
 
       unless PRELOAD_MODEL_WHITELIST.include?(model_name)
         say("Cannot find corresponding model: #{model_name}", :red)
@@ -276,7 +284,9 @@ module Mastodon
       preload_map = Hash.new { |hash, key| hash[key] = [] }
 
       objects.map do |object|
-        segments   = object.key.split('/')
+        segments = object.key.split('/')
+        segments.delete('cache')
+
         model_name = segments.first.classify
         record_id  = segments[2..-2].join.to_i
 
diff --git a/lib/mastodon/upgrade_cli.rb b/lib/mastodon/upgrade_cli.rb
new file mode 100644
index 000000000..74d13f62d
--- /dev/null
+++ b/lib/mastodon/upgrade_cli.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require_relative '../../config/boot'
+require_relative '../../config/environment'
+require_relative 'cli_helper'
+
+module Mastodon
+  class UpgradeCLI < Thor
+    include CLIHelper
+
+    def self.exit_on_failure?
+      true
+    end
+
+    CURRENT_STORAGE_SCHEMA_VERSION = 1
+
+    option :dry_run, type: :boolean, default: false
+    option :verbose, type: :boolean, default: false, aliases: [:v]
+    desc 'storage-schema', 'Upgrade storage schema of various file attachments to the latest version'
+    long_desc <<~LONG_DESC
+      Iterates over every file attachment of every record and, if its storage schema is outdated, performs the
+      necessary upgrade to the latest one. In practice this means e.g. moving files to different directories.
+
+      Will most likely take a long time.
+    LONG_DESC
+    def storage_schema
+      progress = create_progress_bar(nil)
+      dry_run  = dry_run? ? ' (DRY RUN)' : ''
+      records  = 0
+
+      klasses = [
+        Account,
+        CustomEmoji,
+        MediaAttachment,
+        PreviewCard,
+      ]
+
+      klasses.each do |klass|
+        attachment_names = klass.attachment_definitions.keys
+
+        klass.find_each do |record|
+          attachment_names.each do |attachment_name|
+            attachment = record.public_send(attachment_name)
+
+            next if attachment.blank? || attachment.storage_schema_version >= CURRENT_STORAGE_SCHEMA_VERSION
+
+            attachment.styles.each_key do |style|
+              case Paperclip::Attachment.default_options[:storage]
+              when :s3
+                upgrade_storage_s3(progress, attachment, style)
+              when :fog
+                upgrade_storage_fog(progress, attachment, style)
+              when :filesystem
+                upgrade_storage_filesystem(progress, attachment, style)
+              end
+
+              progress.increment
+            end
+
+            attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION)
+          end
+
+          if record.changed?
+            record.save unless dry_run?
+            records += 1
+          end
+        end
+      end
+
+      progress.total = progress.progress
+      progress.finish
+
+      say("Upgraded storage schema of #{records} records#{dry_run}", :green, true)
+    end
+
+    private
+
+    def upgrade_storage_s3(progress, attachment, style)
+      previous_storage_schema_version = attachment.storage_schema_version
+      object                          = attachment.s3_object(style)
+
+      attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION)
+
+      upgraded_path = attachment.path(style)
+
+      if upgraded_path != object.key && object.exists?
+        progress.log("Moving #{object.key} to #{upgraded_path}") if options[:verbose]
+
+        begin
+          object.move_to(upgraded_path) unless dry_run?
+        rescue => e
+          progress.log(pastel.red("Error processing #{object.key}: #{e}"))
+        end
+      end
+
+      # Because we move files style-by-style, it's important to restore
+      # previous version at the end. The upgrade will be recorded after
+      # all styles are updated
+      attachment.instance_write(:storage_schema_version, previous_storage_schema_version)
+    end
+
+    def upgrade_storage_fog(_progress, _attachment, _style)
+      say('The fog storage driver is not supported for this operation at this time', :red)
+      exit(1)
+    end
+
+    def upgrade_storage_filesystem(progress, attachment, style)
+      previous_storage_schema_version = attachment.storage_schema_version
+      previous_path                   = attachment.path(style)
+
+      attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION)
+
+      upgraded_path = attachment.path(style)
+
+      if upgraded_path != previous_path && File.exist?(previous_path)
+        progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
+
+        begin
+          unless dry_run?
+            FileUtils.mkdir_p(File.dirname(upgraded_path))
+            FileUtils.mv(previous_path, upgraded_path)
+
+            begin
+              FileUtils.rmdir(previous_path, parents: true)
+            rescue Errno::ENOTEMPTY
+              # OK
+            end
+          end
+        rescue => e
+          progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
+
+          unless dry_run?
+            begin
+              FileUtils.rmdir(upgraded_path, parents: true)
+            rescue Errno::ENOTEMPTY
+              # OK
+            end
+          end
+        end
+      end
+
+      # Because we move files style-by-style, it's important to restore
+      # previous version at the end. The upgrade will be recorded after
+      # all styles are updated
+      attachment.instance_write(:storage_schema_version, previous_storage_schema_version)
+    end
+  end
+end
diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb
index ce5780557..f3e51dbd3 100644
--- a/lib/paperclip/attachment_extensions.rb
+++ b/lib/paperclip/attachment_extensions.rb
@@ -14,6 +14,15 @@ module Paperclip
       end
     end
 
+    def storage_schema_version
+      instance_read(:storage_schema_version) || 0
+    end
+
+    def assign_attributes
+      super
+      instance_write(:storage_schema_version, 1)
+    end
+
     def variant?(other_filename)
       return true  if original_filename == other_filename
       return false if original_filename.nil?
diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb
index 34114cc85..56be49be3 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/controllers/activitypub/collections_controller_spec.rb
@@ -3,21 +3,133 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::CollectionsController, type: :controller do
-  describe 'POST #show' do
-    let(:account) { Fabricate(:account) }
+  let!(:account) { Fabricate(:account) }
+  let(:remote_account) { nil }
 
-    context 'id is "featured"' do
-      it 'returns 200 with "application/activity+json"' do
-        post :show, params: { id: 'featured', account_username: account.username }
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
 
-        expect(response).to have_http_status(200)
-        expect(response.content_type).to eq 'application/activity+json'
+    Fabricate(:status_pin, account: account)
+    Fabricate(:status_pin, account: account)
+    Fabricate(:status, account: account, visibility: :private)
+  end
+
+  describe 'GET #show' do
+    context 'when id is "featured"' do
+      context 'without signature' do
+        let(:remote_account) { nil }
+
+        before do
+          get :show, params: { id: 'featured', account_username: account.username }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'returns orderedItems with pinned statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 2
+        end
+      end
+
+      context 'with signature' do
+        let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+
+        context do
+          before do
+            get :show, params: { id: 'featured', account_username: account.username }
+          end
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns application/activity+json' do
+            expect(response.content_type).to eq 'application/activity+json'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          it 'returns orderedItems with pinned statuses' do
+            json = body_as_json
+            expect(json[:orderedItems]).to be_an Array
+            expect(json[:orderedItems].size).to eq 2
+          end
+        end
+
+        context 'in authorized fetch mode' do
+          before do
+            allow(controller).to receive(:authorized_fetch_mode?).and_return(true)
+          end
+
+          context 'when signed request account is blocked' do
+            before do
+              account.block!(remote_account)
+              get :show, params: { id: 'featured', account_username: account.username }
+            end
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns application/activity+json' do
+              expect(response.content_type).to eq 'application/activity+json'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns empty orderedItems' do
+              json = body_as_json
+              expect(json[:orderedItems]).to be_an Array
+              expect(json[:orderedItems].size).to eq 0
+            end
+          end
+
+          context 'when signed request account is domain blocked' do
+            before do
+              account.block_domain!(remote_account.domain)
+              get :show, params: { id: 'featured', account_username: account.username }
+            end
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns application/activity+json' do
+              expect(response.content_type).to eq 'application/activity+json'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns empty orderedItems' do
+              json = body_as_json
+              expect(json[:orderedItems]).to be_an Array
+              expect(json[:orderedItems].size).to eq 0
+            end
+          end
+        end
       end
     end
 
-    context 'id is not "featured"' do
-      it 'returns 404' do
-        post :show, params: { id: 'hoge', account_username: account.username }
+    context 'when id is not "featured"' do
+      it 'returns http not found' do
+        get :show, params: { id: 'hoge', account_username: account.username }
         expect(response).to have_http_status(404)
       end
     end
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
index a9ee75490..f3bc23953 100644
--- a/spec/controllers/activitypub/inboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/inboxes_controller_spec.rb
@@ -3,25 +3,31 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::InboxesController, type: :controller do
+  let(:remote_account) { nil }
+
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
+  end
+
   describe 'POST #create' do
-    context 'with signed_request_account' do
-      it 'returns 202' do
-        allow(controller).to receive(:signed_request_account) do
-          Fabricate(:account)
-        end
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) }
 
+      before do
         post :create, body: '{}'
+      end
+
+      it 'returns http accepted' do
         expect(response).to have_http_status(202)
       end
     end
 
-    context 'without signed_request_account' do
-      it 'returns 401' do
-        allow(controller).to receive(:signed_request_account) do
-          false
-        end
-
+    context 'without signature' do
+      before do
         post :create, body: '{}'
+      end
+
+      it 'returns http not authorized' do
         expect(response).to have_http_status(401)
       end
     end
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index 47460b22c..03490533d 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -4,20 +4,174 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
   let!(:account) { Fabricate(:account) }
 
   before do
-    Fabricate(:status, account: account)
+    Fabricate(:status, account: account, visibility: :public)
+    Fabricate(:status, account: account, visibility: :unlisted)
+    Fabricate(:status, account: account, visibility: :private)
+    Fabricate(:status, account: account, visibility: :direct)
+    Fabricate(:status, account: account, visibility: :limited)
+  end
+
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
   end
 
   describe 'GET #show' do
-    before do
-      get :show, params: { account_username: account.username }
-    end
+    context 'without signature' do
+      let(:remote_account) { nil }
+
+      before do
+        get :show, params: { account_username: account.username, page: page }
+      end
+
+      context 'with page not requested' do
+        let(:page) { nil }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns totalItems' do
+          json = body_as_json
+          expect(json[:totalItems]).to eq 4
+        end
 
-    it 'returns http success' do
-      expect(response).to have_http_status(200)
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+      end
+
+      context 'with page requested' do
+        let(:page) { 'true' }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns orderedItems with public or unlisted statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 2
+          expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+      end
     end
 
-    it 'returns application/activity+json' do
-      expect(response.content_type).to eq 'application/activity+json'
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+      let(:page) { 'true' }
+
+      context 'when signed request account does not follow account' do
+        before do
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns orderedItems with public or unlisted statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 2
+          expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
+
+      context 'when signed request account follows account' do
+        before do
+          remote_account.follow!(account)
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns orderedItems with private statuses' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 3
+          expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:to].include?(account_followers_url(account, ActionMailer::Base.default_url_options)) }).to be true
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
+
+      context 'when signed request account is blocked' do
+        before do
+          account.block!(remote_account)
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns empty orderedItems' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 0
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
+
+      context 'when signed request account is domain blocked' do
+        before do
+          account.block_domain!(remote_account.domain)
+          get :show, params: { account_username: account.username, page: page }
+        end
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns empty orderedItems' do
+          json = body_as_json
+          expect(json[:orderedItems]).to be_an Array
+          expect(json[:orderedItems].size).to eq 0
+        end
+
+        it 'returns private Cache-Control header' do
+          expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+        end
+      end
     end
   end
 end
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb
new file mode 100644
index 000000000..a5ed14180
--- /dev/null
+++ b/spec/controllers/activitypub/replies_controller_spec.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ActivityPub::RepliesController, type: :controller do
+  let(:status) { Fabricate(:status, visibility: parent_visibility) }
+  let(:remote_account) { nil }
+
+  before do
+    allow(controller).to receive(:signed_request_account).and_return(remote_account)
+
+    Fabricate(:status, thread: status, visibility: :public)
+    Fabricate(:status, thread: status, visibility: :public)
+    Fabricate(:status, thread: status, visibility: :private)
+    Fabricate(:status, account: status.account, thread: status, visibility: :public)
+    Fabricate(:status, account: status.account, thread: status, visibility: :private)
+  end
+
+  describe 'GET #index' do
+    context 'with no signature' do
+      before do
+        get :index, params: { account_username: status.account.username, status_id: status.id }
+      end
+
+      context 'when status is public' do
+        let(:parent_visibility) { :public }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns application/activity+json' do
+          expect(response.content_type).to eq 'application/activity+json'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'returns items with account\'s own replies' do
+          json = body_as_json
+
+          expect(json[:first]).to be_a Hash
+          expect(json[:first][:items]).to be_an Array
+          expect(json[:first][:items].size).to eq 1
+          expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+        end
+      end
+
+      context 'when status is private' do
+        let(:parent_visibility) { :private }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when status is direct' do
+        let(:parent_visibility) { :direct }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+      let(:only_other_accounts) { nil }
+
+      context do
+        before do
+          get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts }
+        end
+
+        context 'when status is public' do
+          let(:parent_visibility) { :public }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns application/activity+json' do
+            expect(response.content_type).to eq 'application/activity+json'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          context 'without only_other_accounts' do
+            it 'returns items with account\'s own replies' do
+              json = body_as_json
+
+              expect(json[:first]).to be_a Hash
+              expect(json[:first][:items]).to be_an Array
+              expect(json[:first][:items].size).to eq 1
+              expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+            end
+          end
+
+          context 'with only_other_accounts' do
+            let(:only_other_accounts) { 'true' }
+
+            it 'returns items with other public or unlisted replies' do
+              json = body_as_json
+
+              expect(json[:first]).to be_a Hash
+              expect(json[:first][:items]).to be_an Array
+              expect(json[:first][:items].size).to eq 2
+              expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+            end
+          end
+        end
+
+        context 'when status is private' do
+          let(:parent_visibility) { :private }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is direct' do
+          let(:parent_visibility) { :direct }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when signed request account is blocked' do
+        before do
+          status.account.block!(remote_account)
+          get :index, params: { account_username: status.account.username, status_id: status.id }
+        end
+
+        context 'when status is public' do
+          let(:parent_visibility) { :public }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is private' do
+          let(:parent_visibility) { :private }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is direct' do
+          let(:parent_visibility) { :direct }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+
+      context 'when signed request account is domain blocked' do
+        before do
+          status.account.block_domain!(remote_account.domain)
+          get :index, params: { account_username: status.account.username, status_id: status.id }
+        end
+
+        context 'when status is public' do
+          let(:parent_visibility) { :public }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is private' do
+          let(:parent_visibility) { :private }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+
+        context 'when status is direct' do
+          let(:parent_visibility) { :direct }
+
+          it 'returns http not found' do
+            expect(response).to have_http_status(404)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 6905dae10..ba1f1370a 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -5,128 +5,821 @@ require 'rails_helper'
 describe StatusesController do
   render_views
 
-  describe '#show' do
-    context 'account is suspended' do
-      it 'returns gone' do
-        account = Fabricate(:account, suspended: true)
-        status = Fabricate(:status, account: account)
+  describe 'GET #show' do
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
 
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      before do
         get :show, params: { account_username: account.username, id: status.id }
+      end
 
+      it 'returns http gone' do
         expect(response).to have_http_status(410)
       end
     end
 
-    context 'status is not permitted' do
-      it 'raises ActiveRecord::RecordNotFound' do
-        user = Fabricate(:user)
-        status = Fabricate(:status)
-        status.account.block!(user.account)
+    context 'when status is a reblog' do
+      let(:original_account) { Fabricate(:account, domain: 'example.com') }
+      let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
+      let(:status) { Fabricate(:status, account: account, reblog: original_status) }
 
-        sign_in(user)
+      before do
         get :show, params: { account_username: status.account.username, id: status.id }
+      end
 
-        expect(response).to have_http_status(404)
+      it 'redirects to the original status' do
+        expect(response).to redirect_to(original_status.url)
       end
     end
 
-    context 'status is a reblog' do
-      it 'redirects to the original status' do
-        original_account = Fabricate(:account, domain: 'example.com')
-        original_status = Fabricate(:status, account: original_account, uri: 'tag:example.com,2017:foo', url: 'https://example.com/123')
-        status = Fabricate(:status, reblog: original_status)
+    context 'when status is public' do
+      before do
+        get :show, params: { account_username: status.account.username, id: status.id, format: format }
+      end
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+      context 'as HTML' do
+        let(:format) { 'html' }
 
-        expect(response).to redirect_to(original_status.url)
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns Link header' do
+          expect(response.headers['Link'].to_s).to include 'activity+json'
+        end
+
+        it 'returns Vary header' do
+          expect(response.headers['Vary']).to eq 'Accept'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'renders status' do
+          expect(response).to render_template(:show)
+          expect(response.body).to include status.text
+        end
+      end
+
+      context 'as JSON' do
+        let(:format) { 'json' }
+
+        it 'returns http success' do
+          expect(response).to have_http_status(200)
+        end
+
+        it 'returns Link header' do
+          expect(response.headers['Link'].to_s).to include 'activity+json'
+        end
+
+        it 'returns Vary header' do
+          expect(response.headers['Vary']).to eq 'Accept'
+        end
+
+        it 'returns public Cache-Control header' do
+          expect(response.headers['Cache-Control']).to include 'public'
+        end
+
+        it 'returns Content-Type header' do
+          expect(response.headers['Content-Type']).to include 'application/activity+json'
+        end
+
+        it 'renders ActivityPub Note object' do
+          json = body_as_json
+          expect(json[:content]).to include status.text
+        end
       end
     end
 
-    context 'account is not suspended and status is permitted' do
-      it 'assigns @account' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(assigns(:account)).to eq status.account
+    context 'when status is private' do
+      let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+      before do
+        get :show, params: { account_username: status.account.username, id: status.id, format: format }
       end
 
-      it 'assigns @status' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(assigns(:status)).to eq status
+      context 'as JSON' do
+        let(:format) { 'json' }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it 'assigns @ancestors for ancestors of the status if it is a reply' do
-        ancestor = Fabricate(:status)
-        status = Fabricate(:status, in_reply_to_id: ancestor.id)
+      context 'as HTML' do
+        let(:format) { 'html' }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'when status is direct' do
+      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
 
-        expect(assigns(:ancestors)).to eq [ancestor]
+      before do
+        get :show, params: { account_username: status.account.username, id: status.id, format: format }
       end
 
-      it 'assigns @ancestors for [] if it is not a reply' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(assigns(:ancestors)).to eq []
+      context 'as JSON' do
+        let(:format) { 'json' }
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it 'assigns @descendant_threads for a thread with several statuses' do
-        status = Fabricate(:status)
-        child = Fabricate(:status, in_reply_to_id: status.id)
-        grandchild = Fabricate(:status, in_reply_to_id: child.id)
+      context 'as HTML' do
+        let(:format) { 'html' }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+    end
+
+    context 'when signed-in' do
+      let(:user) { Fabricate(:user) }
 
-        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchild.id]
+      before do
+        sign_in(user)
       end
 
-      it 'assigns @descendant_threads for several threads sharing the same descendant' do
-        status = Fabricate(:status)
-        child = Fabricate(:status, in_reply_to_id: status.id)
-        grandchildren = 2.times.map { Fabricate(:status, in_reply_to_id: child.id) }
+      context 'when account blocks user' do
+        before do
+          account.block!(user.account)
+          get :show, params: { account_username: status.account.username, id: status.id }
+        end
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when status is public' do
+        before do
+          get :show, params: { account_username: status.account.username, id: status.id, format: format }
+        end
+
+        context 'as HTML' do
+          let(:format) { 'html' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
 
-        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchildren[0].id]
-        expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).to eq [grandchildren[1].id]
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns no Cache-Control header' do
+            expect(response.headers).to_not include 'Cache-Control'
+          end
+
+          it 'renders status' do
+            expect(response).to render_template(:show)
+            expect(response.body).to include status.text
+          end
+        end
+
+        context 'as JSON' do
+          let(:format) { 'json' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
+
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          it 'returns Content-Type header' do
+            expect(response.headers['Content-Type']).to include 'application/activity+json'
+          end
+
+          it 'renders ActivityPub Note object' do
+            json = body_as_json
+            expect(json[:content]).to include status.text
+          end
+        end
       end
 
-      it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do
-        stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1
-        status = Fabricate(:status)
-        child = Fabricate(:status, in_reply_to_id: status.id)
+      context 'when status is private' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        context 'when user is authorized to see it' do
+          before do
+            user.account.follow!(account)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
 
-        expect(assigns(:descendant_threads)).to eq []
-        expect(assigns(:max_descendant_thread_id)).to eq child.id
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
       end
 
-      it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do
-        stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2
-        status = Fabricate(:status)
-        child0 = Fabricate(:status, in_reply_to_id: status.id)
-        child1 = Fabricate(:status, in_reply_to_id: child0.id)
-        child2 = Fabricate(:status, in_reply_to_id: child0.id)
+      context 'when status is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
 
-        get :show, params: { account_username: status.account.username, id: status.id }
+        context 'when user is authorized to see it' do
+          before do
+            Fabricate(:mention, account: user.account, status: status)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
 
-        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).not_to include child1.id
-        expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).not_to include child2.id
-        expect(assigns(:descendant_threads)[0][:next_status].id).to eq child1.id
-        expect(assigns(:descendant_threads)[1][:next_status].id).to eq child2.id
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
       end
+    end
 
-      it 'returns a success' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
+    context 'with signature' do
+      let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+
+      before do
+        allow(controller).to receive(:signed_request_account).and_return(remote_account)
+      end
+
+      context 'when account blocks account' do
+        before do
+          account.block!(remote_account)
+          get :show, params: { account_username: status.account.username, id: status.id }
+        end
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when account domain blocks account' do
+        before do
+          account.block_domain!(remote_account.domain)
+          get :show, params: { account_username: status.account.username, id: status.id }
+        end
+
+        it 'returns http not found' do
+          expect(response).to have_http_status(404)
+        end
+      end
+
+      context 'when status is public' do
+        before do
+          get :show, params: { account_username: status.account.username, id: status.id, format: format }
+        end
+
+        context 'as HTML' do
+          let(:format) { 'html' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
+
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns no Cache-Control header' do
+            expect(response.headers).to_not include 'Cache-Control'
+          end
+
+          it 'renders status' do
+            expect(response).to render_template(:show)
+            expect(response.body).to include status.text
+          end
+        end
+
+        context 'as JSON' do
+          let(:format) { 'json' }
+
+          it 'returns http success' do
+            expect(response).to have_http_status(200)
+          end
+
+          it 'returns Link header' do
+            expect(response.headers['Link'].to_s).to include 'activity+json'
+          end
+
+          it 'returns Vary header' do
+            expect(response.headers['Vary']).to eq 'Accept'
+          end
+
+          it 'returns public Cache-Control header' do
+            expect(response.headers['Cache-Control']).to include 'public'
+          end
+
+          it 'returns Content-Type header' do
+            expect(response.headers['Content-Type']).to include 'application/activity+json'
+          end
+
+          it 'renders ActivityPub Note object' do
+            json = body_as_json
+            expect(json[:content]).to include status.text
+          end
+        end
+      end
+
+      context 'when status is private' do
+        let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+        context 'when user is authorized to see it' do
+          before do
+            remote_account.follow!(account)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
+      end
+
+      context 'when status is direct' do
+        let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+        context 'when user is authorized to see it' do
+          before do
+            Fabricate(:mention, account: remote_account, status: status)
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns no Cache-Control header' do
+              expect(response.headers).to_not include 'Cache-Control'
+            end
+
+            it 'renders status' do
+              expect(response).to render_template(:show)
+              expect(response.body).to include status.text
+            end
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http success' do
+              expect(response).to have_http_status(200)
+            end
+
+            it 'returns Link header' do
+              expect(response.headers['Link'].to_s).to include 'activity+json'
+            end
+
+            it 'returns Vary header' do
+              expect(response.headers['Vary']).to eq 'Accept'
+            end
+
+            it 'returns private Cache-Control header' do
+              expect(response.headers['Cache-Control']).to include 'private'
+            end
+
+            it 'returns Content-Type header' do
+              expect(response.headers['Content-Type']).to include 'application/activity+json'
+            end
+
+            it 'renders ActivityPub Note object' do
+              json = body_as_json
+              expect(json[:content]).to include status.text
+            end
+          end
+        end
+
+        context 'when user is not authorized to see it' do
+          before do
+            get :show, params: { account_username: status.account.username, id: status.id, format: format }
+          end
+
+          context 'as JSON' do
+            let(:format) { 'json' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+
+          context 'as HTML' do
+            let(:format) { 'html' }
+
+            it 'returns http not found' do
+              expect(response).to have_http_status(404)
+            end
+          end
+        end
+      end
+    end
+  end
+
+  describe 'GET #activity' do
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
+
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      before do
+        get :activity, params: { account_username: account.username, id: status.id }
+      end
+
+      it 'returns http gone' do
+        expect(response).to have_http_status(410)
+      end
+    end
+
+    context 'when status is public' do
+      pending
+    end
+
+    context 'when status is private' do
+      pending
+    end
+
+    context 'when status is direct' do
+      pending
+    end
+
+    context 'when signed-in' do
+      context 'when status is public' do
+        pending
+      end
+
+      context 'when status is private' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+
+      context 'when status is direct' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+    end
+
+    context 'with signature' do
+      context 'when status is public' do
+        pending
+      end
+
+      context 'when status is private' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+
+      context 'when status is direct' do
+        context 'when user is authorized to see it' do
+          pending
+        end
+
+        context 'when user is not authorized to see it' do
+          pending
+        end
+      end
+    end
+  end
+
+  describe 'GET #embed' do
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
+
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      before do
+        get :embed, params: { account_username: account.username, id: status.id }
+      end
+
+      it 'returns http gone' do
+        expect(response).to have_http_status(410)
+      end
+    end
+
+    context 'when status is a reblog' do
+      let(:original_account) { Fabricate(:account, domain: 'example.com') }
+      let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
+      let(:status) { Fabricate(:status, account: account, reblog: original_status) }
+
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when status is public' do
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http success' do
         expect(response).to have_http_status(200)
       end
 
-      it 'renders statuses/show' do
-        status = Fabricate(:status)
-        get :show, params: { account_username: status.account.username, id: status.id }
-        expect(response).to render_template 'statuses/show'
+      it 'returns Link header' do
+        expect(response.headers['Link'].to_s).to include 'activity+json'
+      end
+
+      it 'returns Vary header' do
+        expect(response.headers['Vary']).to eq 'Accept'
+      end
+
+      it 'returns public Cache-Control header' do
+        expect(response.headers['Cache-Control']).to include 'public'
+      end
+
+      it 'renders status' do
+        expect(response).to render_template(:embed)
+        expect(response.body).to include status.text
+      end
+    end
+
+    context 'when status is private' do
+      let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'when status is direct' do
+      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+      before do
+        get :embed, params: { account_username: status.account.username, id: status.id }
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
       end
     end
   end
diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb
index 6a9006c9f..f1f1c05f3 100644
--- a/spec/fabricators/status_pin_fabricator.rb
+++ b/spec/fabricators/status_pin_fabricator.rb
@@ -1,4 +1,4 @@
 Fabricator(:status_pin) do
   account
-  status
+  status { |attrs| Fabricate(:status, account: attrs[:account], visibility: :public) }
 end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 38537da44..02f533287 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -96,20 +96,16 @@ RSpec.describe Status, type: :model do
 
     context 'unless destroyed?' do
       context 'if reblog?' do
-        it 'returns "#{account.acct} shared #{reblog.account.acct}\'s: #{preview}"' do
+        it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do
           reblog = subject.reblog = other
-          preview = subject.text.slice(0, 10).split("\n")[0]
-          expect(subject.title).to(
-            eq "#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
-          )
+          expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}"
         end
       end
 
       context 'unless reblog?' do
-        it 'returns "#{account.acct}: #{preview}"' do
+        it 'returns "New status by #{account.acct}"' do
           subject.reblog = nil
-          preview = subject.text.slice(0, 20).split("\n")[0]
-          expect(subject.title).to eq "#{account.acct}: #{preview}"
+          expect(subject.title).to eq "New status by #{account.acct}"
         end
       end
     end
diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb
index 3af6a0689..ded05ffbc 100644
--- a/spec/services/fetch_resource_service_spec.rb
+++ b/spec/services/fetch_resource_service_spec.rb
@@ -21,7 +21,11 @@ RSpec.describe FetchResourceService, type: :service do
 
     context 'when OpenSSL::SSL::SSLError is raised' do
       before do
-        allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(OpenSSL::SSL::SSLError)
+        request = double()
+        allow(Request).to receive(:new).and_return(request)
+        allow(request).to receive(:add_headers)
+        allow(request).to receive(:on_behalf_of)
+        allow(request).to receive(:perform).and_raise(OpenSSL::SSL::SSLError)
       end
 
       it { is_expected.to be_nil }
@@ -29,7 +33,11 @@ RSpec.describe FetchResourceService, type: :service do
 
     context 'when HTTP::ConnectionError is raised' do
       before do
-        allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(HTTP::ConnectionError)
+        request = double()
+        allow(Request).to receive(:new).and_return(request)
+        allow(request).to receive(:add_headers)
+        allow(request).to receive(:on_behalf_of)
+        allow(request).to receive(:perform).and_raise(HTTP::ConnectionError)
       end
 
       it { is_expected.to be_nil }
diff --git a/yarn.lock b/yarn.lock
index 2d57ec0ba..5715672cb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1327,9 +1327,9 @@
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
 "@types/q@^1.5.1":
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18"
-  integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
+  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
 
 "@types/react@16.4.6":
   version "16.4.6"
@@ -1632,7 +1632,7 @@ ajv@^4.7.0:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1:
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.9.1:
   version "6.10.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
   integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==
@@ -1652,6 +1652,16 @@ ajv@^6.12.0:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ajv@^6.5.5:
+  version "6.12.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
+  integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
 alphanum-sort@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -1971,9 +1981,9 @@ aws-sign2@~0.7.0:
   integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
 
 aws4@^1.8.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
-  integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
 
 axios@^0.18.0:
   version "0.18.1"
@@ -2249,9 +2259,9 @@ bindings@^1.5.0:
     file-uri-to-path "1.0.0"
 
 bluebird@^3.5.5:
-  version "3.5.5"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
-  integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
 blurhash@^1.1.3:
   version "1.1.3"
@@ -2461,11 +2471,6 @@ buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
-builtin-modules@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
-  integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
-
 builtin-status-codes@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -2482,9 +2487,9 @@ bytes@3.1.0:
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
 cacache@^12.0.2, cacache@^12.0.3:
-  version "12.0.3"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390"
-  integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==
+  version "12.0.4"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c"
+  integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==
   dependencies:
     bluebird "^3.5.5"
     chownr "^1.1.1"
@@ -2703,16 +2708,11 @@ chokidar@^2.1.8:
   optionalDependencies:
     fsevents "^1.2.7"
 
-chownr@^1.1.1:
+chownr@^1.1.1, chownr@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
   integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
 
-chownr@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
-  integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==
-
 chrome-trace-event@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -2818,7 +2818,7 @@ co@^4.6.0:
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
   integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
 
-coa@~2.0.1:
+coa@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
   integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
@@ -2880,15 +2880,10 @@ color@^3.0.0:
     color-convert "^1.9.1"
     color-string "^1.5.2"
 
-colors@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
-  integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
-
 combined-stream@^1.0.6, combined-stream@~1.0.6:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
-  integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
   dependencies:
     delayed-stream "~1.0.0"
 
@@ -3052,9 +3047,9 @@ core-js-compat@^3.6.2:
     semver "7.0.0"
 
 core-js-pure@^3.0.0:
-  version "3.6.4"
-  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
-  integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
+  version "3.6.5"
+  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
+  integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
 
 core-js@^2.4.0:
   version "2.6.1"
@@ -3235,18 +3230,18 @@ css-loader@^3.4.2:
     postcss-value-parser "^4.0.2"
     schema-utils "^2.6.0"
 
-css-select-base-adapter@~0.1.0:
+css-select-base-adapter@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
   integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
 
 css-select@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede"
-  integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
+  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
   dependencies:
     boolbase "^1.0.0"
-    css-what "^2.1.2"
+    css-what "^3.2.1"
     domutils "^1.7.0"
     nth-check "^1.0.2"
 
@@ -3265,37 +3260,37 @@ css-system-font-keywords@^1.0.0:
   resolved "https://registry.yarnpkg.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz#85c6f086aba4eb32c571a3086affc434b84823ed"
   integrity sha1-hcbwhquk6zLFcaMIav/ENLhII+0=
 
-css-tree@1.0.0-alpha.28:
-  version "1.0.0-alpha.28"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f"
-  integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==
+css-tree@1.0.0-alpha.37:
+  version "1.0.0-alpha.37"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
+  integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
   dependencies:
-    mdn-data "~1.1.0"
-    source-map "^0.5.3"
+    mdn-data "2.0.4"
+    source-map "^0.6.1"
 
-css-tree@1.0.0-alpha.29:
-  version "1.0.0-alpha.29"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
-  integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==
+css-tree@1.0.0-alpha.39:
+  version "1.0.0-alpha.39"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
+  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
   dependencies:
-    mdn-data "~1.1.0"
-    source-map "^0.5.3"
+    mdn-data "2.0.6"
+    source-map "^0.6.1"
 
 css-unit-converter@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
   integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
 
-css-url-regex@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec"
-  integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=
-
-css-what@2.1, css-what@^2.1.2:
+css-what@2.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
   integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
 
+css-what@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1"
+  integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
+
 cssesc@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703"
@@ -3374,12 +3369,12 @@ cssnano@^4.1.10:
     is-resolvable "^1.0.0"
     postcss "^7.0.0"
 
-csso@^3.5.0:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b"
-  integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==
+csso@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
+  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
   dependencies:
-    css-tree "1.0.0-alpha.29"
+    css-tree "1.0.0-alpha.39"
 
 cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
   version "0.3.8"
@@ -3961,7 +3956,7 @@ error-stack-parser@^2.0.6:
   dependencies:
     stackframe "^1.1.1"
 
-es-abstract@^1.13.0, es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5, es-abstract@^1.5.1:
+es-abstract@^1.13.0, es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
   version "1.17.5"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
   integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
@@ -4524,9 +4519,9 @@ fast-deep-equal@^3.1.1:
   integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
 
 fast-json-stable-stringify@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
-  integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
 fast-levenshtein@~2.0.6:
   version "2.0.6"
@@ -4560,9 +4555,9 @@ fb-watchman@^2.0.0:
     bser "^2.0.0"
 
 figgy-pudding@^3.5.1:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
-  integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
+  integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
 
 figures@^1.3.5:
   version "1.7.0"
@@ -4722,9 +4717,9 @@ flat-cache@^2.0.1:
     write "1.0.3"
 
 flatted@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
-  integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
+  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
 
 flush-write-stream@^1.0.0:
   version "1.1.1"
@@ -4890,9 +4885,9 @@ functional-red-black-tree@^1.0.1:
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
 functions-have-names@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d"
-  integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ==
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91"
+  integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==
 
 gauge@~2.7.3:
   version "2.7.4"
@@ -5119,7 +5114,7 @@ har-schema@^2.0.0:
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
   integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
 
-har-validator@~5.1.0:
+har-validator@~5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
   integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
@@ -5275,9 +5270,9 @@ hoopy@^0.1.4:
   integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==
 
 hosted-git-info@^2.1.4:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
-  integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
+  version "2.8.8"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
+  integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
 
 hpack.js@^2.1.6:
   version "2.1.6"
@@ -5759,13 +5754,6 @@ is-buffer@^2.0.2:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
   integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
 
-is-builtin-module@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
-  integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
-  dependencies:
-    builtin-modules "^1.0.0"
-
 is-callable@^1.1.4, is-callable@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
@@ -6561,7 +6549,7 @@ js-string-escape@1.0.1:
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
-js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0:
+js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0:
   version "3.13.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
   integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@@ -6928,9 +6916,9 @@ lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11,
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
 loglevel@^1.6.6:
-  version "1.6.6"
-  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312"
-  integrity sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ==
+  version "1.6.8"
+  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
+  integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
 
 loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
   version "1.4.0"
@@ -7013,10 +7001,15 @@ md5.js@^1.3.4:
     inherits "^2.0.1"
     safe-buffer "^5.1.2"
 
-mdn-data@~1.1.0:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
-  integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==
+mdn-data@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
+  integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+
+mdn-data@2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
+  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
 
 media-typer@0.3.0:
   version "0.3.0"
@@ -7100,34 +7093,22 @@ miller-rabin@^4.0.0:
     bn.js "^4.0.0"
     brorand "^1.0.1"
 
-mime-db@1.40.0:
-  version "1.40.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
-  integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
+mime-db@1.44.0:
+  version "1.44.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+  integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
 
 "mime-db@>= 1.40.0 < 2":
   version "1.42.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac"
   integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==
 
-mime-db@~1.37.0:
-  version "1.37.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
-  integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
-
-mime-types@^2.1.12, mime-types@~2.1.19:
-  version "2.1.21"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
-  integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
+  version "2.1.27"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+  integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
   dependencies:
-    mime-db "~1.37.0"
-
-mime-types@~2.1.17, mime-types@~2.1.24:
-  version "2.1.24"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
-  integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
-  dependencies:
-    mime-db "1.40.0"
+    mime-db "1.44.0"
 
 mime@1.6.0:
   version "1.6.0"
@@ -7253,14 +7234,7 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@^0.5, mkdirp@^0.5.3, mkdirp@~0.5.1:
-  version "0.5.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512"
-  integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==
-  dependencies:
-    minimist "^1.2.5"
-
-mkdirp@^0.5.0, mkdirp@^0.5.1:
+mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
   integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -7272,10 +7246,10 @@ mkdirp@^1.0.4:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
-moo@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
-  integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==
+moo@^0.5.0:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
+  integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
 
 mousetrap@^1.5.2:
   version "1.6.5"
@@ -7360,12 +7334,12 @@ natural-compare@^1.4.0:
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
 nearley@^2.7.10:
-  version "2.16.0"
-  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7"
-  integrity sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg==
+  version "2.19.2"
+  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.2.tgz#40cafbf235121ae94b1aa1e585890d24fade182d"
+  integrity sha512-h6lygT0BWAGErDvoE2LfI+tDeY2+UUrqG5dcBPdCmjnjud9z1wE0P7ljb85iNbE93YA+xJLpoSYGMuUqhnSSSA==
   dependencies:
     commander "^2.19.0"
-    moo "^0.4.3"
+    moo "^0.5.0"
     railroad-diagrams "^1.0.0"
     randexp "0.4.6"
     semver "^5.4.1"
@@ -7484,12 +7458,12 @@ nopt@^4.0.1:
     osenv "^0.1.4"
 
 normalize-package-data@^2.3.2:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
-  integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+  integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
   dependencies:
     hosted-git-info "^2.1.4"
-    is-builtin-module "^1.0.0"
+    resolve "^1.10.0"
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
@@ -7666,13 +7640,13 @@ object.fromentries@^2.0.2:
     function-bind "^1.1.1"
     has "^1.0.3"
 
-object.getownpropertydescriptors@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
-  integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
   dependencies:
-    define-properties "^1.1.2"
-    es-abstract "^1.5.1"
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
 
 object.pick@^1.3.0:
   version "1.3.0"
@@ -7681,7 +7655,7 @@ object.pick@^1.3.0:
   dependencies:
     isobject "^3.0.1"
 
-object.values@^1.0.4, object.values@^1.1.0, object.values@^1.1.1:
+object.values@^1.1.0, object.values@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
   integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
@@ -8745,10 +8719,10 @@ prr@~1.0.1:
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
   integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
 
-psl@^1.1.24, psl@^1.1.28:
-  version "1.1.31"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
-  integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+psl@^1.1.28:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
 
 public-encrypt@^4.0.0:
   version "4.0.3"
@@ -8792,7 +8766,7 @@ punycode@1.3.2:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
   integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
 
-punycode@^1.2.4, punycode@^1.4.1:
+punycode@^1.2.4:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
   integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
@@ -9223,10 +9197,10 @@ read-pkg@^3.0.0:
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
-  integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.3"
@@ -9236,10 +9210,10 @@ read-pkg@^3.0.0:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
-readable-stream@^2.0.2, readable-stream@^2.0.6:
-  version "2.3.7"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
-  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+readable-stream@^2.0.1, readable-stream@^2.3.3:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+  integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.3"
@@ -9493,9 +9467,9 @@ request-promise-native@^1.0.5:
     tough-cookie ">=2.3.3"
 
 request@^2.87.0:
-  version "2.88.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
-  integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
   dependencies:
     aws-sign2 "~0.7.0"
     aws4 "^1.8.0"
@@ -9504,7 +9478,7 @@ request@^2.87.0:
     extend "~3.0.2"
     forever-agent "~0.6.1"
     form-data "~2.3.2"
-    har-validator "~5.1.0"
+    har-validator "~5.1.3"
     http-signature "~1.2.0"
     is-typedarray "~1.0.0"
     isstream "~0.1.2"
@@ -9514,7 +9488,7 @@ request@^2.87.0:
     performance-now "^2.1.0"
     qs "~6.5.2"
     safe-buffer "^5.1.2"
-    tough-cookie "~2.4.3"
+    tough-cookie "~2.5.0"
     tunnel-agent "^0.6.0"
     uuid "^3.3.2"
 
@@ -9621,10 +9595,10 @@ resolve@1.1.7:
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
   integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
 
-resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
-  version "1.15.1"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
-  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
+  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
   dependencies:
     path-parse "^1.0.6"
 
@@ -10168,7 +10142,7 @@ source-map@0.5.6:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
   integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
 
-source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
+source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -10187,9 +10161,9 @@ spdx-correct@^3.0.0:
     spdx-license-ids "^3.0.0"
 
 spdx-exceptions@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
-  integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
 
 spdx-expression-parse@^3.0.0:
   version "3.0.0"
@@ -10200,9 +10174,9 @@ spdx-expression-parse@^3.0.0:
     spdx-license-ids "^3.0.0"
 
 spdx-license-ids@^3.0.0:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e"
-  integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
+  integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
 
 spdy-transport@^3.0.0:
   version "3.0.0"
@@ -10276,7 +10250,7 @@ ssri@^7.0.0:
     figgy-pudding "^3.5.1"
     minipass "^3.1.1"
 
-stable@~0.1.6:
+stable@^0.1.8:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
@@ -10590,22 +10564,21 @@ supports-color@^7.0.0, supports-color@^7.1.0:
     has-flag "^4.0.0"
 
 svgo@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985"
-  integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
+  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
   dependencies:
-    coa "~2.0.1"
-    colors "~1.1.2"
+    chalk "^2.4.1"
+    coa "^2.0.2"
     css-select "^2.0.0"
-    css-select-base-adapter "~0.1.0"
-    css-tree "1.0.0-alpha.28"
-    css-url-regex "^1.1.0"
-    csso "^3.5.0"
-    js-yaml "^3.12.0"
+    css-select-base-adapter "^0.1.1"
+    css-tree "1.0.0-alpha.37"
+    csso "^4.0.2"
+    js-yaml "^3.13.1"
     mkdirp "~0.5.1"
-    object.values "^1.0.4"
+    object.values "^1.1.0"
     sax "~1.2.4"
-    stable "~0.1.6"
+    stable "^0.1.8"
     unquote "~1.1.1"
     util.promisify "~1.0.0"
 
@@ -10873,7 +10846,7 @@ toidentifier@1.0.0:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
   integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
 
-tough-cookie@>=2.3.3, tough-cookie@^2.3.4:
+tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
   integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -10881,14 +10854,6 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.4:
     psl "^1.1.28"
     punycode "^2.1.1"
 
-tough-cookie@~2.4.3:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
-  integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
-  dependencies:
-    psl "^1.1.24"
-    punycode "^1.4.1"
-
 tr46@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@@ -11104,12 +11069,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
 util.promisify@^1.0.0, util.promisify@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
-  integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
   dependencies:
-    define-properties "^1.1.2"
-    object.getownpropertydescriptors "^2.0.3"
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
 
 util@0.10.3:
   version "0.10.3"