From 4a61bbe1760f38474aa97d4bc0a38f228abc1075 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 13 Feb 2022 08:00:26 +0900
Subject: [PATCH 01/35] Bump rspec-rails from 5.0.2 to 5.1.0 (#17406)

Bumps [rspec-rails](https://github.com/rspec/rspec-rails) from 5.0.2 to 5.1.0.
- [Release notes](https://github.com/rspec/rspec-rails/releases)
- [Changelog](https://github.com/rspec/rspec-rails/blob/main/Changelog.md)
- [Commits](https://github.com/rspec/rspec-rails/compare/v5.0.2...v5.1.0)

---
updated-dependencies:
- dependency-name: rspec-rails
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      |  2 +-
 Gemfile.lock | 22 +++++++++++-----------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/Gemfile b/Gemfile
index 9b52c0e6c6..1a7193b19f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -103,7 +103,7 @@ group :development, :test do
   gem 'i18n-tasks', '~> 0.9', require: false
   gem 'pry-byebug', '~> 3.9'
   gem 'pry-rails', '~> 0.3'
-  gem 'rspec-rails', '~> 5.0'
+  gem 'rspec-rails', '~> 5.1'
 end
 
 group :production, :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index eee2890568..f0ba4efdc9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -181,7 +181,7 @@ GEM
     devise_pam_authenticatable2 (9.2.0)
       devise (>= 4.0.0)
       rpam2 (~> 4.0)
-    diff-lcs (1.4.4)
+    diff-lcs (1.5.0)
     discard (1.2.1)
       activerecord (>= 4.2, < 8)
     docile (1.3.4)
@@ -354,7 +354,7 @@ GEM
       activesupport (>= 4)
       railties (>= 4)
       request_store (~> 1.0)
-    loofah (2.13.0)
+    loofah (2.14.0)
       crass (~> 1.0.2)
       nokogiri (>= 1.5.9)
     mail (2.7.1)
@@ -510,15 +510,15 @@ GEM
       chunky_png (~> 1.0)
       rqrcode_core (~> 1.0)
     rqrcode_core (1.2.0)
-    rspec-core (3.10.1)
-      rspec-support (~> 3.10.0)
-    rspec-expectations (3.10.1)
+    rspec-core (3.11.0)
+      rspec-support (~> 3.11.0)
+    rspec-expectations (3.11.0)
       diff-lcs (>= 1.2.0, < 2.0)
-      rspec-support (~> 3.10.0)
-    rspec-mocks (3.10.2)
+      rspec-support (~> 3.11.0)
+    rspec-mocks (3.11.0)
       diff-lcs (>= 1.2.0, < 2.0)
-      rspec-support (~> 3.10.0)
-    rspec-rails (5.0.2)
+      rspec-support (~> 3.11.0)
+    rspec-rails (5.1.0)
       actionpack (>= 5.2)
       activesupport (>= 5.2)
       railties (>= 5.2)
@@ -529,7 +529,7 @@ GEM
     rspec-sidekiq (3.1.0)
       rspec-core (~> 3.0, >= 3.0.0)
       sidekiq (>= 2.4.0)
-    rspec-support (3.10.3)
+    rspec-support (3.11.0)
     rspec_junit_formatter (0.5.1)
       rspec-core (>= 2, < 4, != 2.12.0)
     rubocop (1.25.1)
@@ -770,7 +770,7 @@ DEPENDENCIES
   redis-namespace (~> 1.8)
   rexml (~> 3.2)
   rqrcode (~> 2.1)
-  rspec-rails (~> 5.0)
+  rspec-rails (~> 5.1)
   rspec-sidekiq (~> 3.1)
   rspec_junit_formatter (~> 0.5)
   rubocop (~> 1.25)

From b87c8538069a2763e407b93397d32379f2d5acc5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 13 Feb 2022 08:04:32 +0900
Subject: [PATCH 02/35] Bump annotate from 3.1.1 to 3.2.0 (#17503)

Bumps [annotate](https://github.com/ctran/annotate_models) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/ctran/annotate_models/releases)
- [Changelog](https://github.com/ctran/annotate_models/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/ctran/annotate_models/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: annotate
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Gemfile b/Gemfile
index 1a7193b19f..55ed06bb72 100644
--- a/Gemfile
+++ b/Gemfile
@@ -124,7 +124,7 @@ end
 
 group :development do
   gem 'active_record_query_trace', '~> 1.8'
-  gem 'annotate', '~> 3.1'
+  gem 'annotate', '~> 3.2'
   gem 'better_errors', '~> 2.9'
   gem 'binding_of_caller', '~> 1.0'
   gem 'bullet', '~> 7.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index f0ba4efdc9..84a28f9040 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -71,8 +71,8 @@ GEM
     airbrussh (1.4.0)
       sshkit (>= 1.6.1, != 1.7.0)
     android_key_attestation (0.3.0)
-    annotate (3.1.1)
-      activerecord (>= 3.2, < 7.0)
+    annotate (3.2.0)
+      activerecord (>= 3.2, < 8.0)
       rake (>= 10.4, < 14.0)
     ast (2.4.2)
     attr_encrypted (3.1.0)
@@ -682,7 +682,7 @@ DEPENDENCIES
   active_model_serializers (~> 0.10)
   active_record_query_trace (~> 1.8)
   addressable (~> 2.8)
-  annotate (~> 3.1)
+  annotate (~> 3.2)
   aws-sdk-s3 (~> 1.112)
   better_errors (~> 2.9)
   binding_of_caller (~> 1.0)

From f76dd51aa5dadc1799a1e6bd178247069d5acceb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 13 Feb 2022 08:05:22 +0900
Subject: [PATCH 03/35] Bump strong_migrations from 0.7.9 to 0.8.0 (#17504)

Bumps [strong_migrations](https://github.com/ankane/strong_migrations) from 0.7.9 to 0.8.0.
- [Release notes](https://github.com/ankane/strong_migrations/releases)
- [Changelog](https://github.com/ankane/strong_migrations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ankane/strong_migrations/compare/v0.7.9...v0.8.0)

---
updated-dependencies:
- dependency-name: strong_migrations
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Gemfile b/Gemfile
index 55ed06bb72..47fa180ca7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -85,7 +85,7 @@ gem 'simple-navigation', '~> 4.3'
 gem 'simple_form', '~> 5.1'
 gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
 gem 'stoplight', '~> 2.2.1'
-gem 'strong_migrations', '~> 0.7'
+gem 'strong_migrations', '~> 0.8'
 gem 'tty-prompt', '~> 0.23', require: false
 gem 'twitter-text', '~> 3.1.0'
 gem 'tzinfo-data', '~> 1.2021'
diff --git a/Gemfile.lock b/Gemfile.lock
index 84a28f9040..0e62f0a097 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -606,8 +606,8 @@ GEM
     stackprof (0.2.17)
     statsd-ruby (1.5.0)
     stoplight (2.2.1)
-    strong_migrations (0.7.9)
-      activerecord (>= 5)
+    strong_migrations (0.8.0)
+      activerecord (>= 5.2)
     temple (0.8.2)
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
@@ -789,7 +789,7 @@ DEPENDENCIES
   sprockets-rails (~> 3.4)
   stackprof
   stoplight (~> 2.2.1)
-  strong_migrations (~> 0.7)
+  strong_migrations (~> 0.8)
   thor (~> 1.2)
   tty-prompt (~> 0.23)
   twitter-text (~> 3.1.0)

From 62404668664e3349d87484f73b802f83efccbafa Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 13 Feb 2022 01:58:26 +0100
Subject: [PATCH 04/35] Fix duplicate accounts when searching by IP range in
 admin UI (#17524)

---
 app/models/account_filter.rb | 2 +-
 app/models/user.rb           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index dcb1741227..86b7f5f417 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -49,7 +49,7 @@ class AccountFilter
     when 'email'
       accounts_with_users.merge(User.matches_email(value))
     when 'ip'
-      valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
+      valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value).group('users.id, accounts.id')) : Account.none
     when 'invited_by'
       invited_by_scope(value)
     when 'order'
diff --git a/app/models/user.rb b/app/models/user.rb
index a11bc38248..fd1d7049a9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -111,7 +111,7 @@ class User < ApplicationRecord
   scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
   scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
   scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
-  scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value) }
+  scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') }
   scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
 
   before_validation :sanitize_languages

From bbd34744161fc46fa0e75d64e08a2f70d951bb40 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 13 Feb 2022 02:52:34 +0100
Subject: [PATCH 05/35] Fix privacy policy link not being visible on small
 screens (#17533)

Fix #17482
---
 app/javascript/styles/mastodon/footer.scss | 17 ++++++++++++++++-
 app/views/layouts/public.html.haml         |  4 ++++
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss
index 00d2908832..073ebda7e4 100644
--- a/app/javascript/styles/mastodon/footer.scss
+++ b/app/javascript/styles/mastodon/footer.scss
@@ -90,6 +90,20 @@
         .column-4 {
           display: none;
         }
+
+        .column-2 h4 {
+          display: none;
+        }
+      }
+    }
+
+    .legal-xs {
+      display: none;
+      text-align: center;
+      padding-top: 20px;
+
+      @media screen and (max-width: $no-gap-breakpoint) {
+        display: block;
       }
     }
 
@@ -105,7 +119,8 @@
       }
     }
 
-    ul a {
+    ul a,
+    .legal-xs a {
       text-decoration: none;
       color: lighten($ui-base-color, 34%);
 
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 069931cfd6..3a9ca7ed7b 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -54,5 +54,9 @@
             %ul
               %li= link_to t('about.source_code'), Mastodon::Version.source_url
               %li= link_to t('about.apps'), 'https://joinmastodon.org/apps'
+        .legal-xs
+          = link_to "v#{Mastodon::Version.to_s}", Mastodon::Version.source_url
+          ·
+          = link_to t('about.privacy_policy'), terms_path
 
 = render template: 'layouts/application'

From 901bbf2e5fd83a4aadbb6e28f305e30a952e1939 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 13 Feb 2022 20:21:09 +0900
Subject: [PATCH 06/35] Bump kt-paperclip from 7.0.1 to 7.1.1 (#17502)

Bumps [kt-paperclip](https://github.com/kreeti/kt-paperclip) from 7.0.1 to 7.1.1.
- [Release notes](https://github.com/kreeti/kt-paperclip/releases)
- [Changelog](https://github.com/kreeti/kt-paperclip/blob/master/NEWS)
- [Commits](https://github.com/kreeti/kt-paperclip/compare/v7.0.1...v7.1.1)

---
updated-dependencies:
- dependency-name: kt-paperclip
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Gemfile b/Gemfile
index 47fa180ca7..c5bf5c73e0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -21,7 +21,7 @@ gem 'dotenv-rails', '~> 2.7'
 gem 'aws-sdk-s3', '~> 1.112', require: false
 gem 'fog-core', '<= 2.1.0'
 gem 'fog-openstack', '~> 0.3', require: false
-gem 'kt-paperclip', '~> 7.0'
+gem 'kt-paperclip', '~> 7.1'
 gem 'blurhash', '~> 0.1'
 
 gem 'active_model_serializers', '~> 0.10'
diff --git a/Gemfile.lock b/Gemfile.lock
index 0e62f0a097..9314fc27e7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -330,7 +330,7 @@ GEM
       activerecord
       kaminari-core (= 1.2.2)
     kaminari-core (1.2.2)
-    kt-paperclip (7.0.1)
+    kt-paperclip (7.1.1)
       activemodel (>= 4.2.0)
       activesupport (>= 4.2.0)
       marcel (~> 1.0.1)
@@ -361,7 +361,7 @@ GEM
       mini_mime (>= 0.1.1)
     makara (0.5.1)
       activerecord (>= 5.2.0)
-    marcel (1.0.1)
+    marcel (1.0.2)
     mario-redis-lock (1.2.1)
       redis (>= 3.0.5)
     matrix (0.4.2)
@@ -372,7 +372,7 @@ GEM
       nokogiri (~> 1.10)
     mime-types (3.4.1)
       mime-types-data (~> 3.2015)
-    mime-types-data (3.2021.1115)
+    mime-types-data (3.2022.0105)
     mini_mime (1.1.2)
     mini_portile2 (2.7.1)
     minitest (5.15.0)
@@ -728,7 +728,7 @@ DEPENDENCIES
   json-ld
   json-ld-preloaded (~> 3.2)
   kaminari (~> 1.2)
-  kt-paperclip (~> 7.0)
+  kt-paperclip (~> 7.1)
   letter_opener (~> 1.7)
   letter_opener_web (~> 2.0)
   link_header (~> 0.0)

From 9a015e43ef173a3afc93cc1d06fd7f76adb8876a Mon Sep 17 00:00:00 2001
From: Jeong Arm <kjwonmail@gmail.com>
Date: Mon, 14 Feb 2022 08:17:09 +0900
Subject: [PATCH 07/35] Add `from:` query operator to search syntax (#16526)

* Add 'by:userhandle' parameter to search api

* Use search syntax for "by" prefix

* Codeclimate

* Use 'from' instead of 'by'
---
 app/lib/search_query_transformer.rb | 37 +++++++++++++++++++++++++++--
 1 file changed, 35 insertions(+), 2 deletions(-)

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index e07ebfffed..c685d7b6fd 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -2,19 +2,21 @@
 
 class SearchQueryTransformer < Parslet::Transform
   class Query
-    attr_reader :should_clauses, :must_not_clauses, :must_clauses
+    attr_reader :should_clauses, :must_not_clauses, :must_clauses, :filter_clauses
 
     def initialize(clauses)
       grouped = clauses.chunk(&:operator).to_h
       @should_clauses = grouped.fetch(:should, [])
       @must_not_clauses = grouped.fetch(:must_not, [])
       @must_clauses = grouped.fetch(:must, [])
+      @filter_clauses = grouped.fetch(:filter, [])
     end
 
     def apply(search)
       should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) }
       must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) }
       must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) }
+      filter_clauses.each { |clause| search = search.filter(**clause_to_filter(clause)) }
       search.query.minimum_should_match(1)
     end
 
@@ -30,6 +32,15 @@ class SearchQueryTransformer < Parslet::Transform
         raise "Unexpected clause type: #{clause}"
       end
     end
+
+    def clause_to_filter(clause)
+      case clause
+      when PrefixClause
+        { term: { clause.filter => clause.term } }
+      else
+        raise "Unexpected clause type: #{clause}"
+      end
+    end
   end
 
   class Operator
@@ -69,11 +80,33 @@ class SearchQueryTransformer < Parslet::Transform
     end
   end
 
+  class PrefixClause
+    attr_reader :filter, :operator, :term
+
+    def initialize(prefix, term)
+      @operator = :filter
+      case prefix
+      when 'from'
+        @filter = :account_id
+        username, domain = term.split('@')
+        account = Account.find_remote(username, domain)
+
+        raise "Account not found: #{term}" unless account
+
+        @term = account.id
+      else
+        raise "Unknown prefix: #{prefix}"
+      end
+    end
+  end
+
   rule(clause: subtree(:clause)) do
     prefix   = clause[:prefix][:term].to_s if clause[:prefix]
     operator = clause[:operator]&.to_s
 
-    if clause[:term]
+    if clause[:prefix]
+      PrefixClause.new(prefix, clause[:term].to_s)
+    elsif clause[:term]
       TermClause.new(prefix, operator, clause[:term].to_s)
     elsif clause[:shortcode]
       TermClause.new(prefix, operator, ":#{clause[:term]}:")

From d39df35441d7e4b39a57402e554ed92b1ec2e1f1 Mon Sep 17 00:00:00 2001
From: Jeong Arm <kjwonmail@gmail.com>
Date: Tue, 15 Feb 2022 00:07:04 +0900
Subject: [PATCH 08/35] Fix admin statuses page order with media (#17538)

---
 app/models/admin/status_filter.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb
index ce5bb5f461..4fba612a65 100644
--- a/app/models/admin/status_filter.rb
+++ b/app/models/admin/status_filter.rb
@@ -31,7 +31,7 @@ class Admin::StatusFilter
   def scope_for(key, value)
     case key.to_s
     when 'media'
-      Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
+      Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc')
     when 'id'
       Status.where(id: value)
     else

From 298491a8163fe2a015ac7b34b66b606a06e9798c Mon Sep 17 00:00:00 2001
From: Yamagishi Kazutoshi <ykzts@desire.sh>
Date: Tue, 15 Feb 2022 00:08:02 +0900
Subject: [PATCH 09/35] Remove protobuf dependencies (#17539)

---
 .circleci/config.yml              | 2 +-
 .github/workflows/build-image.yml | 6 +++++-
 .github/workflows/check-i18n.yml  | 2 +-
 Aptfile                           | 2 --
 Dockerfile                        | 4 ++--
 Vagrantfile                       | 2 --
 6 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 751ca95b18..a9ad921457 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -32,7 +32,7 @@ commands:
           name: Install system dependencies
           command: |
             sudo apt-get update
-            sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+            sudo apt-get install -y libicu-dev libidn11-dev
   install-ruby-dependencies:
     parameters:
       ruby-version:
diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml
index 0aaea6b1fe..92a164c40e 100644
--- a/.github/workflows/build-image.yml
+++ b/.github/workflows/build-image.yml
@@ -6,6 +6,10 @@ on:
       - "main"
     tags:
       - "*"
+  pull_request:
+    paths:
+      - .github/workflows/build-image.yml
+      - Dockerfile
 jobs:
   build-image:
     runs-on: ubuntu-latest
@@ -30,7 +34,7 @@ jobs:
         with:
           context: .
           platforms: linux/amd64,linux/arm64
-          push: true
+          push: ${{ github.event_name != 'pull_request' }}
           tags: ${{ steps.meta.outputs.tags }}
           cache-from: type=registry,ref=tootsuite/mastodon:latest
           cache-to: type=inline
diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml
index 2e8f230f31..9cb98dd125 100644
--- a/.github/workflows/check-i18n.yml
+++ b/.github/workflows/check-i18n.yml
@@ -18,7 +18,7 @@ jobs:
     - name: Install system dependencies
       run: |
         sudo apt-get update
-        sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+        sudo apt-get install -y libicu-dev libidn11-dev
     - name: Set up Ruby
       uses: ruby/setup-ruby@v1
       with:
diff --git a/Aptfile b/Aptfile
index b2cbad714d..9235141ad5 100644
--- a/Aptfile
+++ b/Aptfile
@@ -4,10 +4,8 @@ libicu-dev
 libidn11
 libidn11-dev
 libpq-dev
-libprotobuf-dev
 libxdamage1
 libxfixes3
-protobuf-compiler
 zlib1g-dev
 libcairo2
 libcroco3
diff --git a/Dockerfile b/Dockerfile
index c6287b5a7a..1b3661561e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -51,7 +51,7 @@ RUN npm install -g npm@latest && \
 	gem install bundler && \
 	apt-get update && \
 	apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
-	libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info
+	libpq-dev shared-mime-info
 
 COPY Gemfile* package.json yarn.lock /opt/mastodon/
 
@@ -88,7 +88,7 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio
 RUN apt-get update && \
   apt-get -y --no-install-recommends install \
 	  libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
-	  libicu66 libprotobuf17 libidn11 libyaml-0-2 \
+	  libicu66 libidn11 libyaml-0-2 \
 	  file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
 	ln -s /opt/mastodon /mastodon && \
 	gem install bundler && \
diff --git a/Vagrantfile b/Vagrantfile
index e086ddd987..3e73d9e470 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -33,11 +33,9 @@ sudo apt-get install \
   redis-tools \
   postgresql \
   postgresql-contrib \
-  protobuf-compiler \
   yarn \
   libicu-dev \
   libidn11-dev \
-  libprotobuf-dev \
   libreadline-dev \
   libpam0g-dev \
   -y

From 5be705e1e0e3c05486c6069a7c8387c123a6d405 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 14 Feb 2022 16:08:23 +0100
Subject: [PATCH 10/35] Revert "Bump strong_migrations from 0.7.9 to 0.8.0
 (#17504)" (#17540)

This reverts commit f76dd51aa5dadc1799a1e6bd178247069d5acceb.
---
 Gemfile      | 2 +-
 Gemfile.lock | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Gemfile b/Gemfile
index c5bf5c73e0..ee5395bfcd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -85,7 +85,7 @@ gem 'simple-navigation', '~> 4.3'
 gem 'simple_form', '~> 5.1'
 gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
 gem 'stoplight', '~> 2.2.1'
-gem 'strong_migrations', '~> 0.8'
+gem 'strong_migrations', '~> 0.7'
 gem 'tty-prompt', '~> 0.23', require: false
 gem 'twitter-text', '~> 3.1.0'
 gem 'tzinfo-data', '~> 1.2021'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9314fc27e7..71e8cf4cd4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -606,8 +606,8 @@ GEM
     stackprof (0.2.17)
     statsd-ruby (1.5.0)
     stoplight (2.2.1)
-    strong_migrations (0.8.0)
-      activerecord (>= 5.2)
+    strong_migrations (0.7.9)
+      activerecord (>= 5)
     temple (0.8.2)
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
@@ -789,7 +789,7 @@ DEPENDENCIES
   sprockets-rails (~> 3.4)
   stackprof
   stoplight (~> 2.2.1)
-  strong_migrations (~> 0.8)
+  strong_migrations (~> 0.7)
   thor (~> 1.2)
   tty-prompt (~> 0.23)
   twitter-text (~> 3.1.0)

From 564efd06515edc524a8a1cdf7a3d8a7d9a376c04 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 14 Feb 2022 21:27:53 +0100
Subject: [PATCH 11/35] Add appeals (#17364)

* Add appeals

* Add ability to reject appeals and ability to browse pending appeals in admin UI

* Add strikes to account page in settings

* Various fixes and improvements

- Add separate notification setting for appeals, separate from reports
- Fix style of links in report/strike header
- Change approving an appeal to not restore statuses (due to federation complexities)
- Change style of successfully appealed strikes on account settings page
- Change account settings page to only show unappealed or recently appealed strikes

* Change appealed_at to overruled_at

* Fix missing method error
---
 app/controllers/admin/accounts_controller.rb  |   4 +-
 app/controllers/admin/dashboard_controller.rb |   1 +
 .../admin/disputes/appeals_controller.rb      |  40 ++++++
 .../auth/registrations_controller.rb          |  11 +-
 .../disputes/appeals_controller.rb            |  25 ++++
 app/controllers/disputes/base_controller.rb   |  18 +++
 .../disputes/strikes_controller.rb            |  17 +++
 .../admin/account_moderation_notes_helper.rb  |   4 +-
 app/helpers/admin/action_logs_helper.rb       |   2 +
 app/helpers/admin/trends/statuses_helper.rb   |  20 +++
 app/javascript/styles/mastodon/admin.scss     |  89 +++++++++++-
 app/mailers/admin_mailer.rb                   |  10 ++
 app/mailers/user_mailer.rb                    |  20 +++
 app/models/account.rb                         |   4 +
 app/models/account_filter.rb                  |   2 +
 app/models/account_warning.rb                 |   8 +-
 app/models/admin/action_log_filter.rb         |   2 +
 app/models/admin/appeal_filter.rb             |  49 +++++++
 app/models/appeal.rb                          |  58 ++++++++
 app/models/user.rb                            |   4 +
 app/policies/account_warning_policy.rb        |  17 +++
 app/policies/appeal_policy.rb                 |  13 ++
 app/services/appeal_service.rb                |  28 ++++
 app/services/approve_appeal_service.rb        |  74 ++++++++++
 .../_account_moderation_note.html.haml        |   7 -
 .../_account_warning.html.haml                |  30 ++++-
 app/views/admin/accounts/show.html.haml       |  21 ++-
 app/views/admin/dashboard/index.html.haml     |   3 +
 .../admin/disputes/appeals/_appeal.html.haml  |  21 +++
 .../admin/disputes/appeals/index.html.haml    |  22 +++
 .../admin/report_notes/_report_note.html.haml |   2 +-
 app/views/admin/reports/show.html.haml        |   2 +-
 app/views/admin_mailer/new_appeal.text.erb    |   9 ++
 .../registrations/_account_warning.html.haml  |  20 +++
 .../auth/registrations/_status.html.haml      |  31 ++---
 app/views/disputes/strikes/show.html.haml     | 127 ++++++++++++++++++
 .../preferences/notifications/show.html.haml  |   1 +
 app/views/statuses/_detailed_status.html.haml |   2 +-
 .../user_mailer/appeal_approved.html.haml     |  59 ++++++++
 .../user_mailer/appeal_approved.text.erb      |   7 +
 .../user_mailer/appeal_rejected.html.haml     |  59 ++++++++
 .../user_mailer/appeal_rejected.text.erb      |   7 +
 app/views/user_mailer/warning.html.haml       |   6 +-
 config/brakeman.ignore                        |  64 +++++----
 config/locales/en.yml                         |  77 ++++++++++-
 config/locales/simple_form.en.yml             |   9 +-
 config/navigation.rb                          |   4 +-
 config/routes.rb                              |  15 +++
 config/settings.yml                           |   1 +
 db/migrate/20220124141035_create_appeals.rb   |  14 ++
 ...19_add_overruled_at_to_account_warnings.rb |   5 +
 db/schema.rb                                  |  23 +++-
 .../admin/disputes/appeals_controller_spec.rb |  53 ++++++++
 .../disputes/appeals_controller_spec.rb       |  27 ++++
 .../disputes/strikes_controller_spec.rb       |  30 +++++
 .../fabricators/account_warning_fabricator.rb |   7 +-
 spec/fabricators/appeal_fabricator.rb         |   5 +
 spec/mailers/previews/admin_mailer_preview.rb |   5 +
 spec/mailers/previews/user_mailer_preview.rb  |   5 +
 spec/models/appeal_spec.rb                    |   5 +
 60 files changed, 1212 insertions(+), 93 deletions(-)
 create mode 100644 app/controllers/admin/disputes/appeals_controller.rb
 create mode 100644 app/controllers/disputes/appeals_controller.rb
 create mode 100644 app/controllers/disputes/base_controller.rb
 create mode 100644 app/controllers/disputes/strikes_controller.rb
 create mode 100644 app/helpers/admin/trends/statuses_helper.rb
 create mode 100644 app/models/admin/appeal_filter.rb
 create mode 100644 app/models/appeal.rb
 create mode 100644 app/policies/account_warning_policy.rb
 create mode 100644 app/policies/appeal_policy.rb
 create mode 100644 app/services/appeal_service.rb
 create mode 100644 app/services/approve_appeal_service.rb
 delete mode 100644 app/views/admin/account_moderation_notes/_account_moderation_note.html.haml
 create mode 100644 app/views/admin/disputes/appeals/_appeal.html.haml
 create mode 100644 app/views/admin/disputes/appeals/index.html.haml
 create mode 100644 app/views/admin_mailer/new_appeal.text.erb
 create mode 100644 app/views/auth/registrations/_account_warning.html.haml
 create mode 100644 app/views/disputes/strikes/show.html.haml
 create mode 100644 app/views/user_mailer/appeal_approved.html.haml
 create mode 100644 app/views/user_mailer/appeal_approved.text.erb
 create mode 100644 app/views/user_mailer/appeal_rejected.html.haml
 create mode 100644 app/views/user_mailer/appeal_rejected.text.erb
 create mode 100644 db/migrate/20220124141035_create_appeals.rb
 create mode 100644 db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb
 create mode 100644 spec/controllers/admin/disputes/appeals_controller_spec.rb
 create mode 100644 spec/controllers/disputes/appeals_controller_spec.rb
 create mode 100644 spec/controllers/disputes/strikes_controller_spec.rb
 create mode 100644 spec/fabricators/appeal_fabricator.rb
 create mode 100644 spec/models/appeal_spec.rb

diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e7f56e243b..e0ae71b9f2 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -28,7 +28,7 @@ module Admin
       @deletion_request        = @account.deletion_request
       @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
       @moderation_notes        = @account.targeted_moderation_notes.latest
-      @warnings                = @account.strikes.custom.latest
+      @warnings                = @account.strikes.includes(:target_account, :account, :appeal).latest
       @domain_block            = DomainBlock.rule_for(@account.domain)
     end
 
@@ -146,7 +146,7 @@ module Admin
     end
 
     def filter_params
-      params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
+      params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS)
     end
 
     def form_account_batch_params
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index f0a9354110..e376baab22 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -8,6 +8,7 @@ module Admin
       @pending_users_count   = User.pending.count
       @pending_reports_count = Report.unresolved.count
       @pending_tags_count    = Tag.pending_review.count
+      @pending_appeals_count = Appeal.pending.count
     end
 
     private
diff --git a/app/controllers/admin/disputes/appeals_controller.rb b/app/controllers/admin/disputes/appeals_controller.rb
new file mode 100644
index 0000000000..32e5e2f6fd
--- /dev/null
+++ b/app/controllers/admin/disputes/appeals_controller.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class Admin::Disputes::AppealsController < Admin::BaseController
+  before_action :set_appeal, except: :index
+
+  def index
+    authorize :appeal, :index?
+
+    @appeals = filtered_appeals.page(params[:page])
+  end
+
+  def approve
+    authorize @appeal, :approve?
+    log_action :approve, @appeal
+    ApproveAppealService.new.call(@appeal, current_account)
+    redirect_to disputes_strike_path(@appeal.strike)
+  end
+
+  def reject
+    authorize @appeal, :approve?
+    log_action :reject, @appeal
+    @appeal.reject!(current_account)
+    UserMailer.appeal_rejected(@appeal.account.user, @appeal)
+    redirect_to disputes_strike_path(@appeal.strike)
+  end
+
+  private
+
+  def filtered_appeals
+    Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account)
+  end
+
+  def filter_params
+    params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS)
+  end
+
+  def set_appeal
+    @appeal = Appeal.find(params[:id])
+  end
+end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index f37e906fde..3b025838b0 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   before_action :check_enabled_registrations, only: [:new, :create]
   before_action :configure_sign_up_params, only: [:create]
   before_action :set_sessions, only: [:edit, :update]
+  before_action :set_strikes, only: [:edit, :update]
   before_action :set_instance_presenter, only: [:new, :create, :update]
   before_action :set_body_classes, only: [:new, :create, :edit, :update]
   before_action :require_not_suspended!, only: [:update]
@@ -111,8 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   end
 
   def set_invite
-    invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
-    @invite = invite&.valid_for_use? ? invite : nil
+    @invite = begin
+      invite = Invite.find_by(code: invite_code) if invite_code.present?
+      invite if invite&.valid_for_use?
+    end
   end
 
   def determine_layout
@@ -123,6 +126,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
     @sessions = current_user.session_activations
   end
 
+  def set_strikes
+    @strikes = current_account.strikes.active.latest
+  end
+
   def require_not_suspended!
     forbidden if current_account.suspended?
   end
diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb
new file mode 100644
index 0000000000..15367c8792
--- /dev/null
+++ b/app/controllers/disputes/appeals_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Disputes::AppealsController < Disputes::BaseController
+  before_action :set_strike
+
+  def create
+    authorize @strike, :appeal?
+
+    @appeal = AppealService.new.call(@strike, appeal_params[:text])
+
+    redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
+  rescue ActiveRecord::RecordInvalid
+    render template: 'disputes/strikes/show'
+  end
+
+  private
+
+  def set_strike
+    @strike = current_account.strikes.find(params[:strike_id])
+  end
+
+  def appeal_params
+    params.require(:appeal).permit(:text)
+  end
+end
diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb
new file mode 100644
index 0000000000..865146b5cc
--- /dev/null
+++ b/app/controllers/disputes/base_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Disputes::BaseController < ApplicationController
+  include Authorization
+
+  layout 'admin'
+
+  skip_before_action :require_functional!
+
+  before_action :set_body_classes
+  before_action :authenticate_user!
+
+  private
+
+  def set_body_classes
+    @body_classes = 'admin'
+  end
+end
diff --git a/app/controllers/disputes/strikes_controller.rb b/app/controllers/disputes/strikes_controller.rb
new file mode 100644
index 0000000000..d41c5c727e
--- /dev/null
+++ b/app/controllers/disputes/strikes_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class Disputes::StrikesController < Disputes::BaseController
+  before_action :set_strike
+
+  def show
+    authorize @strike, :show?
+
+    @appeal = @strike.appeal || @strike.build_appeal
+  end
+
+  private
+
+  def set_strike
+    @strike = AccountWarning.find(params[:id])
+  end
+end
diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb
index 40b2a52892..2f08538ca6 100644
--- a/app/helpers/admin/account_moderation_notes_helper.rb
+++ b/app/helpers/admin/account_moderation_notes_helper.rb
@@ -1,10 +1,10 @@
 # frozen_string_literal: true
 
 module Admin::AccountModerationNotesHelper
-  def admin_account_link_to(account)
+  def admin_account_link_to(account, path: nil)
     return if account.nil?
 
-    link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
+    link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
       safe_join([
                   image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
                   content_tag(:span, account.acct, class: 'username'),
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index f3aa4be4f2..47eeeaac3a 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -33,6 +33,8 @@ module Admin::ActionLogsHelper
       "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
     when 'Instance'
       record.domain
+    when 'Appeal'
+      link_to record.account.acct, disputes_strike_path(record.strike)
     end
   end
 
diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb
new file mode 100644
index 0000000000..d16e3dd121
--- /dev/null
+++ b/app/helpers/admin/trends/statuses_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Admin::Trends::StatusesHelper
+  def one_line_preview(status)
+    text = begin
+      if status.local?
+        status.text.split("\n").first
+      else
+        Nokogiri::HTML(status.text).css('html > body > *').first&.text
+      end
+    end
+
+    return '' if text.blank?
+
+    html = Formatter.instance.send(:encode, text)
+    html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?)
+
+    html.html_safe # rubocop:disable Rails/OutputSafety
+  end
+end
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 546de16400..f5741bd502 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -578,12 +578,16 @@ body,
 }
 
 .log-entry {
+  display: block;
   line-height: 20px;
   padding: 15px;
   padding-left: 15px * 2 + 40px;
   background: $ui-base-color;
   border-bottom: 1px solid darken($ui-base-color, 8%);
   position: relative;
+  text-decoration: none;
+  color: $darker-text-color;
+  font-size: 14px;
 
   &:first-child {
     border-top-left-radius: 4px;
@@ -596,15 +600,12 @@ body,
     border-bottom: 0;
   }
 
-  &:hover {
+  &:hover,
+  &:focus,
+  &:active {
     background: lighten($ui-base-color, 4%);
   }
 
-  &__header {
-    color: $darker-text-color;
-    font-size: 14px;
-  }
-
   &__avatar {
     position: absolute;
     left: 15px;
@@ -640,6 +641,18 @@ body,
       text-decoration: underline;
     }
   }
+
+  &--inactive {
+    .log-entry__title {
+      text-decoration: line-through;
+    }
+
+    a,
+    .username,
+    .target {
+      color: $darker-text-color;
+    }
+  }
 }
 
 a.name-tag,
@@ -1175,6 +1188,17 @@ a.sparkline {
         font-weight: 600;
         padding: 4px 0;
       }
+
+      a {
+        color: $ui-highlight-color;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
     }
 
     &--horizontal {
@@ -1451,3 +1475,56 @@ a.sparkline {
     }
   }
 }
+
+.strike-card {
+  padding: 15px;
+  border-radius: 4px;
+  background: $ui-base-color;
+  font-size: 15px;
+  line-height: 20px;
+  word-wrap: break-word;
+  font-weight: 400;
+  color: $primary-text-color;
+
+  p {
+    margin-bottom: 20px;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  &__statuses-list {
+    border-radius: 4px;
+    border: 1px solid darken($ui-base-color, 8%);
+    font-size: 13px;
+    line-height: 18px;
+    overflow: hidden;
+
+    &__item {
+      padding: 16px;
+      background: lighten($ui-base-color, 2%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &__meta {
+        color: $darker-text-color;
+      }
+
+      a {
+        color: inherit;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index b23bd1296f..a9d00c000d 100644
--- a/app/mailers/admin_mailer.rb
+++ b/app/mailers/admin_mailer.rb
@@ -15,6 +15,16 @@ class AdminMailer < ApplicationMailer
     end
   end
 
+  def new_appeal(recipient, appeal)
+    @appeal   = appeal
+    @me       = recipient
+    @instance = Rails.configuration.x.local_domain
+
+    locale_for_account(@me) do
+      mail to: @me.user_email, subject: I18n.t('admin_mailer.new_appeal.subject', instance: @instance, username: @appeal.account.username)
+    end
+  end
+
   def new_pending_account(recipient, user)
     @account  = user.account
     @me       = recipient
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 5221a48928..583c948b0e 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -173,6 +173,26 @@ class UserMailer < Devise::Mailer
     end
   end
 
+  def appeal_approved(user, appeal)
+    @resource = user
+    @instance = Rails.configuration.x.local_domain
+    @appeal   = appeal
+
+    I18n.with_locale(@resource.locale || I18n.default_locale) do
+      mail to: @resource.email, subject: I18n.t('user_mailer.appeal_approved.subject', date: l(@appeal.created_at))
+    end
+  end
+
+  def appeal_rejected(user, appeal)
+    @resource = user
+    @instance = Rails.configuration.x.local_domain
+    @appeal   = appeal
+
+    I18n.with_locale(@resource.locale || I18n.default_locale) do
+      mail to: @resource.email, subject: I18n.t('user_mailer.appeal_rejected.subject', date: l(@appeal.created_at))
+    end
+  end
+
   def sign_in_token(user, remote_ip, user_agent, timestamp)
     @resource   = user
     @instance   = Rails.configuration.x.local_domain
diff --git a/app/models/account.rb b/app/models/account.rb
index 771cc0b1ba..2ad45feda0 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -270,6 +270,10 @@ class Account < ApplicationRecord
     true
   end
 
+  def previous_strikes_count
+    strikes.where(overruled_at: nil).count
+  end
+
   def keypair
     @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
   end
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 86b7f5f417..9da1522dd1 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -24,6 +24,8 @@ class AccountFilter
     scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil)
 
     params.each do |key, value|
+      next if key.to_s == 'page'
+
       scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
     end
 
diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb
index fc0d988fdc..05d01942df 100644
--- a/app/models/account_warning.rb
+++ b/app/models/account_warning.rb
@@ -12,6 +12,7 @@
 #  updated_at        :datetime         not null
 #  report_id         :bigint(8)
 #  status_ids        :string           is an Array
+#  overruled_at      :datetime
 #
 
 class AccountWarning < ApplicationRecord
@@ -28,12 +29,17 @@ class AccountWarning < ApplicationRecord
   belongs_to :target_account, class_name: 'Account', inverse_of: :strikes
   belongs_to :report, optional: true
 
-  has_one :appeal, dependent: :destroy
+  has_one :appeal, dependent: :destroy, inverse_of: :strike
 
   scope :latest, -> { order(id: :desc) }
   scope :custom, -> { where.not(text: '') }
+  scope :active, -> { where(overruled_at: nil).or(where('account_warnings.overruled_at >= ?', 30.days.ago)) }
 
   def statuses
     Status.with_discarded.where(id: status_ids || [])
   end
+
+  def overruled?
+    overruled_at.present?
+  end
 end
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
index 12136223be..0f2f712a25 100644
--- a/app/models/admin/action_log_filter.rb
+++ b/app/models/admin/action_log_filter.rb
@@ -8,6 +8,8 @@ class Admin::ActionLogFilter
   ).freeze
 
   ACTION_TYPE_MAP = {
+    approve_appeal: { target_type: 'Appeal', action: 'approve' }.freeze,
+    reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
     assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
     change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
     confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
diff --git a/app/models/admin/appeal_filter.rb b/app/models/admin/appeal_filter.rb
new file mode 100644
index 0000000000..b163d2e568
--- /dev/null
+++ b/app/models/admin/appeal_filter.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class Admin::AppealFilter
+  KEYS = %i(
+    status
+  ).freeze
+
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Appeal.order(id: :desc)
+
+    params.each do |key, value|
+      next if %w(page).include?(key.to_s)
+
+      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
+    end
+
+    scope
+  end
+
+  private
+
+  def scope_for(key, value)
+    case key.to_s
+    when 'status'
+      status_scope(value)
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+
+  def status_scope(value)
+    case value
+    when 'approved'
+      Appeal.approved
+    when 'rejected'
+      Appeal.rejected
+    when 'pending'
+      Appeal.pending
+    else
+      raise "Unknown status: #{value}"
+    end
+  end
+end
diff --git a/app/models/appeal.rb b/app/models/appeal.rb
new file mode 100644
index 0000000000..46f35ae375
--- /dev/null
+++ b/app/models/appeal.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: appeals
+#
+#  id                     :bigint(8)        not null, primary key
+#  account_id             :bigint(8)        not null
+#  account_warning_id     :bigint(8)        not null
+#  text                   :text             default(""), not null
+#  approved_at            :datetime
+#  approved_by_account_id :bigint(8)
+#  rejected_at            :datetime
+#  rejected_by_account_id :bigint(8)
+#  created_at             :datetime         not null
+#  updated_at             :datetime         not null
+#
+class Appeal < ApplicationRecord
+  belongs_to :account
+  belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id'
+  belongs_to :approved_by_account, class_name: 'Account', optional: true
+  belongs_to :rejected_by_account, class_name: 'Account', optional: true
+
+  validates :text, presence: true, length: { maximum: 2_000 }
+  validates :account_warning_id, uniqueness: true
+
+  validate :validate_time_frame, on: :create
+
+  scope :approved, -> { where.not(approved_at: nil) }
+  scope :rejected, -> { where.not(rejected_at: nil) }
+  scope :pending, -> { where(approved_at: nil, rejected_at: nil) }
+
+  def pending?
+    !approved? && !rejected?
+  end
+
+  def approved?
+    approved_at.present?
+  end
+
+  def rejected?
+    rejected_at.present?
+  end
+
+  def approve!(current_account)
+    update!(approved_at: Time.now.utc, approved_by_account: current_account)
+  end
+
+  def reject!(current_account)
+    update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
+  end
+
+  private
+
+  def validate_time_frame
+    errors.add(:base, I18n.t('strikes.errors.too_late')) if Time.now.utc > (strike.created_at + 20.days)
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index fd1d7049a9..517254a910 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -265,6 +265,10 @@ class User < ApplicationRecord
     settings.notification_emails['pending_account']
   end
 
+  def allows_appeal_emails?
+    settings.notification_emails['appeal']
+  end
+
   def allows_trending_tag_emails?
     settings.notification_emails['trending_tag']
   end
diff --git a/app/policies/account_warning_policy.rb b/app/policies/account_warning_policy.rb
new file mode 100644
index 0000000000..6b92da475b
--- /dev/null
+++ b/app/policies/account_warning_policy.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AccountWarningPolicy < ApplicationPolicy
+  def show?
+    target? || staff?
+  end
+
+  def appeal?
+    target?
+  end
+
+  private
+
+  def target?
+    record.target_account_id == current_account&.id
+  end
+end
diff --git a/app/policies/appeal_policy.rb b/app/policies/appeal_policy.rb
new file mode 100644
index 0000000000..a25187172a
--- /dev/null
+++ b/app/policies/appeal_policy.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AppealPolicy < ApplicationPolicy
+  def index?
+    staff?
+  end
+
+  def approve?
+    record.pending? && staff?
+  end
+
+  alias reject? approve?
+end
diff --git a/app/services/appeal_service.rb b/app/services/appeal_service.rb
new file mode 100644
index 0000000000..1397c50f5f
--- /dev/null
+++ b/app/services/appeal_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AppealService < BaseService
+  def call(strike, text)
+    @strike = strike
+    @text   = text
+
+    create_appeal!
+    notify_staff!
+
+    @appeal
+  end
+
+  private
+
+  def create_appeal!
+    @appeal = @strike.create_appeal!(
+      text: @text,
+      account: @strike.target_account
+    )
+  end
+
+  def notify_staff!
+    User.staff.includes(:account).each do |u|
+      AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails?
+    end
+  end
+end
diff --git a/app/services/approve_appeal_service.rb b/app/services/approve_appeal_service.rb
new file mode 100644
index 0000000000..f76bf8943e
--- /dev/null
+++ b/app/services/approve_appeal_service.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+class ApproveAppealService < BaseService
+  def call(appeal, current_account)
+    @appeal          = appeal
+    @strike          = appeal.strike
+    @current_account = current_account
+
+    ApplicationRecord.transaction do
+      undo_strike_action!
+      mark_strike_as_appealed!
+    end
+
+    queue_workers!
+    notify_target_account!
+  end
+
+  private
+
+  def target_account
+    @strike.target_account
+  end
+
+  def undo_strike_action!
+    case @strike.action
+    when 'disable'
+      undo_disable!
+    when 'delete_statuses'
+      undo_delete_statuses!
+    when 'sensitive'
+      undo_sensitive!
+    when 'silence'
+      undo_silence!
+    when 'suspend'
+      undo_suspend!
+    end
+  end
+
+  def mark_strike_as_appealed!
+    @appeal.approve!(@current_account)
+    @strike.touch(:overruled_at)
+  end
+
+  def undo_disable!
+    target_account.user.enable!
+  end
+
+  def undo_delete_statuses!
+    # Cannot be undone
+  end
+
+  def undo_sensitive!
+    target_account.unsensitize!
+  end
+
+  def undo_silence!
+    target_account.unsilence!
+  end
+
+  def undo_suspend!
+    target_account.unsuspend!
+  end
+
+  def queue_workers!
+    case @strike.action
+    when 'suspend'
+      Admin::UnsuspensionWorker.perform_async(target_account.id)
+    end
+  end
+
+  def notify_target_account!
+    UserMailer.appeal_approved(target_account.user, @appeal).deliver_later
+  end
+end
diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml
deleted file mode 100644
index 432fb79a6e..0000000000
--- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.speech-bubble
-  .speech-bubble__bubble
-    = simple_format(h(account_moderation_note.content))
-  .speech-bubble__owner
-    = admin_account_link_to account_moderation_note.account
-    %time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at
-    = table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note)
diff --git a/app/views/admin/account_warnings/_account_warning.html.haml b/app/views/admin/account_warnings/_account_warning.html.haml
index 8c9c9679ce..ef23c3b776 100644
--- a/app/views/admin/account_warnings/_account_warning.html.haml
+++ b/app/views/admin/account_warnings/_account_warning.html.haml
@@ -1,6 +1,24 @@
-.speech-bubble.warning
-  .speech-bubble__bubble
-    = Formatter.instance.linkify(account_warning.text)
-  .speech-bubble__owner
-    = admin_account_link_to account_warning.account
-    %time.formatted{ datetime: account_warning.created_at.iso8601 }= l account_warning.created_at
+= link_to disputes_strike_path(account_warning), class: ['log-entry', account_warning.overruled? && 'log-entry--inactive'] do
+  .log-entry__header
+    .log-entry__avatar
+      = image_tag account_warning.target_account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
+    .log-entry__content
+      .log-entry__title
+        = t(account_warning.action, scope: 'admin.strikes.actions', name: content_tag(:span, account_warning.account.username, class: 'username'), target: content_tag(:span, account_warning.target_account.acct, class: 'target')).html_safe
+      .log-entry__timestamp
+        %time.formatted{ datetime: account_warning.created_at.iso8601 }
+          = l(account_warning.created_at)
+
+        - if account_warning.report_id.present?
+          ·
+          = t('admin.reports.title', id: account_warning.report_id)
+
+        - if account_warning.overruled?
+          ·
+          %span.positive-hint= t('admin.strikes.appeal_approved')
+        - elsif account_warning.appeal&.pending?
+          ·
+          %span.warning-hint= t('admin.strikes.appeal_pending')
+        - elsif account_warning.appeal&.rejected?
+          ·
+          %span.negative-hint= t('admin.strikes.appeal_rejected')
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index f3853d629e..9a1f07a066 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -246,18 +246,29 @@
   %hr.spacer/
 
   - unless @warnings.empty?
-    = render @warnings
+
+    %h3= t 'admin.accounts.previous_strikes'
+
+    %p= t('admin.accounts.previous_strikes_description_html', count: @account.previous_strikes_count)
+
+    .account-strikes
+      = render @warnings
 
     %hr.spacer/
 
-  = render @moderation_notes
+  %h3= t 'admin.reports.notes.title'
+
+  %p= t 'admin.reports.notes_description_html'
+
+  .report-notes
+    = render partial: 'admin/report_notes/report_note', collection: @moderation_notes
 
   = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f|
-    = render 'shared/error_messages', object: @account_moderation_note
-
-    = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
     = f.hidden_field :target_account_id
 
+    .field-group
+      = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
+
     .actions
       = f.button :button, t('admin.account_moderation_notes.create'), type: :submit
 
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 4b581f5ea6..59b75e0e1d 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -46,6 +46,9 @@
       %span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
       = fa_icon 'chevron-right fw'
 
+    = link_to admin_disputes_appeals_path(status: 'pending'), class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count)
+      = fa_icon 'chevron-right fw'
   .dashboard__item
     = react_admin_component :dimension, dimension: 'sources', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.sources')
 
diff --git a/app/views/admin/disputes/appeals/_appeal.html.haml b/app/views/admin/disputes/appeals/_appeal.html.haml
new file mode 100644
index 0000000000..02b8777e13
--- /dev/null
+++ b/app/views/admin/disputes/appeals/_appeal.html.haml
@@ -0,0 +1,21 @@
+= link_to disputes_strike_path(appeal.strike), class: ['log-entry', appeal.approved? && 'log-entry--inactive'] do
+  .log-entry__header
+    .log-entry__avatar
+      = image_tag appeal.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
+    .log-entry__content
+      .log-entry__title
+        = t(appeal.strike.action, scope: 'admin.strikes.actions', name: content_tag(:span, appeal.strike.account.username, class: 'username'), target: content_tag(:span, appeal.account.acct, class: 'target')).html_safe
+      .log-entry__timestamp
+        %time.formatted{ datetime: appeal.strike.created_at.iso8601 }
+          = l(appeal.strike.created_at)
+
+        - if appeal.strike.report_id.present?
+          ·
+          = t('admin.reports.title', id: appeal.strike.report_id)
+        ·
+        - if appeal.approved?
+          %span.positive-hint= t('admin.strikes.appeal_approved')
+        - elsif appeal.rejected?
+          %span.negative-hint= t('admin.strikes.appeal_rejected')
+        - else
+          %span.warning-hint= t('admin.strikes.appeal_pending')
diff --git a/app/views/admin/disputes/appeals/index.html.haml b/app/views/admin/disputes/appeals/index.html.haml
new file mode 100644
index 0000000000..dd6a6f403f
--- /dev/null
+++ b/app/views/admin/disputes/appeals/index.html.haml
@@ -0,0 +1,22 @@
+- content_for :page_title do
+  = t('admin.disputes.appeals.title')
+
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+.filters
+  .filter-subset
+    %strong= t('admin.tags.review')
+    %ul
+      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Appeal.pending.count})"], ' '), status: 'pending'
+      %li= filter_link_to t('admin.trends.approved'), status: 'approved'
+      %li= filter_link_to t('admin.trends.rejected'), status: 'rejected'
+
+- if @appeals.empty?
+  %div.muted-hint.center-text
+    = t 'admin.disputes.appeals.empty'
+- else
+  .announcements-list
+    = render partial: 'appeal', collection: @appeals
+
+= paginate @appeals
diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml
index 428b6cf59c..f9d57c2ae3 100644
--- a/app/views/admin/report_notes/_report_note.html.haml
+++ b/app/views/admin/report_notes/_report_note.html.haml
@@ -3,7 +3,7 @@
 
   .report-notes__item__header
     %span.username
-      = link_to display_name(report_note.account), admin_account_path(report_note.account_id)
+      = link_to report_note.account.username, admin_account_path(report_note.account_id)
     %time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
       - if report_note.created_at.today?
         = t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 02c46e3840..e53c180e50 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -57,7 +57,7 @@
         .report-header__details__item__header
           %strong= t('admin.accounts.strikes')
         .report-header__details__item__content
-          = @report.target_account.strikes.count
+          = @report.target_account.previous_strikes_count
 
   .report-header__details
     .report-header__details__item
diff --git a/app/views/admin_mailer/new_appeal.text.erb b/app/views/admin_mailer/new_appeal.text.erb
new file mode 100644
index 0000000000..db4529eb7d
--- /dev/null
+++ b/app/views/admin_mailer/new_appeal.text.erb
@@ -0,0 +1,9 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_appeal.body', target: @appeal.account.username, action_taken_by: @appeal.strike.account.username, date: l(@appeal.strike.created_at), type: t(@appeal.strike.action, scope: 'admin_mailer.new_appeal.actions')) %>
+
+> <%= raw word_wrap(@appeal.text, break_sequence: "\n> ") %>
+
+<%= raw t('admin_mailer.new_appeal.next_steps') %>
+
+<%= raw t('application_mailer.view')%> <%= disputes_strike_url(@appeal.strike) %>
diff --git a/app/views/auth/registrations/_account_warning.html.haml b/app/views/auth/registrations/_account_warning.html.haml
new file mode 100644
index 0000000000..40e7e12968
--- /dev/null
+++ b/app/views/auth/registrations/_account_warning.html.haml
@@ -0,0 +1,20 @@
+= link_to disputes_strike_path(account_warning), class: 'log-entry' do
+  .log-entry__header
+    .log-entry__avatar
+      .indicator-icon{ class: account_warning.overruled? ? 'success' : 'failure' }
+        = fa_icon 'warning'
+    .log-entry__content
+      .log-entry__title
+        = t('disputes.strikes.title', action: t(account_warning.action, scope: 'disputes.strikes.title_actions'), date: l(account_warning.created_at.to_date))
+      .log-entry__timestamp
+        %time.formatted{ datetime: account_warning.created_at.iso8601 }= l(account_warning.created_at)
+
+        - if account_warning.overruled?
+          ·
+          %span.positive-hint= t('disputes.strikes.your_appeal_approved')
+        - elsif account_warning.appeal&.pending?
+          ·
+          %span.warning-hint= t('disputes.strikes.your_appeal_pending')
+        - elsif account_warning.appeal&.rejected?
+          ·
+          %span.negative-hint= t('disputes.strikes.your_appeal_rejected')
diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml
index 47112dae07..3546510b21 100644
--- a/app/views/auth/registrations/_status.html.haml
+++ b/app/views/auth/registrations/_status.html.haml
@@ -1,22 +1,17 @@
+- if !@user.confirmed?
+  .flash-message.warning
+    = t('auth.status.confirming')
+    = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
+- elsif !@user.approved?
+  .flash-message.warning
+    = t('auth.status.pending')
+- elsif @user.account.moved_to_account_id.present?
+  .flash-message.warning
+    = t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
+    = link_to t('migrations.cancel'), settings_migration_path
+
 %h3= t('auth.status.account_status')
 
-.simple_form
-  %p.hint
-    - if @user.account.suspended?
-      %span.negative-hint= t('user_mailer.warning.explanation.suspend')
-    - elsif @user.disabled?
-      %span.negative-hint= t('user_mailer.warning.explanation.disable')
-    - elsif @user.account.silenced?
-      %span.warning-hint= t('user_mailer.warning.explanation.silence')
-    - elsif !@user.confirmed?
-      %span.warning-hint= t('auth.status.confirming')
-      = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
-    - elsif !@user.approved?
-      %span.warning-hint= t('auth.status.pending')
-    - elsif @user.account.moved_to_account_id.present?
-      %span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
-      = link_to t('migrations.cancel'), settings_migration_path
-    - else
-      %span.positive-hint= t('auth.status.functional')
+= render partial: 'account_warning', collection: @strikes
 
 %hr.spacer/
diff --git a/app/views/disputes/strikes/show.html.haml b/app/views/disputes/strikes/show.html.haml
new file mode 100644
index 0000000000..3dcb19016e
--- /dev/null
+++ b/app/views/disputes/strikes/show.html.haml
@@ -0,0 +1,127 @@
+- content_for :page_title do
+  = t('disputes.strikes.title', action: t(@strike.action, scope: 'disputes.strikes.title_actions'), date: l(@strike.created_at.to_date))
+
+- content_for :heading_actions do
+  - if @appeal.persisted?
+    = link_to t('admin.accounts.approve'), approve_admin_disputes_appeal_path(@appeal), method: :post, class: 'button' if can?(:approve, @appeal)
+    = link_to t('admin.accounts.reject'), reject_admin_disputes_appeal_path(@appeal), method: :post, class: 'button button--destructive' if can?(:reject, @appeal)
+
+- if @strike.overruled?
+  %p.hint
+    %span.positive-hint
+      = fa_icon 'check'
+      = ' '
+      = t 'disputes.strikes.appeal_approved'
+- elsif @appeal.persisted? && @appeal.rejected?
+  %p.hint
+    %span.negative-hint
+      = fa_icon 'times'
+      = ' '
+      = t 'disputes.strikes.appeal_rejected'
+
+.report-header
+  .report-header__card
+    .strike-card
+      - unless @strike.none_action?
+        %p= t "user_mailer.warning.explanation.#{@strike.action}"
+
+      - unless @strike.text.blank?
+        = Formatter.instance.linkify(@strike.text)
+
+      - if @strike.report && !@strike.report.other?
+        %p
+          %strong= t('user_mailer.warning.reason')
+          = t("user_mailer.warning.categories.#{@strike.report.category}")
+
+        - if @strike.report.violation? && @strike.report.rule_ids.present?
+          %ul.rules-list
+            - @strike.report.rules.each do |rule|
+              %li= rule.text
+
+      - if @strike.status_ids.present? && !@strike.status_ids.empty?
+        %p
+          %strong= t('user_mailer.warning.statuses')
+
+        .strike-card__statuses-list
+          - status_map = @strike.statuses.includes(:application, :media_attachments).index_by(&:id)
+
+          - @strike.status_ids.each do |status_id|
+            .strike-card__statuses-list__item
+              - if (status = status_map[status_id.to_i])
+                .one-liner
+                  = link_to short_account_status_url(@strike.target_account, status_id), class: 'emojify' do
+                    = one_line_preview(status)
+
+                    - status.media_attachments.each do |media_attachment|
+                      %abbr{ title: media_attachment.description }
+                        = fa_icon 'link'
+                        = media_attachment.file_file_name
+                .strike-card__statuses-list__item__meta
+                  %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
+                  ·
+                  = status.application.name
+              - else
+                .one-liner= t('disputes.strikes.status', id: status_id)
+                .strike-card__statuses-list__item__meta
+                  = t('disputes.strikes.status_removed')
+
+  .report-header__details
+    .report-header__details__item
+      .report-header__details__item__header
+        %strong= t('disputes.strikes.created_at')
+      .report-header__details__item__content
+        %time.formatted{ datetime: @strike.created_at.iso8601, title: l(@strike.created_at) }= l(@strike.created_at)
+    .report-header__details__item
+      .report-header__details__item__header
+        %strong= t('disputes.strikes.recipient')
+      .report-header__details__item__content
+        = admin_account_link_to @strike.target_account, path: can?(:show, @strike.target_account) ? admin_account_path(@strike.target_account_id) : ActivityPub::TagManager.instance.url_for(@strike.target_account)
+    .report-header__details__item
+      .report-header__details__item__header
+        %strong= t('disputes.strikes.action_taken')
+      .report-header__details__item__content
+        - if @strike.overruled?
+          %del= t(@strike.action, scope: 'user_mailer.warning.title')
+        - else
+          = t(@strike.action, scope: 'user_mailer.warning.title')
+    - if @strike.report && can?(:show, @strike.report)
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('disputes.strikes.associated_report')
+        .report-header__details__item__content
+          = link_to t('admin.reports.report', id: @strike.report.id), admin_report_path(@strike.report)
+    - if @appeal.persisted?
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('disputes.strikes.appeal_submitted_at')
+        .report-header__details__item__content
+          %time.formatted{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }= l(@appeal.created_at)
+%hr.spacer/
+
+- if @appeal.persisted?
+  %h3= t('disputes.strikes.appeal')
+
+  .report-notes
+    .report-notes__item
+      = image_tag @appeal.account.avatar.url, class: 'report-notes__item__avatar'
+
+      .report-notes__item__header
+        %span.username
+          = link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account)
+        %time{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }
+          - if @appeal.created_at.today?
+            = t('admin.report_notes.today_at', time: l(@appeal.created_at, format: :time))
+          - else
+            = l @appeal.created_at.to_date
+
+      .report-notes__item__content
+        = simple_format(h(@appeal.text))
+- elsif can?(:appeal, @strike)
+  %h3= t('disputes.strikes.appeals.submit')
+
+  = simple_form_for(@appeal, url: disputes_strike_appeal_path(@strike)) do |f|
+    .fields-group
+      = f.input :text, wrapper: :with_label, input_html: { maxlength: 500 }
+
+    .actions
+      = f.button :button, t('disputes.strikes.appeals.submit'), type: :submit
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index d7cc1ed5d1..223e5d7407 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -21,6 +21,7 @@
 
       - if current_user.staff?
         = ff.input :report, as: :boolean, wrapper: :with_label
+        = ff.input :appeal, as: :boolean, wrapper: :with_label
         = ff.input :pending_account, as: :boolean, wrapper: :with_label
         = ff.input :trending_tag, as: :boolean, wrapper: :with_label
 
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index cd5ed52af4..1922f53ce3 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -49,7 +49,7 @@
     %span.detailed-status__visibility-icon
       = visibility_icon status
     ·
-    - if status.application && @account.user&.setting_show_application
+    - if status.application && status.account.user&.setting_show_application
       - if status.application.website.blank?
         %strong.detailed-status__application= status.application.name
       - else
diff --git a/app/views/user_mailer/appeal_approved.html.haml b/app/views/user_mailer/appeal_approved.html.haml
new file mode 100644
index 0000000000..962cab2e2c
--- /dev/null
+++ b/app/views/user_mailer/appeal_approved.html.haml
@@ -0,0 +1,59 @@
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.hero
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center.padded
+                              %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                                %tbody
+                                  %tr
+                                    %td
+                                      = image_tag full_pack_url('media/images/mailer/icon_done.png'), alt: ''
+
+                              %h1= t 'user_mailer.appeal_approved.title'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.content-start
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center
+                              %p= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell
+                  %table.column{ cellspacing: 0, cellpadding: 0 }
+                    %tbody
+                      %tr
+                        %td.column-cell.button-cell
+                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                            %tbody
+                              %tr
+                                %td.button-primary
+                                  = link_to root_url do
+                                    %span= t 'user_mailer.appeal_approved.action'
diff --git a/app/views/user_mailer/appeal_approved.text.erb b/app/views/user_mailer/appeal_approved.text.erb
new file mode 100644
index 0000000000..290fa24c36
--- /dev/null
+++ b/app/views/user_mailer/appeal_approved.text.erb
@@ -0,0 +1,7 @@
+<%= t 'user_mailer.appeal_approved.title' %>
+
+===
+
+<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
+
+=> <%= root_url %>
diff --git a/app/views/user_mailer/appeal_rejected.html.haml b/app/views/user_mailer/appeal_rejected.html.haml
new file mode 100644
index 0000000000..75cd9d023b
--- /dev/null
+++ b/app/views/user_mailer/appeal_rejected.html.haml
@@ -0,0 +1,59 @@
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.hero
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center.padded
+                              %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                                %tbody
+                                  %tr
+                                    %td
+                                      = image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: ''
+
+                              %h1= t 'user_mailer.appeal_rejected.title'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.content-start
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center
+                              %p= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell
+                  %table.column{ cellspacing: 0, cellpadding: 0 }
+                    %tbody
+                      %tr
+                        %td.column-cell.button-cell
+                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                            %tbody
+                              %tr
+                                %td.button-primary
+                                  = link_to root_url do
+                                    %span= t 'user_mailer.appeal_approved.action'
diff --git a/app/views/user_mailer/appeal_rejected.text.erb b/app/views/user_mailer/appeal_rejected.text.erb
new file mode 100644
index 0000000000..f47a768181
--- /dev/null
+++ b/app/views/user_mailer/appeal_rejected.text.erb
@@ -0,0 +1,7 @@
+<%= t 'user_mailer.appeal_rejected.title' %>
+
+===
+
+<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
+
+=> <%= root_url %>
diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml
index bda1fef6cf..b308e18f7b 100644
--- a/app/views/user_mailer/warning.html.haml
+++ b/app/views/user_mailer/warning.html.haml
@@ -77,8 +77,8 @@
                             %tbody
                               %tr
                                 %td.button-primary
-                                  = link_to about_more_url do
-                                    %span= t 'user_mailer.warning.review_server_policies'
+                                  = link_to disputes_strike_url(@warning) do
+                                    %span= t 'user_mailer.warning.appeal'
 
 %table.email-table{ cellspacing: 0, cellpadding: 0 }
   %tbody
@@ -95,4 +95,4 @@
                         %tbody
                           %tr
                             %td.column-cell.text-center
-                              %p= t 'user_mailer.warning.get_in_touch', instance: @instance
+                              %p= t 'user_mailer.warning.appeal_description', instance: @instance
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index 4245b71924..6ffe12ae06 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -1,25 +1,5 @@
 {
   "ignored_warnings": [
-    {
-      "warning_type": "SQL Injection",
-      "warning_code": 0,
-      "fingerprint": "04dbbc249b989db2e0119bbb0f59c9818e12889d2b97c529cdc0b1526002ba4b",
-      "check_name": "SQL",
-      "message": "Possible SQL injection",
-      "file": "app/models/report.rb",
-      "line": 113,
-      "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
-      "code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")",
-      "render_path": null,
-      "location": {
-        "type": "method",
-        "class": "Report",
-        "method": "history"
-      },
-      "user_input": "Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)",
-      "confidence": "High",
-      "note": ""
-    },
     {
       "warning_type": "SQL Injection",
       "warning_code": 0,
@@ -27,7 +7,7 @@
       "check_name": "SQL",
       "message": "Possible SQL injection",
       "file": "app/models/status.rb",
-      "line": 100,
+      "line": 104,
       "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
       "code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
       "render_path": null,
@@ -107,7 +87,7 @@
       "check_name": "PermitAttributes",
       "message": "Potentially dangerous key allowed for mass assignment",
       "file": "app/controllers/api/v1/admin/reports_controller.rb",
-      "line": 78,
+      "line": 90,
       "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
       "code": "params.permit(:resolved, :account_id, :target_account_id)",
       "render_path": null,
@@ -140,6 +120,36 @@
       "confidence": "Medium",
       "note": ""
     },
+    {
+      "warning_type": "Cross-Site Scripting",
+      "warning_code": 2,
+      "fingerprint": "afad51718ae373b2f19d2513029fd2afccf58b9148e475934bc6a162ee33c352",
+      "check_name": "CrossSiteScripting",
+      "message": "Unescaped model attribute",
+      "file": "app/views/admin/disputes/appeals/_appeal.html.haml",
+      "line": 7,
+      "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
+      "code": "t((Unresolved Model).new.strike.action, :scope => \"admin.strikes.actions\", :name => content_tag(:span, (Unresolved Model).new.strike.account.username, :class => \"username\"), :target => content_tag(:span, (Unresolved Model).new.account.acct, :class => \"target\"))",
+      "render_path": [
+        {
+          "type": "template",
+          "name": "admin/disputes/appeals/index",
+          "line": 16,
+          "file": "app/views/admin/disputes/appeals/index.html.haml",
+          "rendered": {
+            "name": "admin/disputes/appeals/_appeal",
+            "file": "app/views/admin/disputes/appeals/_appeal.html.haml"
+          }
+        }
+      ],
+      "location": {
+        "type": "template",
+        "template": "admin/disputes/appeals/_appeal"
+      },
+      "user_input": "(Unresolved Model).new.strike",
+      "confidence": "Weak",
+      "note": ""
+    },
     {
       "warning_type": "Redirect",
       "warning_code": 18,
@@ -194,7 +204,7 @@
         {
           "type": "template",
           "name": "admin/trends/links/index",
-          "line": 37,
+          "line": 39,
           "file": "app/views/admin/trends/links/index.html.haml",
           "rendered": {
             "name": "admin/trends/links/_preview_card",
@@ -213,13 +223,13 @@
     {
       "warning_type": "Mass Assignment",
       "warning_code": 105,
-      "fingerprint": "e867661b2c9812bc8b75a5df12b28e2a53ab97015de0638b4e732fe442561b28",
+      "fingerprint": "f9de0ca4b04ae4b51b74d98db14dcbb6dae6809e627b58e711019cf9b4a47866",
       "check_name": "PermitAttributes",
       "message": "Potentially dangerous key allowed for mass assignment",
       "file": "app/controllers/api/v1/reports_controller.rb",
       "line": 36,
       "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
-      "code": "params.permit(:account_id, :comment, :forward, :status_ids => ([]))",
+      "code": "params.permit(:account_id, :comment, :category, :forward, :status_ids => ([]), :rule_ids => ([]))",
       "render_path": null,
       "location": {
         "type": "method",
@@ -231,6 +241,6 @@
       "note": ""
     }
   ],
-  "updated": "2021-11-14 05:26:09 +0100",
-  "brakeman_version": "5.1.2"
+  "updated": "2022-02-13 02:24:12 +0100",
+  "brakeman_version": "5.2.1"
 }
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1809f123ed..05c64e18a7 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -94,7 +94,6 @@ en:
     account_moderation_notes:
       create: Leave note
       created_msg: Moderation note successfully created!
-      delete: Delete
       destroyed_msg: Moderation note successfully destroyed!
     accounts:
       add_email_domain_block: Block e-mail domain
@@ -163,6 +162,11 @@ en:
       not_subscribed: Not subscribed
       pending: Pending review
       perform_full_suspension: Suspend
+      previous_strikes: Previous strikes
+      previous_strikes_description_html:
+        one: This account has <strong>one</strong> strike.
+        other: This account has <strong>%{count}</strong> strikes.
+        zero: This account is <strong>in good standing</strong>.
       promote: Promote
       protocol: Protocol
       public: Public
@@ -227,6 +231,7 @@ en:
       whitelisted: Allowed for federation
     action_logs:
       action_types:
+        approve_appeal: Approve Appeal
         approve_user: Approve User
         assigned_to_self_report: Assign Report
         change_email_user: Change E-mail for User
@@ -258,6 +263,7 @@ en:
         enable_user: Enable User
         memorialize_account: Memorialize Account
         promote_user: Promote User
+        reject_appeal: Reject Appeal
         reject_user: Reject User
         remove_avatar_user: Remove Avatar
         reopen_report: Reopen Report
@@ -276,6 +282,7 @@ en:
         update_domain_block: Update Domain Block
         update_status: Update Post
       actions:
+        approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
         approve_user_html: "%{name} approved sign-up from %{target}"
         assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
         change_email_user_html: "%{name} changed the e-mail address of user %{target}"
@@ -307,6 +314,7 @@ en:
         enable_user_html: "%{name} enabled login for user %{target}"
         memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
         promote_user_html: "%{name} promoted user %{target}"
+        reject_appeal_html: "%{name} rejected moderation decision appeal from %{target}"
         reject_user_html: "%{name} rejected sign-up from %{target}"
         remove_avatar_user_html: "%{name} removed %{target}'s avatar"
         reopen_report_html: "%{name} reopened report %{target}"
@@ -385,6 +393,9 @@ en:
       media_storage: Media storage
       new_users: new users
       opened_reports: reports opened
+      pending_appeals_html:
+        one: "<strong>1</strong> pending appeal"
+        other: "<strong>%{count}</strong> pending appeals"
       pending_reports_html:
         one: "<strong>1</strong> pending report"
         other: "<strong>%{count}</strong> pending reports"
@@ -402,6 +413,10 @@ en:
       top_languages: Top active languages
       top_servers: Top active servers
       website: Website
+    disputes:
+      appeals:
+        empty: No appeals found.
+        title: Appeals
     domain_allows:
       add_new: Allow federation with domain
       created_msg: Domain has been successfully allowed for federation
@@ -720,6 +735,16 @@ en:
       no_status_selected: No posts were changed as none were selected
       title: Account posts
       with_media: With media
+    strikes:
+      actions:
+        delete_statuses: "%{name} deleted %{target}'s posts"
+        disable: "%{name} froze %{target}'s account"
+        none: "%{name} sent a warning to %{target}"
+        sensitive: "%{name} marked %{target}'s account as sensitive"
+        silence: "%{name} limited %{target}'s account"
+        suspend: "%{name} suspended %{target}'s account"
+      appeal_approved: Appealed
+      appeal_pending: Appeal pending
     system_checks:
       database_schema_check:
         message_html: There are pending database migrations. Please run them to ensure the application behaves as expected
@@ -781,6 +806,17 @@ en:
       empty: You haven't defined any warning presets yet.
       title: Manage warning presets
   admin_mailer:
+    new_appeal:
+      actions:
+        delete_statuses: to delete their posts
+        disable: to freeze their account
+        none: a warning
+        sensitive: to mark their account as sensitive
+        silence: to limit their account
+        suspend: to suspend their account
+      body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
+      next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
+      subject: "%{username} is appealing a moderation decision on %{instance}"
     new_pending_account:
       body: The details of the new account are below. You can approve or reject this application.
       subject: New account up for review on %{instance} (%{username})
@@ -871,7 +907,6 @@ en:
     status:
       account_status: Account status
       confirming: Waiting for e-mail confirmation to be completed.
-      functional: Your account is fully operational.
       pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
       redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
     too_fast: Form submitted too fast, try again.
@@ -937,6 +972,32 @@ en:
     directory: Profile directory
     explanation: Discover users based on their interests
     explore_mastodon: Explore %{title}
+  disputes:
+    strikes:
+      action_taken: Action taken
+      appeal: Appeal
+      appeal_approved: This strike has been successfully appealed and is no longer valid
+      appeal_rejected: The appeal has been rejected
+      appeal_submitted_at: Appeal submitted
+      appealed_msg: Your appeal has been submitted. If it is approved, you will be notified.
+      appeals:
+        submit: Submit appeal
+      associated_report: Associated report
+      created_at: Dated
+      recipient: Addressed to
+      status: 'Post #%{id}'
+      status_removed: Post already removed from system
+      title: "%{action} from %{date}"
+      title_actions:
+        delete_statuses: Post removal
+        disable: Freezing of account
+        none: Warning
+        sensitive: Marking as sensitive of account
+        silence: Limitation of account
+        suspend: Suspension of account
+      your_appeal_approved: Your appeal has been approved
+      your_appeal_pending: You have submitted an appeal
+      your_appeal_rejected: Your appeal has been rejected
   domain_validator:
     invalid_domain: is not a valid domain name
   errors:
@@ -1501,6 +1562,15 @@ en:
     recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
     webauthn: Security keys
   user_mailer:
+    appeal_approved:
+      action: Go to your account
+      explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing.
+      subject: Your appeal from %{date} has been approved
+      title: Appeal approved
+    appeal_rejected:
+      explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been rejected.
+      subject: Your appeal from %{date} has been rejected
+      title: Appeal rejected
     backup_ready:
       explanation: You requested a full backup of your Mastodon account. It's now ready for download!
       subject: Your archive is ready for download
@@ -1512,6 +1582,8 @@ en:
       subject: Please confirm attempted sign in
       title: Sign in attempt
     warning:
+      appeal: Submit an appeal
+      appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}.
       categories:
         spam: Spam
         violation: Content violates the following community guidelines
@@ -1523,7 +1595,6 @@ en:
         suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed in about 30 days, but we will retain some basic data to prevent you from evading the suspension.
       get_in_touch: If you believe this is an error, you can reply to this e-mail to get in touch with the staff of %{instance}.
       reason: 'Reason:'
-      review_server_policies: Review server policies
       statuses: 'Posts that have been found in violation:'
       subject:
         delete_statuses: Your posts on %{acct} have been removed
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index d6376782d4..03eefd0d5a 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -27,6 +27,8 @@ en:
         scheduled_at: Leave blank to publish the announcement immediately
         starts_at: Optional. In case your announcement is bound to a specific time range
         text: You can use post syntax. Please be mindful of the space the announcement will take up on the user's screen
+      appeal:
+        text: You can only appeal a strike once
       defaults:
         autofollow: People who sign up through the invite will automatically follow you
         avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
@@ -119,6 +121,8 @@ en:
         scheduled_at: Schedule publication
         starts_at: Start of event
         text: Announcement
+      appeal:
+        text: Explain why this decision should be reversed
       defaults:
         autofollow: Invite to follow your account
         avatar: Avatar
@@ -197,6 +201,7 @@ en:
           sign_up_requires_approval: Limit sign-ups
         severity: Rule
       notification_emails:
+        appeal: Someone appeals a moderator decision
         digest: Send digest e-mails
         favourite: Someone favourited your post
         follow: Someone followed you
@@ -204,8 +209,8 @@ en:
         mention: Someone mentioned you
         pending_account: New account needs review
         reblog: Someone boosted your post
-        report: A new report is submitted
-        trending_tag: A new trend requires approval
+        report: New report is submitted
+        trending_tag: New trend requires review
       rule:
         text: Rule
       tag:
diff --git a/config/navigation.rb b/config/navigation.rb
index fc03a2a778..3fc3747d59 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -20,7 +20,7 @@ SimpleNavigation::Configuration.run do |navigation|
     n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? }
 
     n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
-      s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities}
+      s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
       s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
       s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
     end
@@ -41,7 +41,7 @@ SimpleNavigation::Configuration.run do |navigation|
     n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s|
       s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
       s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
-      s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts}
+      s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes}
       s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
       s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
       s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
diff --git a/config/routes.rb b/config/routes.rb
index 5edb36519a..f59f2a5bba 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -164,6 +164,12 @@ Rails.application.routes.draw do
     resources :login_activities, only: [:index]
   end
 
+  namespace :disputes do
+    resources :strikes, only: [:show] do
+      resource :appeal, only: [:create]
+    end
+  end
+
   resources :media, only: [:show] do
     get :player
   end
@@ -324,6 +330,15 @@ Rails.application.routes.draw do
         end
       end
     end
+
+    namespace :disputes do
+      resources :appeals, only: [:index] do
+        member do
+          post :approve
+          post :reject
+        end
+      end
+    end
   end
 
   get '/admin', to: redirect('/admin/dashboard', status: 302)
diff --git a/config/settings.yml b/config/settings.yml
index 06cee25324..e63788ba2f 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -48,6 +48,7 @@ defaults: &defaults
     report: true
     pending_account: true
     trending_tag: true
+    appeal: true
   interactions:
     must_be_follower: false
     must_be_following: false
diff --git a/db/migrate/20220124141035_create_appeals.rb b/db/migrate/20220124141035_create_appeals.rb
new file mode 100644
index 0000000000..afb3efbd53
--- /dev/null
+++ b/db/migrate/20220124141035_create_appeals.rb
@@ -0,0 +1,14 @@
+class CreateAppeals < ActiveRecord::Migration[6.1]
+  def change
+    create_table :appeals do |t|
+      t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }
+      t.belongs_to :account_warning, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
+      t.text :text, null: false, default: ''
+      t.datetime :approved_at
+      t.belongs_to :approved_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
+      t.datetime :rejected_at
+      t.belongs_to :rejected_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb b/db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb
new file mode 100644
index 0000000000..a082da774c
--- /dev/null
+++ b/db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb
@@ -0,0 +1,5 @@
+class AddOverruledAtToAccountWarnings < ActiveRecord::Migration[6.1]
+  def change
+    add_column :account_warnings, :overruled_at, :datetime
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fd4633d695..8842dcd8c0 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: 2022_01_18_183123) do
+ActiveRecord::Schema.define(version: 2022_02_10_153119) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -135,6 +135,7 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do
     t.datetime "updated_at", null: false
     t.bigint "report_id"
     t.string "status_ids", array: true
+    t.datetime "overruled_at"
     t.index ["account_id"], name: "index_account_warnings_on_account_id"
     t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id"
   end
@@ -243,6 +244,22 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do
     t.bigint "status_ids", array: true
   end
 
+  create_table "appeals", force: :cascade do |t|
+    t.bigint "account_id", null: false
+    t.bigint "account_warning_id", null: false
+    t.text "text", default: "", null: false
+    t.datetime "approved_at"
+    t.bigint "approved_by_account_id"
+    t.datetime "rejected_at"
+    t.bigint "rejected_by_account_id"
+    t.datetime "created_at", precision: 6, null: false
+    t.datetime "updated_at", precision: 6, null: false
+    t.index ["account_id"], name: "index_appeals_on_account_id"
+    t.index ["account_warning_id"], name: "index_appeals_on_account_warning_id", unique: true
+    t.index ["approved_by_account_id"], name: "index_appeals_on_approved_by_account_id"
+    t.index ["rejected_by_account_id"], name: "index_appeals_on_rejected_by_account_id"
+  end
+
   create_table "backups", force: :cascade do |t|
     t.bigint "user_id"
     t.string "dump_file_name"
@@ -1031,6 +1048,10 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do
   add_foreign_key "announcement_reactions", "accounts", on_delete: :cascade
   add_foreign_key "announcement_reactions", "announcements", on_delete: :cascade
   add_foreign_key "announcement_reactions", "custom_emojis", on_delete: :cascade
+  add_foreign_key "appeals", "account_warnings", on_delete: :cascade
+  add_foreign_key "appeals", "accounts", column: "approved_by_account_id", on_delete: :nullify
+  add_foreign_key "appeals", "accounts", column: "rejected_by_account_id", on_delete: :nullify
+  add_foreign_key "appeals", "accounts", on_delete: :cascade
   add_foreign_key "backups", "users", on_delete: :nullify
   add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
   add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb
new file mode 100644
index 0000000000..6a06f94069
--- /dev/null
+++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+RSpec.describe Admin::Disputes::AppealsController, type: :controller do
+  render_views
+
+  before { sign_in current_user, scope: :user }
+
+  let(:target_account) { Fabricate(:account) }
+  let(:strike) { Fabricate(:account_warning, target_account: target_account, action: :suspend) }
+  let(:appeal) { Fabricate(:appeal, strike: strike, account: target_account) }
+
+  before do
+    target_account.suspend!
+  end
+
+  describe 'POST #approve' do
+    let(:current_user) { Fabricate(:user, admin: true) }
+
+    before do
+      allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil))
+      post :approve, params: { id: appeal.id }
+    end
+
+    it 'unsuspends a suspended account' do
+      expect(target_account.reload.suspended?).to be false
+    end
+
+    it 'redirects back to the strike page' do
+      expect(response).to redirect_to(disputes_strike_path(appeal.strike))
+    end
+
+    it 'notifies target account about approved appeal' do
+      expect(UserMailer).to have_received(:appeal_approved).with(target_account.user, appeal)
+    end
+  end
+
+  describe 'POST #reject' do
+    let(:current_user) { Fabricate(:user, admin: true) }
+
+    before do
+      allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil))
+      post :reject, params: { id: appeal.id }
+    end
+
+    it 'redirects back to the strike page' do
+      expect(response).to redirect_to(disputes_strike_path(appeal.strike))
+    end
+
+    it 'notifies target account about rejected appeal' do
+      expect(UserMailer).to have_received(:appeal_rejected).with(target_account.user, appeal)
+    end
+  end
+end
diff --git a/spec/controllers/disputes/appeals_controller_spec.rb b/spec/controllers/disputes/appeals_controller_spec.rb
new file mode 100644
index 0000000000..faa571fc9e
--- /dev/null
+++ b/spec/controllers/disputes/appeals_controller_spec.rb
@@ -0,0 +1,27 @@
+require 'rails_helper'
+
+RSpec.describe Disputes::AppealsController, type: :controller do
+  render_views
+
+  before { sign_in current_user, scope: :user }
+
+  let!(:admin) { Fabricate(:user, admin: true) }
+
+  describe '#create' do
+    let(:current_user) { Fabricate(:user) }
+    let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
+
+    before do
+      allow(AdminMailer).to receive(:new_appeal).and_return(double('email', deliver_later: nil))
+      post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } }
+    end
+
+    it 'notifies staff about new appeal' do
+      expect(AdminMailer).to have_received(:new_appeal).with(admin.account, Appeal.last)
+    end
+
+    it 'redirects back to the strike page' do
+      expect(response).to redirect_to(disputes_strike_path(strike.id))
+    end
+  end
+end
diff --git a/spec/controllers/disputes/strikes_controller_spec.rb b/spec/controllers/disputes/strikes_controller_spec.rb
new file mode 100644
index 0000000000..157f9ec3c7
--- /dev/null
+++ b/spec/controllers/disputes/strikes_controller_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe Disputes::StrikesController, type: :controller do
+  render_views
+
+  before { sign_in current_user, scope: :user }
+
+  describe '#show' do
+    let(:current_user) { Fabricate(:user) }
+    let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
+
+    before do
+      get :show, params: { id: strike.id }
+    end
+
+    context 'when meant for the user' do
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'when meant for a different user' do
+      let(:strike) { Fabricate(:account_warning) }
+
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+  end
+end
diff --git a/spec/fabricators/account_warning_fabricator.rb b/spec/fabricators/account_warning_fabricator.rb
index db161d4464..72fe835d9a 100644
--- a/spec/fabricators/account_warning_fabricator.rb
+++ b/spec/fabricators/account_warning_fabricator.rb
@@ -1,5 +1,6 @@
 Fabricator(:account_warning) do
-  account        nil
-  target_account nil
-  text           "MyText"
+  account
+  target_account(fabricator: :account)
+  text { Faker::Lorem.paragraph }
+  action 'suspend'
 end
diff --git a/spec/fabricators/appeal_fabricator.rb b/spec/fabricators/appeal_fabricator.rb
new file mode 100644
index 0000000000..339363822d
--- /dev/null
+++ b/spec/fabricators/appeal_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator(:appeal) do
+  strike(fabricator: :account_warning)
+  account { |attrs| attrs[:strike].target_account }
+  text { Faker::Lorem.paragraph }
+end
diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb
index 75ffbbf40f..9c0372b47f 100644
--- a/spec/mailers/previews/admin_mailer_preview.rb
+++ b/spec/mailers/previews/admin_mailer_preview.rb
@@ -15,4 +15,9 @@ class AdminMailerPreview < ActionMailer::Preview
   def new_trending_links
     AdminMailer.new_trending_links(Account.first, PreviewCard.limit(3))
   end
+
+  # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal
+  def new_appeal
+    AdminMailer.new_appeal(Account.first, Appeal.first)
+  end
 end
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index 69b9b971ee..8de7d86696 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -82,6 +82,11 @@ class UserMailerPreview < ActionMailer::Preview
     UserMailer.warning(User.first, AccountWarning.last)
   end
 
+  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/appeal_approved
+  def appeal_approved
+    UserMailer.appeal_approved(User.first, Appeal.last)
+  end
+
   # Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token
   def sign_in_token
     UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc)
diff --git a/spec/models/appeal_spec.rb b/spec/models/appeal_spec.rb
new file mode 100644
index 0000000000..14062dc4f4
--- /dev/null
+++ b/spec/models/appeal_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Appeal, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end

From f5c6ee4d54687a2824316447746fad45a9d05f2c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 14 Feb 2022 21:49:40 +0100
Subject: [PATCH 12/35] Bump dotenv from 10.0.0 to 16.0.0 (#17476)

Bumps [dotenv](https://github.com/motdotla/dotenv) from 10.0.0 to 16.0.0.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v10.0.0...v16.0.0)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 017bbc0539..5f642d7e6a 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
     "css-loader": "^5.2.7",
     "cssnano": "^4.1.11",
     "detect-passive-events": "^2.0.3",
-    "dotenv": "^10.0.0",
+    "dotenv": "^16.0.0",
     "emoji-mart": "npm:emoji-mart-lazyload",
     "es6-symbol": "^3.1.3",
     "escape-html": "^1.0.3",
diff --git a/yarn.lock b/yarn.lock
index e9bcf882c8..20e670656c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4073,10 +4073,10 @@ dot-prop@^5.2.0:
   dependencies:
     is-obj "^2.0.0"
 
-dotenv@^10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
-  integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
+dotenv@^16.0.0:
+  version "16.0.0"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
+  integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
 
 duplexer@^0.1.2:
   version "0.1.2"

From 61c2e74f07a0ca1001e64ea07cf10f457fd7f265 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Feb 2022 11:05:18 +0900
Subject: [PATCH 13/35] Bump @babel/runtime from 7.17.0 to 7.17.2 (#17544)

Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.17.0 to 7.17.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.2/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 5f642d7e6a..005808a0a6 100644
--- a/package.json
+++ b/package.json
@@ -66,7 +66,7 @@
     "@babel/plugin-transform-runtime": "^7.17.0",
     "@babel/preset-env": "^7.16.11",
     "@babel/preset-react": "^7.16.7",
-    "@babel/runtime": "^7.17.0",
+    "@babel/runtime": "^7.17.2",
     "@gamestdio/websocket": "^0.3.2",
     "@github/webauthn-json": "^0.5.7",
     "@rails/ujs": "^6.1.4",
diff --git a/yarn.lock b/yarn.lock
index 20e670656c..a72b015e9f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1024,10 +1024,10 @@
   dependencies:
     regenerator-runtime "^0.12.0"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.0", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
-  version "7.17.0"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.0.tgz#b8d142fc0f7664fb3d9b5833fd40dcbab89276c0"
-  integrity sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+  version "7.17.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
+  integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
   dependencies:
     regenerator-runtime "^0.13.4"
 

From fee7d5cce64b79cd000c7d387ba5788d824b1d48 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Feb 2022 11:06:06 +0900
Subject: [PATCH 14/35] Bump babel-jest from 27.5.0 to 27.5.1 (#17549)

Bumps [babel-jest](https://github.com/facebook/jest/tree/HEAD/packages/babel-jest) from 27.5.0 to 27.5.1.
- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/facebook/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/commits/v27.5.1/packages/babel-jest)

---
updated-dependencies:
- dependency-name: babel-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json |   2 +-
 yarn.lock    | 120 +--------------------------------------------------
 2 files changed, 2 insertions(+), 120 deletions(-)

diff --git a/package.json b/package.json
index 005808a0a6..d5c1f96513 100644
--- a/package.json
+++ b/package.json
@@ -175,7 +175,7 @@
     "@testing-library/jest-dom": "^5.16.2",
     "@testing-library/react": "^12.1.2",
     "babel-eslint": "^10.1.0",
-    "babel-jest": "^27.5.0",
+    "babel-jest": "^27.5.1",
     "eslint": "^7.32.0",
     "eslint-plugin-import": "~2.25.4",
     "eslint-plugin-jsx-a11y": "~6.5.1",
diff --git a/yarn.lock b/yarn.lock
index a72b015e9f..79a900a36d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1338,27 +1338,6 @@
     jest-haste-map "^27.5.1"
     jest-runtime "^27.5.1"
 
-"@jest/transform@^27.5.0":
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.0.tgz#a4941e69ac51e8aa9a255ff4855b564c228c400b"
-  integrity sha512-yXUy/iO3TH1itxJ9BF7LLjuXt8TtgtjAl0PBQbUaCvRa+L0yYBob6uayW9dFRX/CDQweouLhvmXh44zRiaB+yA==
-  dependencies:
-    "@babel/core" "^7.1.0"
-    "@jest/types" "^27.5.0"
-    babel-plugin-istanbul "^6.1.1"
-    chalk "^4.0.0"
-    convert-source-map "^1.4.0"
-    fast-json-stable-stringify "^2.0.0"
-    graceful-fs "^4.2.9"
-    jest-haste-map "^27.5.0"
-    jest-regex-util "^27.5.0"
-    jest-util "^27.5.0"
-    micromatch "^4.0.4"
-    pirates "^4.0.4"
-    slash "^3.0.0"
-    source-map "^0.6.1"
-    write-file-atomic "^3.0.0"
-
 "@jest/transform@^27.5.1":
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409"
@@ -1390,18 +1369,7 @@
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
 
-"@jest/types@^27.0.2", "@jest/types@^27.5.0":
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.0.tgz#6ad04a5c5355fd9f46e5cf761850e0edb3c209dd"
-  integrity sha512-oDHEp7gwSgA82RZ6pzUL3ugM2njP/lVB1MsxRZNOBk+CoNvh9SpH1lQixPFc/kDlV50v59csiW4HLixWmhmgPQ==
-  dependencies:
-    "@types/istanbul-lib-coverage" "^2.0.0"
-    "@types/istanbul-reports" "^3.0.0"
-    "@types/node" "*"
-    "@types/yargs" "^16.0.0"
-    chalk "^4.0.0"
-
-"@jest/types@^27.5.1":
+"@jest/types@^27.0.2", "@jest/types@^27.5.1":
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80"
   integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==
@@ -2359,20 +2327,6 @@ babel-eslint@^10.1.0:
     eslint-visitor-keys "^1.0.0"
     resolve "^1.12.0"
 
-babel-jest@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.0.tgz#c653985241af3c76f59d70d65a570860c2594a50"
-  integrity sha512-puhCyvBTNLevhbd1oyw6t3gWBicWoUARQYKCBB/B1moif17NbyhxbsfadqZIw8zfJJD+W7Vw0Nb20pEjLxkXqQ==
-  dependencies:
-    "@jest/transform" "^27.5.0"
-    "@jest/types" "^27.5.0"
-    "@types/babel__core" "^7.1.14"
-    babel-plugin-istanbul "^6.1.1"
-    babel-preset-jest "^27.5.0"
-    chalk "^4.0.0"
-    graceful-fs "^4.2.9"
-    slash "^3.0.0"
-
 babel-jest@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444"
@@ -2415,16 +2369,6 @@ babel-plugin-istanbul@^6.1.1:
     istanbul-lib-instrument "^5.0.4"
     test-exclude "^6.0.0"
 
-babel-plugin-jest-hoist@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.0.tgz#8fdf07835f2165a068de3ce95fd7749a89801b51"
-  integrity sha512-ztwNkHl+g1GaoQcb8f2BER4C3LMvSXuF7KVqtUioXQgScSEnkl6lLgCILUYIR+CPTwL8H3F/PNLze64HPWF9JA==
-  dependencies:
-    "@babel/template" "^7.3.3"
-    "@babel/types" "^7.3.3"
-    "@types/babel__core" "^7.0.0"
-    "@types/babel__traverse" "^7.0.6"
-
 babel-plugin-jest-hoist@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e"
@@ -2525,14 +2469,6 @@ babel-preset-current-node-syntax@^1.0.0:
     "@babel/plugin-syntax-optional-chaining" "^7.8.3"
     "@babel/plugin-syntax-top-level-await" "^7.8.3"
 
-babel-preset-jest@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.0.tgz#4e308711c3d2ff1f45cf5d9a23646e37b621fc9f"
-  integrity sha512-7bfu1cJBlgK/nKfTvMlElzA3jpi6GzDWX3fntnyP2cQSzoi/KUz6ewGlcb3PSRYZGyv+uPnVHY0Im3JbsViqgA==
-  dependencies:
-    babel-plugin-jest-hoist "^27.5.0"
-    babel-preset-current-node-syntax "^1.0.0"
-
 babel-preset-jest@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81"
@@ -6466,26 +6402,6 @@ jest-get-type@^27.5.1:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
   integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
 
-jest-haste-map@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.0.tgz#7cc3a920caf304c89fbfceb5d5717b929873f175"
-  integrity sha512-0KfckSBEKV+D6e0toXmIj4zzp72EiBnvkC0L+xYxenkLhAdkp2/8tye4AgMzz7Fqb1r8SWtz7+s1UQLrxMBang==
-  dependencies:
-    "@jest/types" "^27.5.0"
-    "@types/graceful-fs" "^4.1.2"
-    "@types/node" "*"
-    anymatch "^3.0.3"
-    fb-watchman "^2.0.0"
-    graceful-fs "^4.2.9"
-    jest-regex-util "^27.5.0"
-    jest-serializer "^27.5.0"
-    jest-util "^27.5.0"
-    jest-worker "^27.5.0"
-    micromatch "^4.0.4"
-    walker "^1.0.7"
-  optionalDependencies:
-    fsevents "^2.3.2"
-
 jest-haste-map@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f"
@@ -6575,11 +6491,6 @@ jest-pnp-resolver@^1.2.2:
   resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
   integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
 
-jest-regex-util@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.0.tgz#26c26cf15a73edba13cb8930e261443d25ed8608"
-  integrity sha512-e9LqSd6HsDsqd7KS3rNyYwmQAaG9jq4U3LbnwVxN/y3nNlDzm2OFs596uo9zrUY+AV1opXq6ome78tRDUCRWfA==
-
 jest-regex-util@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95"
@@ -6665,14 +6576,6 @@ jest-runtime@^27.5.1:
     slash "^3.0.0"
     strip-bom "^4.0.0"
 
-jest-serializer@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.0.tgz#439a110df27f97a40c114a429b708c2ada15a81f"
-  integrity sha512-aSDFqQlVXtBH+Zb5dl9mCvTSFkabixk/9P9cpngL4yJKpmEi9USxfDhONFMzJrtftPvZw3PcltUVmtFZTB93rg==
-  dependencies:
-    "@types/node" "*"
-    graceful-fs "^4.2.9"
-
 jest-serializer@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64"
@@ -6709,18 +6612,6 @@ jest-snapshot@^27.5.1:
     pretty-format "^27.5.1"
     semver "^7.3.2"
 
-jest-util@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.0.tgz#0b9540d91b0de65d288f235fa9899e6eeeab8d35"
-  integrity sha512-FUUqOx0gAzJy3ytatT1Ss372M1kmhczn8x7aE0++11oPGW1FyD/5NjYBI8w1KOXFm6IVjtaZm2szfJJL+CHs0g==
-  dependencies:
-    "@jest/types" "^27.5.0"
-    "@types/node" "*"
-    chalk "^4.0.0"
-    ci-info "^3.2.0"
-    graceful-fs "^4.2.9"
-    picomatch "^2.2.3"
-
 jest-util@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9"
@@ -6767,15 +6658,6 @@ jest-worker@^26.5.0:
     merge-stream "^2.0.0"
     supports-color "^7.0.0"
 
-jest-worker@^27.5.0:
-  version "27.5.0"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.0.tgz#99ee77e4d06168107c27328bd7f54e74c3a48d59"
-  integrity sha512-8OEHiPNOPTfaWnJ2SUHM8fmgeGq37uuGsQBvGKQJl1f+6WIy6g7G3fE2ruI5294bUKUI9FaCWt5hDvO8HSwsSg==
-  dependencies:
-    "@types/node" "*"
-    merge-stream "^2.0.0"
-    supports-color "^8.0.0"
-
 jest-worker@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"

From 5939aa934b73cb7a4446e574aa8b3677bbe9d8f9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Feb 2022 11:06:25 +0900
Subject: [PATCH 15/35] Bump @babel/plugin-proposal-decorators from 7.17.0 to
 7.17.2 (#17548)

Bumps [@babel/plugin-proposal-decorators](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-decorators) from 7.17.0 to 7.17.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.2/packages/babel-plugin-proposal-decorators)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-decorators"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json |  2 +-
 yarn.lock    | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index d5c1f96513..65eef34039 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
   "private": true,
   "dependencies": {
     "@babel/core": "^7.17.2",
-    "@babel/plugin-proposal-decorators": "^7.17.0",
+    "@babel/plugin-proposal-decorators": "^7.17.2",
     "@babel/plugin-transform-react-inline-elements": "^7.16.7",
     "@babel/plugin-transform-runtime": "^7.17.0",
     "@babel/preset-env": "^7.16.11",
diff --git a/yarn.lock b/yarn.lock
index 79a900a36d..c0bef8fc5b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -91,7 +91,7 @@
     browserslist "^4.17.5"
     semver "^6.3.0"
 
-"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.0":
+"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.1":
   version "7.17.1"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz#9699f14a88833a7e055ce57dcd3ffdcd25186b21"
   integrity sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ==
@@ -357,12 +357,12 @@
     "@babel/helper-plugin-utils" "^7.16.7"
     "@babel/plugin-syntax-class-static-block" "^7.14.5"
 
-"@babel/plugin-proposal-decorators@^7.17.0":
-  version "7.17.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.0.tgz#fc0f689fe2535075056c587bc10c176fa9990443"
-  integrity sha512-JR8HTf3T1CsdMqfENrZ9pqncwsH4sPcvsyDLpvmv8iIbpDmeyBD7HPfGAIqkQph2j5d3B84hTm+m3qHPAedaPw==
+"@babel/plugin-proposal-decorators@^7.17.2":
+  version "7.17.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.2.tgz#c36372ddfe0360cac1ee331a238310bddca11493"
+  integrity sha512-WH8Z95CwTq/W8rFbMqb9p3hicpt4RX4f0K659ax2VHxgOyT6qQmUaEVEjIh4WR9Eh9NymkVn5vwsrE68fAQNUw==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.17.0"
+    "@babel/helper-create-class-features-plugin" "^7.17.1"
     "@babel/helper-plugin-utils" "^7.16.7"
     "@babel/helper-replace-supers" "^7.16.7"
     "@babel/plugin-syntax-decorators" "^7.17.0"

From b2896442278e5bdcac013c81762ac8a3aad76ee3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Feb 2022 11:07:14 +0900
Subject: [PATCH 16/35] Bump npmlog from 6.0.0 to 6.0.1 (#17545)

Bumps [npmlog](https://github.com/npm/npmlog) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/npm/npmlog/releases)
- [Changelog](https://github.com/npm/npmlog/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/npmlog/compare/v6.0.0...v6.0.1)

---
updated-dependencies:
- dependency-name: npmlog
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json |  2 +-
 yarn.lock    | 18 +++++++++---------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/package.json b/package.json
index 65eef34039..01582b5f97 100644
--- a/package.json
+++ b/package.json
@@ -112,7 +112,7 @@
     "marky": "^1.2.2",
     "mini-css-extract-plugin": "^1.6.2",
     "mkdirp": "^1.0.4",
-    "npmlog": "^6.0.0",
+    "npmlog": "^6.0.1",
     "object-assign": "^4.1.1",
     "object-fit-images": "^3.2.3",
     "object.values": "^1.1.5",
diff --git a/yarn.lock b/yarn.lock
index c0bef8fc5b..caefb1f440 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2116,10 +2116,10 @@ aproba@^1.1.1:
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
 
-are-we-there-yet@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
-  integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
+are-we-there-yet@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz#ba20bd6b553e31d62fc8c31bd23d22b95734390d"
+  integrity sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==
   dependencies:
     delegates "^1.0.0"
     readable-stream "^3.6.0"
@@ -7562,12 +7562,12 @@ npm-run-path@^4.0.1:
   dependencies:
     path-key "^3.0.0"
 
-npmlog@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c"
-  integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==
+npmlog@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.1.tgz#06f1344a174c06e8de9c6c70834cfba2964bba17"
+  integrity sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg==
   dependencies:
-    are-we-there-yet "^2.0.0"
+    are-we-there-yet "^3.0.0"
     console-control-strings "^1.1.0"
     gauge "^4.0.0"
     set-blocking "^2.0.0"

From db94a3d94d51e6223d41da412c18529e2db4156a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Feb 2022 11:07:42 +0900
Subject: [PATCH 17/35] Bump rqrcode from 2.1.0 to 2.1.1 (#17542)

Bumps [rqrcode](https://github.com/whomwah/rqrcode) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/whomwah/rqrcode/releases)
- [Changelog](https://github.com/whomwah/rqrcode/blob/master/CHANGELOG.md)
- [Commits](https://github.com/whomwah/rqrcode/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: rqrcode
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 71e8cf4cd4..5cb0676034 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -506,7 +506,7 @@ GEM
     rexml (3.2.5)
     rotp (6.2.0)
     rpam2 (4.0.2)
-    rqrcode (2.1.0)
+    rqrcode (2.1.1)
       chunky_png (~> 1.0)
       rqrcode_core (~> 1.0)
     rqrcode_core (1.2.0)

From 5f4b9e1bce3e722f3e058ea73c3e137aa1cc26d0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Feb 2022 14:13:48 +0900
Subject: [PATCH 18/35] Bump rails from 6.1.4.4 to 6.1.4.6 (#17550)

Bumps [rails](https://github.com/rails/rails) from 6.1.4.4 to 6.1.4.6.
- [Release notes](https://github.com/rails/rails/releases)
- [Commits](https://github.com/rails/rails/compare/v6.1.4.4...v6.1.4.6)

---
updated-dependencies:
- dependency-name: rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 106 +++++++++++++++++++++++++--------------------------
 1 file changed, 53 insertions(+), 53 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 5cb0676034..8c2062f58c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,40 +1,40 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (6.1.4.4)
-      actionpack (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    actioncable (6.1.4.6)
+      actionpack (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailbox (6.1.4.4)
-      actionpack (= 6.1.4.4)
-      activejob (= 6.1.4.4)
-      activerecord (= 6.1.4.4)
-      activestorage (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    actionmailbox (6.1.4.6)
+      actionpack (= 6.1.4.6)
+      activejob (= 6.1.4.6)
+      activerecord (= 6.1.4.6)
+      activestorage (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       mail (>= 2.7.1)
-    actionmailer (6.1.4.4)
-      actionpack (= 6.1.4.4)
-      actionview (= 6.1.4.4)
-      activejob (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    actionmailer (6.1.4.6)
+      actionpack (= 6.1.4.6)
+      actionview (= 6.1.4.6)
+      activejob (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (6.1.4.4)
-      actionview (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    actionpack (6.1.4.6)
+      actionview (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       rack (~> 2.0, >= 2.0.9)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.2.0)
-    actiontext (6.1.4.4)
-      actionpack (= 6.1.4.4)
-      activerecord (= 6.1.4.4)
-      activestorage (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    actiontext (6.1.4.6)
+      actionpack (= 6.1.4.6)
+      activerecord (= 6.1.4.6)
+      activestorage (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       nokogiri (>= 1.8.5)
-    actionview (6.1.4.4)
-      activesupport (= 6.1.4.4)
+    actionview (6.1.4.6)
+      activesupport (= 6.1.4.6)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
@@ -45,22 +45,22 @@ GEM
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
     active_record_query_trace (1.8)
-    activejob (6.1.4.4)
-      activesupport (= 6.1.4.4)
+    activejob (6.1.4.6)
+      activesupport (= 6.1.4.6)
       globalid (>= 0.3.6)
-    activemodel (6.1.4.4)
-      activesupport (= 6.1.4.4)
-    activerecord (6.1.4.4)
-      activemodel (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
-    activestorage (6.1.4.4)
-      actionpack (= 6.1.4.4)
-      activejob (= 6.1.4.4)
-      activerecord (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    activemodel (6.1.4.6)
+      activesupport (= 6.1.4.6)
+    activerecord (6.1.4.6)
+      activemodel (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
+    activestorage (6.1.4.6)
+      actionpack (= 6.1.4.6)
+      activejob (= 6.1.4.6)
+      activerecord (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       marcel (~> 1.0.0)
       mini_mime (>= 1.1.0)
-    activesupport (6.1.4.4)
+    activesupport (6.1.4.6)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 1.6, < 2)
       minitest (>= 5.1)
@@ -453,20 +453,20 @@ GEM
       rack
     rack-test (1.1.0)
       rack (>= 1.0, < 3)
-    rails (6.1.4.4)
-      actioncable (= 6.1.4.4)
-      actionmailbox (= 6.1.4.4)
-      actionmailer (= 6.1.4.4)
-      actionpack (= 6.1.4.4)
-      actiontext (= 6.1.4.4)
-      actionview (= 6.1.4.4)
-      activejob (= 6.1.4.4)
-      activemodel (= 6.1.4.4)
-      activerecord (= 6.1.4.4)
-      activestorage (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    rails (6.1.4.6)
+      actioncable (= 6.1.4.6)
+      actionmailbox (= 6.1.4.6)
+      actionmailer (= 6.1.4.6)
+      actionpack (= 6.1.4.6)
+      actiontext (= 6.1.4.6)
+      actionview (= 6.1.4.6)
+      activejob (= 6.1.4.6)
+      activemodel (= 6.1.4.6)
+      activerecord (= 6.1.4.6)
+      activestorage (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       bundler (>= 1.15.0)
-      railties (= 6.1.4.4)
+      railties (= 6.1.4.6)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.5)
       actionpack (>= 5.0.1.rc1)
@@ -482,9 +482,9 @@ GEM
       railties (>= 6.0.0, < 7)
     rails-settings-cached (0.6.6)
       rails (>= 4.2.0)
-    railties (6.1.4.4)
-      actionpack (= 6.1.4.4)
-      activesupport (= 6.1.4.4)
+    railties (6.1.4.6)
+      actionpack (= 6.1.4.6)
+      activesupport (= 6.1.4.6)
       method_source
       rake (>= 0.13)
       thor (~> 1.0)

From 073543a19c071286fba09adda6b4baf45d5239ad Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Feb 2022 13:05:10 +0900
Subject: [PATCH 19/35] Bump pg from 1.3.1 to 1.3.2 (#17555)

Bumps [pg](https://github.com/ged/ruby-pg) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/ged/ruby-pg/releases)
- [Changelog](https://github.com/ged/ruby-pg/blob/master/History.rdoc)
- [Commits](https://github.com/ged/ruby-pg/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: pg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 8c2062f58c..988d507dc2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -416,7 +416,7 @@ GEM
     parslet (2.0.0)
     pastel (0.8.0)
       tty-color (~> 0.5)
-    pg (1.3.1)
+    pg (1.3.2)
     pghero (2.8.2)
       activerecord (>= 5)
     pkg-config (1.4.7)

From 448d62058ca49203e220db8e912b5d9b3ca16918 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Feb 2022 19:18:09 +0900
Subject: [PATCH 20/35] Bump scenic from 1.5.5 to 1.6.0 (#17547)

Bumps [scenic](https://github.com/scenic-views/scenic) from 1.5.5 to 1.6.0.
- [Release notes](https://github.com/scenic-views/scenic/releases)
- [Changelog](https://github.com/scenic-views/scenic/blob/main/CHANGELOG.md)
- [Commits](https://github.com/scenic-views/scenic/compare/v1.5.5...v1.6.0)

---
updated-dependencies:
- dependency-name: scenic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index ee5395bfcd..5ade2e8c4e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -76,7 +76,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
 gem 'rqrcode', '~> 2.1'
 gem 'ruby-progressbar', '~> 1.11'
 gem 'sanitize', '~> 6.0'
-gem 'scenic', '~> 1.5'
+gem 'scenic', '~> 1.6'
 gem 'sidekiq', '~> 6.4'
 gem 'sidekiq-scheduler', '~> 3.1'
 gem 'sidekiq-unique-jobs', '~> 7.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 988d507dc2..37d101be53 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -559,7 +559,7 @@ GEM
     sanitize (6.0.0)
       crass (~> 1.0.2)
       nokogiri (>= 1.12.0)
-    scenic (1.5.5)
+    scenic (1.6.0)
       activerecord (>= 4.0.0)
       railties (>= 4.0.0)
     securecompare (1.0.0)
@@ -777,7 +777,7 @@ DEPENDENCIES
   rubocop-rails (~> 2.13)
   ruby-progressbar (~> 1.11)
   sanitize (~> 6.0)
-  scenic (~> 1.5)
+  scenic (~> 1.6)
   sidekiq (~> 6.4)
   sidekiq-bulk (~> 0.2.0)
   sidekiq-scheduler (~> 3.1)

From 436d1243e5e4ed25e8c871960e443e87d2266be0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Feb 2022 19:20:37 +0900
Subject: [PATCH 21/35] Bump axios from 0.25.0 to 0.26.0 (#17551)

Bumps [axios](https://github.com/axios/axios) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json |  2 +-
 yarn.lock    | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index 01582b5f97..387fc4bb66 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
     "array-includes": "^3.1.4",
     "arrow-key-navigation": "^1.2.0",
     "autoprefixer": "^9.8.8",
-    "axios": "^0.25.0",
+    "axios": "^0.26.0",
     "babel-loader": "^8.2.3",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-preval": "^5.1.0",
diff --git a/yarn.lock b/yarn.lock
index caefb1f440..1d67179fc0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2303,12 +2303,12 @@ axe-core@^4.3.5:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
   integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==
 
-axios@^0.25.0:
-  version "0.25.0"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a"
-  integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==
+axios@^0.26.0:
+  version "0.26.0"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928"
+  integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==
   dependencies:
-    follow-redirects "^1.14.7"
+    follow-redirects "^1.14.8"
 
 axobject-query@^2.2.0:
   version "2.2.0"
@@ -4925,7 +4925,7 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
-follow-redirects@^1.0.0, follow-redirects@^1.14.7:
+follow-redirects@^1.0.0, follow-redirects@^1.14.8:
   version "1.14.8"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
   integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==

From 2fd2666eea571737fa689a2808b3e5f8786701f0 Mon Sep 17 00:00:00 2001
From: Jeong Arm <kjwonmail@gmail.com>
Date: Wed, 16 Feb 2022 21:14:53 +0900
Subject: [PATCH 22/35] Add test for user matching ip (#17572)

---
 spec/models/user_spec.rb | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 406438c220..1645ab59e0 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -89,6 +89,19 @@ RSpec.describe User, type: :model do
         expect(User.matches_email('specified')).to match_array([specified])
       end
     end
+
+    describe 'matches_ip' do
+      it 'returns a relation of users whose ip address is matching with the given CIDR' do
+        user1 = Fabricate(:user)
+        user2 = Fabricate(:user)
+        Fabricate(:session_activation, user: user1, ip: '2160:2160::22', session_id: '1')
+        Fabricate(:session_activation, user: user1, ip: '2160:2160::23', session_id: '2')
+        Fabricate(:session_activation, user: user2, ip: '2160:8888::24', session_id: '3')
+        Fabricate(:session_activation, user: user2, ip: '2160:8888::25', session_id: '4')
+
+        expect(User.matches_ip('2160:2160::/32')).to match_array([user1])
+      end
+    end
   end
 
   let(:account) { Fabricate(:account, username: 'alice') }

From 793da08995d66793fed31ec1b889404e7808cbfb Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 13:17:28 +0100
Subject: [PATCH 23/35] =?UTF-8?q?Change=20dasbhoard=20links=20for=20?=
 =?UTF-8?q?=E2=80=9Cnew=20users=E2=80=9D=20and=20=E2=80=9Cactive=20users?=
 =?UTF-8?q?=E2=80=9D=20(#17570)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Make them filter for local accounts by default
---
 app/views/admin/dashboard/index.html.haml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 59b75e0e1d..8354f0b9f5 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -19,10 +19,10 @@
 
 .dashboard
   .dashboard__item
-    = react_admin_component :counter, measure: 'new_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.new_users'), href: admin_accounts_path
+    = react_admin_component :counter, measure: 'new_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.new_users'), href: admin_accounts_path(origin: 'local')
 
   .dashboard__item
-    = react_admin_component :counter, measure: 'active_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.active_users'), href: admin_accounts_path
+    = react_admin_component :counter, measure: 'active_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.active_users'), href: admin_accounts_path(origin: 'local')
 
   .dashboard__item
     = react_admin_component :counter, measure: 'interactions', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.interactions')

From 73fce8d31115f44d6d3f20f563c656b1595e70f8 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 14:28:45 +0100
Subject: [PATCH 24/35] Fix performance of server-side filtering (#17575)

Fixes #17567
---
 app/lib/feed_manager.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 7840afee81..ccd4d36109 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -441,7 +441,7 @@ class FeedManager
 
     return false if active_filters.empty?
 
-    combined_regex = active_filters.reduce { |memo, obj| Regexp.union(memo, obj) }
+    combined_regex = Regexp.union(active_filters)
     status         = status.reblog if status.reblog?
 
     combined_text = [

From 8f537a116802ee39727d8b6c286883b9440ab51a Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 14:36:44 +0100
Subject: [PATCH 25/35] Change relays handling to not record boosts (#17571)

* Change relays handling to not record boosts

* Update tests
---
 app/lib/activitypub/activity/announce.rb      |  1 +
 .../lib/activitypub/activity/announce_spec.rb | 32 +++++++------------
 2 files changed, 13 insertions(+), 20 deletions(-)

diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 1f93192905..12fad8da40 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -8,6 +8,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
       original_status = status_from_object
 
       return reject_payload! if original_status.nil? || !announceable?(original_status)
+      return if requested_through_relay?
 
       @status = Status.find_by(account: @account, reblog: original_status)
 
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
index b93fcbe665..41806b2582 100644
--- a/spec/lib/activitypub/activity/announce_spec.rb
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -113,26 +113,23 @@ RSpec.describe ActivityPub::Activity::Announce do
       let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
       let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
 
+      let(:object_json) { 'https://example.com/actor/hello-world' }
+
       subject { described_class.new(json, sender, relayed_through_account: relay_account) }
 
+      before do
+        stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
+      end
+
       context 'and the relay is enabled' do
         before do
           relay.update(state: :accepted)
           subject.perform
         end
 
-        let(:object_json) do
-          {
-            id: 'https://example.com/actor#bar',
-            type: 'Note',
-            content: 'Lorem ipsum',
-            to: 'http://example.com/followers',
-            attributedTo: 'https://example.com/actor',
-          }
-        end
-
-        it 'creates a reblog by sender of status' do
-          expect(sender.statuses.count).to eq 2
+        it 'fetches the remote status' do
+          expect(a_request(:get, 'https://example.com/actor/hello-world')).to have_been_made
+          expect(Status.find_by(uri: 'https://example.com/actor/hello-world').text).to eq 'Hello world'
         end
       end
 
@@ -141,14 +138,9 @@ RSpec.describe ActivityPub::Activity::Announce do
           subject.perform
         end
 
-        let(:object_json) do
-          {
-            id: 'https://example.com/actor#bar',
-            type: 'Note',
-            content: 'Lorem ipsum',
-            to: 'http://example.com/followers',
-            attributedTo: 'https://example.com/actor',
-          }
+        it 'does not fetch the remote status' do
+          expect(a_request(:get, 'https://example.com/actor/hello-world')).not_to have_been_made
+          expect(Status.find_by(uri: 'https://example.com/actor/hello-world')).to be_nil
         end
 
         it 'does not create anything' do

From 6ea80ba2a22aaeae4f773b28b33168395c5297af Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 14:37:26 +0100
Subject: [PATCH 26/35] Change streaming server error messages when failing to
 parse client input (#17559)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fixes #17541

- prefix JSON parsing error message by “Error parsing message from …”
- output user id if a user is logged in, IP address otherwise
- reduce log level from error to warning when a user is logged in, and to silly
  otherwise
---
 streaming/index.js | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/streaming/index.js b/streaming/index.js
index 47f938b869..3db94b1600 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -92,13 +92,18 @@ const numWorkers = +process.env.STREAMING_CLUSTER_NUM || (env === 'development'
 
 /**
  * @param {string} json
+ * @param {any} req
  * @return {Object.<string, any>|null}
  */
-const parseJSON = (json) => {
+const parseJSON = (json, req) => {
   try {
     return JSON.parse(json);
   } catch (err) {
-    log.error(err);
+    if (req.accountId) {
+      log.warn(req.requestId, `Error parsing message from user ${req.accountId}: ${err}`);
+    } else {
+      log.silly(req.requestId, `Error parsing message from ${req.remoteAddress}: ${err}`);
+    }
     return null;
   }
 };
@@ -450,7 +455,7 @@ const startWorker = async (workerId) => {
    */
   const createSystemMessageListener = (req, eventHandlers) => {
     return message => {
-      const json = parseJSON(message);
+      const json = parseJSON(message, req);
 
       if (!json) return;
 
@@ -573,7 +578,7 @@ const startWorker = async (workerId) => {
     log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`);
 
     const listener = message => {
-      const json = parseJSON(message);
+      const json = parseJSON(message, req);
 
       if (!json) return;
 
@@ -1037,7 +1042,7 @@ const startWorker = async (workerId) => {
     ws.on('error', onEnd);
 
     ws.on('message', data => {
-      const json = parseJSON(data);
+      const json = parseJSON(data, session.request);
 
       if (!json) return;
 

From 1abf0f90000c86bfbc5d6ac9a976834dcd17983a Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 14:57:57 +0100
Subject: [PATCH 27/35] Fix 0 pluralization for some localization strings
 (#17576)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Apparently, when the `zero` case is not defined, the translation string used
for `0` depends on the language. In French, `one` is used, causing some strings
with hardcoded “one” or `1` to misrepresent the actual data.

For instance, the dashboard would display « 1 utilisateur·rice en attente » for
both 0 and 1 pending users.
---
 config/locales/en.yml | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/config/locales/en.yml b/config/locales/en.yml
index 05c64e18a7..cf4c2cc37e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -394,16 +394,16 @@ en:
       new_users: new users
       opened_reports: reports opened
       pending_appeals_html:
-        one: "<strong>1</strong> pending appeal"
+        one: "<strong>%{count}</strong> pending appeal"
         other: "<strong>%{count}</strong> pending appeals"
       pending_reports_html:
-        one: "<strong>1</strong> pending report"
+        one: "<strong>%{count}</strong> pending report"
         other: "<strong>%{count}</strong> pending reports"
       pending_tags_html:
-        one: "<strong>1</strong> pending hashtag"
+        one: "<strong>%{count}</strong> pending hashtag"
         other: "<strong>%{count}</strong> pending hashtags"
       pending_users_html:
-        one: "<strong>1</strong> pending user"
+        one: "<strong>%{count}</strong> pending user"
         other: "<strong>%{count}</strong> pending users"
       resolved_reports: reports resolved
       software: Software
@@ -457,6 +457,7 @@ en:
         affected_accounts:
           one: One account in the database affected
           other: "%{count} accounts in the database affected"
+          zero: No account in the database is affected
         retroactive:
           silence: Undo limit of existing affected accounts from this domain
           suspend: Unsuspend existing affected accounts from this domain
@@ -510,6 +511,7 @@ en:
       known_accounts:
         one: "%{count} known account"
         other: "%{count} known accounts"
+        zero: No known account
       moderation:
         all: All
         limited: Limited
@@ -769,6 +771,7 @@ en:
         shared_by_over_week:
           one: Shared by one person over the last week
           other: Shared by %{count} people over the last week
+          zero: Shared by noone over the last week
         title: Trending links
         usage_comparison: Shared %{today} times today, compared to %{yesterday} yesterday
       pending_review: Pending review
@@ -798,6 +801,7 @@ en:
         used_by_over_week:
           one: Used by one person over the last week
           other: Used by %{count} people over the last week
+          zero: Used by noone over the last week
       title: Trends
     warning_presets:
       add_new: Add new

From 00b45b967e0c92714e1ec54a2d5c924f8b1dd38b Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 21:44:19 +0100
Subject: [PATCH 28/35] Fix edge case where settings/admin page sidebar would
 be incorrectly hidden (#17580)

---
 app/javascript/packs/public.js            | 8 +-------
 app/javascript/styles/mastodon/admin.scss | 4 ++++
 2 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index c0c0886461..3d0a937e1f 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -276,13 +276,7 @@ function main() {
   });
 
   delegate(document, '.sidebar__toggle__icon', 'click', () => {
-    const target = document.querySelector('.sidebar ul');
-
-    if (target.style.display === 'block') {
-      target.style.display = 'none';
-    } else {
-      target.style.display = 'block';
-    }
+    document.querySelector('.sidebar ul').classList.toggle('visible');
   });
 
   // Empty the honeypot fields in JS in case something like an extension
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index f5741bd502..1921eb1466 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -322,6 +322,10 @@ $content-width: 840px;
 
       & > ul {
         display: none;
+
+        &.visible {
+          display: block;
+        }
       }
 
       ul a,

From aa86cf955755cd05ed9c274daebbec248c39d863 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 21:44:47 +0100
Subject: [PATCH 29/35] Fix opening the emoji picker scrolling the
 single-column view to the top (#17579)

Fixes #17577
---
 .../compose/components/emoji_picker_dropdown.js   | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index 4a87714e64..f433e4de9f 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -170,7 +170,7 @@ class EmojiPickerMenu extends React.PureComponent {
 
   state = {
     modifierOpen: false,
-    placement: null,
+    readyToFocus: false,
   };
 
   handleDocumentClick = e => {
@@ -182,6 +182,16 @@ class EmojiPickerMenu extends React.PureComponent {
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+
+    // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
+    // to wait for a frame before focusing
+    requestAnimationFrame(() => {
+      this.setState({ readyToFocus: true });
+      if (this.node) {
+        const element = this.node.querySelector('input[type="search"]');
+        if (element) element.focus();
+      }
+    });
   }
 
   componentWillUnmount () {
@@ -281,7 +291,7 @@ class EmojiPickerMenu extends React.PureComponent {
           showSkinTones={false}
           backgroundImageFn={backgroundImageFn}
           notFound={notFoundFn}
-          autoFocus
+          autoFocus={this.state.readyToFocus}
           emojiTooltip
         />
 
@@ -314,6 +324,7 @@ class EmojiPickerDropdown extends React.PureComponent {
   state = {
     active: false,
     loading: false,
+    placement: null,
   };
 
   setRef = (c) => {

From ac99f586bb4138e083676579097d951434e90515 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 22:29:48 +0100
Subject: [PATCH 30/35] Fix issues when attempting to appeal an old strike
 (#17554)

* Display an error when an appeal could not be submitted

* Do not offer users to appeal old strikes

* Fix 500 error when trying to appeal a strike that is too old

* Avoid using an extra translatable string
---
 app/controllers/disputes/appeals_controller.rb | 3 ++-
 app/models/appeal.rb                           | 4 +++-
 app/policies/account_warning_policy.rb         | 2 +-
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb
index 15367c8792..eefd92b5a8 100644
--- a/app/controllers/disputes/appeals_controller.rb
+++ b/app/controllers/disputes/appeals_controller.rb
@@ -9,7 +9,8 @@ class Disputes::AppealsController < Disputes::BaseController
     @appeal = AppealService.new.call(@strike, appeal_params[:text])
 
     redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
-  rescue ActiveRecord::RecordInvalid
+  rescue ActiveRecord::RecordInvalid => e
+    @appeal = e.record
     render template: 'disputes/strikes/show'
   end
 
diff --git a/app/models/appeal.rb b/app/models/appeal.rb
index 46f35ae375..1f32cfa8b2 100644
--- a/app/models/appeal.rb
+++ b/app/models/appeal.rb
@@ -16,6 +16,8 @@
 #  updated_at             :datetime         not null
 #
 class Appeal < ApplicationRecord
+  MAX_STRIKE_AGE = 20.days
+
   belongs_to :account
   belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id'
   belongs_to :approved_by_account, class_name: 'Account', optional: true
@@ -53,6 +55,6 @@ class Appeal < ApplicationRecord
   private
 
   def validate_time_frame
-    errors.add(:base, I18n.t('strikes.errors.too_late')) if Time.now.utc > (strike.created_at + 20.days)
+    errors.add(:base, I18n.t('strikes.errors.too_late')) if strike.created_at < MAX_STRIKE_AGE.ago
   end
 end
diff --git a/app/policies/account_warning_policy.rb b/app/policies/account_warning_policy.rb
index 6b92da475b..65707dfa7c 100644
--- a/app/policies/account_warning_policy.rb
+++ b/app/policies/account_warning_policy.rb
@@ -6,7 +6,7 @@ class AccountWarningPolicy < ApplicationPolicy
   end
 
   def appeal?
-    target?
+    target? && record.created_at >= Appeal::MAX_STRIKE_AGE.ago
   end
 
   private

From 3a0eba4918f62ade5785098137eb6ce0ae87a9be Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 13 Feb 2022 02:52:34 +0100
Subject: [PATCH 31/35] [Glitch] Fix privacy policy link not being visible on
 small screens

Port bbd34744161fc46fa0e75d64e08a2f70d951bb40 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../flavours/glitch/styles/footer.scss          | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/app/javascript/flavours/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss
index 00d2908832..073ebda7e4 100644
--- a/app/javascript/flavours/glitch/styles/footer.scss
+++ b/app/javascript/flavours/glitch/styles/footer.scss
@@ -90,6 +90,20 @@
         .column-4 {
           display: none;
         }
+
+        .column-2 h4 {
+          display: none;
+        }
+      }
+    }
+
+    .legal-xs {
+      display: none;
+      text-align: center;
+      padding-top: 20px;
+
+      @media screen and (max-width: $no-gap-breakpoint) {
+        display: block;
       }
     }
 
@@ -105,7 +119,8 @@
       }
     }
 
-    ul a {
+    ul a,
+    .legal-xs a {
       text-decoration: none;
       color: lighten($ui-base-color, 34%);
 

From 5ed35a0df4087c788c709dbb0bfb3a2cc020f78e Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 14 Feb 2022 21:27:53 +0100
Subject: [PATCH 32/35] [Glitch] Add appeals

Port SCSS changes from 564efd06515edc524a8a1cdf7a3d8a7d9a376c04 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../flavours/glitch/styles/admin.scss         | 89 +++++++++++++++++--
 1 file changed, 83 insertions(+), 6 deletions(-)

diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 66ce92ce2c..31f8839256 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -594,12 +594,16 @@ body,
 }
 
 .log-entry {
+  display: block;
   line-height: 20px;
   padding: 15px;
   padding-left: 15px * 2 + 40px;
   background: $ui-base-color;
   border-bottom: 1px solid darken($ui-base-color, 8%);
   position: relative;
+  text-decoration: none;
+  color: $darker-text-color;
+  font-size: 14px;
 
   &:first-child {
     border-top-left-radius: 4px;
@@ -612,15 +616,12 @@ body,
     border-bottom: 0;
   }
 
-  &:hover {
+  &:hover,
+  &:focus,
+  &:active {
     background: lighten($ui-base-color, 4%);
   }
 
-  &__header {
-    color: $darker-text-color;
-    font-size: 14px;
-  }
-
   &__avatar {
     position: absolute;
     left: 15px;
@@ -656,6 +657,18 @@ body,
       text-decoration: underline;
     }
   }
+
+  &--inactive {
+    .log-entry__title {
+      text-decoration: line-through;
+    }
+
+    a,
+    .username,
+    .target {
+      color: $darker-text-color;
+    }
+  }
 }
 
 a.name-tag,
@@ -1191,6 +1204,17 @@ a.sparkline {
         font-weight: 600;
         padding: 4px 0;
       }
+
+      a {
+        color: $ui-highlight-color;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
     }
 
     &--horizontal {
@@ -1467,3 +1491,56 @@ a.sparkline {
     }
   }
 }
+
+.strike-card {
+  padding: 15px;
+  border-radius: 4px;
+  background: $ui-base-color;
+  font-size: 15px;
+  line-height: 20px;
+  word-wrap: break-word;
+  font-weight: 400;
+  color: $primary-text-color;
+
+  p {
+    margin-bottom: 20px;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  &__statuses-list {
+    border-radius: 4px;
+    border: 1px solid darken($ui-base-color, 8%);
+    font-size: 13px;
+    line-height: 18px;
+    overflow: hidden;
+
+    &__item {
+      padding: 16px;
+      background: lighten($ui-base-color, 2%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &__meta {
+        color: $darker-text-color;
+      }
+
+      a {
+        color: inherit;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}

From 41d52ee4b5d72b3f63bf05b61bfb08b4ea270042 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 14 Feb 2022 21:54:01 +0100
Subject: [PATCH 33/35] Fix issue with glitch-soc's theming system

---
 app/controllers/disputes/base_controller.rb      | 5 +++++
 app/views/admin/disputes/appeals/index.html.haml | 3 ---
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb
index 865146b5cc..7830c55247 100644
--- a/app/controllers/disputes/base_controller.rb
+++ b/app/controllers/disputes/base_controller.rb
@@ -9,9 +9,14 @@ class Disputes::BaseController < ApplicationController
 
   before_action :set_body_classes
   before_action :authenticate_user!
+  before_action :set_pack
 
   private
 
+  def set_pack
+    use_pack 'admin'
+  end
+
   def set_body_classes
     @body_classes = 'admin'
   end
diff --git a/app/views/admin/disputes/appeals/index.html.haml b/app/views/admin/disputes/appeals/index.html.haml
index dd6a6f403f..42e9c4b1d7 100644
--- a/app/views/admin/disputes/appeals/index.html.haml
+++ b/app/views/admin/disputes/appeals/index.html.haml
@@ -1,9 +1,6 @@
 - content_for :page_title do
   = t('admin.disputes.appeals.title')
 
-- content_for :header_tags do
-  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
-
 .filters
   .filter-subset
     %strong= t('admin.tags.review')

From d70e4e3da07057cd1d71d73f591a9953ad86f8e4 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 21:44:19 +0100
Subject: [PATCH 34/35] [Glitch] Fix edge case where settings/admin page
 sidebar would be incorrectly hidden

Port 00b45b967e0c92714e1ec54a2d5c924f8b1dd38b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 app/javascript/flavours/glitch/packs/public.js   | 8 +-------
 app/javascript/flavours/glitch/packs/settings.js | 8 +-------
 app/javascript/flavours/glitch/styles/admin.scss | 4 ++++
 3 files changed, 6 insertions(+), 14 deletions(-)

diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index a92f3d5a84..84ec9fce74 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -147,13 +147,7 @@ function main() {
   });
 
   delegate(document, '.sidebar__toggle__icon', 'click', () => {
-    const target = document.querySelector('.sidebar ul');
-
-    if (target.style.display === 'block') {
-      target.style.display = 'none';
-    } else {
-      target.style.display = 'block';
-    }
+    document.querySelector('.sidebar ul').classList.toggle('visible');
   });
 
   // Empty the honeypot fields in JS in case something like an extension
diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js
index 9c4d119c1e..0a53e1c25a 100644
--- a/app/javascript/flavours/glitch/packs/settings.js
+++ b/app/javascript/flavours/glitch/packs/settings.js
@@ -7,13 +7,7 @@ function main() {
   const { delegate } = require('@rails/ujs');
 
   delegate(document, '.sidebar__toggle__icon', 'click', () => {
-    const target = document.querySelector('.sidebar ul');
-
-    if (target.style.display === 'block') {
-      target.style.display = 'none';
-    } else {
-      target.style.display = 'block';
-    }
+    document.querySelector('.sidebar ul').classList.toggle('visible');
   });
 }
 
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 31f8839256..33e115c1a0 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -322,6 +322,10 @@ $content-width: 840px;
 
       & > ul {
         display: none;
+
+        &.visible {
+          display: block;
+        }
       }
 
       ul a,

From 41a8606627216e8ed32fb562d245327e5985ba2d Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Feb 2022 21:44:47 +0100
Subject: [PATCH 35/35] [Glitch] Fix opening the emoji picker scrolling the
 single-column view to the top

Port aa86cf955755cd05ed9c274daebbec248c39d863 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../glitch/features/emoji_picker/index.js         | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index 78f691c980..5de9fe107f 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -250,7 +250,7 @@ class EmojiPickerMenu extends React.PureComponent {
 
   state = {
     modifierOpen: false,
-    placement: null,
+    readyToFocus: false,
   };
 
   handleDocumentClick = e => {
@@ -262,6 +262,16 @@ class EmojiPickerMenu extends React.PureComponent {
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+
+    // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
+    // to wait for a frame before focusing
+    requestAnimationFrame(() => {
+      this.setState({ readyToFocus: true });
+      if (this.node) {
+        const element = this.node.querySelector('input[type="search"]');
+        if (element) element.focus();
+      }
+    });
   }
 
   componentWillUnmount () {
@@ -361,7 +371,7 @@ class EmojiPickerMenu extends React.PureComponent {
           showSkinTones={false}
           backgroundImageFn={backgroundImageFn}
           notFound={notFoundFn}
-          autoFocus
+          autoFocus={this.state.readyToFocus}
           emojiTooltip
           native={useSystemEmojiFont}
         />
@@ -396,6 +406,7 @@ class EmojiPickerDropdown extends React.PureComponent {
   state = {
     active: false,
     loading: false,
+    placement: null,
   };
 
   setRef = (c) => {