about summary refs log tree commit diff
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2022-12-07 09:03:42 +0100
committerClaire <claire.github-309c@sitedethib.com>2022-12-07 09:03:42 +0100
commit9a3d91f629ae0e8f722d56b8634bfad299ab9f04 (patch)
tree324a2b04d34049d7f31a041d6a44b23d45a46e6b
parentfe523a304520a09f6371f45bd63b9e8988776c03 (diff)
parentb59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54 (diff)
Merge branch 'main' into glitch-soc/merge-upstream
Conflicts:
- `app/models/concerns/domain_materializable.rb`:
  Fixed a code style issue upstream in a PR that got merged in glitch-soc
  earlier.
  Changed the code to match upstream's.
-rw-r--r--.github/workflows/check-i18n.yml2
-rw-r--r--.github/workflows/linter.yml2
-rw-r--r--Vagrantfile71
-rw-r--r--app/controllers/admin/accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/admin/accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/filters_controller.rb6
-rw-r--r--app/javascript/mastodon/features/ui/components/header.js31
-rw-r--r--app/javascript/styles/mastodon/components.scss11
-rw-r--r--app/models/account_migration.rb2
-rw-r--r--app/models/concerns/domain_materializable.rb1
-rw-r--r--app/models/custom_filter.rb2
-rw-r--r--app/models/form/redirect.rb2
-rw-r--r--config/application.rb1
-rw-r--r--config/database.yml1
-rw-r--r--db/post_migrate/20221101190723_backfill_admin_action_logs.rb29
-rw-r--r--db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb153
-rw-r--r--db/schema.rb2
-rw-r--r--lib/tasks/tests.rake14
-rw-r--r--spec/controllers/admin/accounts_controller_spec.rb81
-rw-r--r--spec/controllers/api/v1/admin/accounts_controller_spec.rb18
-rw-r--r--spec/controllers/api/v1/filters_controller_spec.rb26
-rw-r--r--spec/models/account_migration_spec.rb43
22 files changed, 443 insertions, 59 deletions
diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml
index a9d8ea2ea..9a7463060 100644
--- a/.github/workflows/check-i18n.yml
+++ b/.github/workflows/check-i18n.yml
@@ -25,7 +25,7 @@ jobs:
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: '3.0'
+          ruby-version: .ruby-version
           bundler-cache: true
       - name: Check locale file normalization
         run: bundle exec i18n-tasks check-normalized
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 874afc459..319152e93 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -53,7 +53,7 @@ jobs:
       - name: Set-up Node.js
         uses: actions/setup-node@v3
         with:
-          node-version: 16.x
+          node-version-file: .nvmrc
           cache: yarn
       - name: Install dependencies
         run: yarn install --frozen-lockfile
diff --git a/Vagrantfile b/Vagrantfile
index 0d44b4d23..043bab3e9 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -3,16 +3,14 @@
 
 ENV["PORT"] ||= "3000"
 
-$provision = <<SCRIPT
-
-cd /vagrant # This is where the host folder/repo is mounted
+$provisionA = <<SCRIPT
 
 # Add the yarn repo + yarn repo keys
 curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
 sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
 
 # Add repo for NodeJS
-curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -
+curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
 
 # Add firewall rule to redirect 80 to PORT and save
 sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
@@ -33,32 +31,56 @@ sudo apt-get install \
   redis-tools \
   postgresql \
   postgresql-contrib \
-  yarn \
   libicu-dev \
   libidn11-dev \
-  libreadline-dev \
-  libpam0g-dev \
+  libreadline6-dev \
+  autoconf \
+  bison \
+  build-essential \
+  ffmpeg \
+  file \
+  gcc \
+  libffi-dev \
+  libgdbm-dev \
+  libjemalloc-dev \
+  libncurses5-dev \
+  libprotobuf-dev \
+  libssl-dev \
+  libyaml-dev \
+  pkg-config \
+  protobuf-compiler \
+  zlib1g-dev \
   -y
 
 # Install rvm
-read RUBY_VERSION < .ruby-version
+sudo apt-add-repository -y ppa:rael-gc/rvm
+sudo apt-get install rvm -y
 
-curl -sSL https://rvm.io/mpapis.asc | gpg --import
-curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
+sudo usermod -a -G rvm $USER
+
+SCRIPT
 
-curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
-source /home/vagrant/.rvm/scripts/rvm
+$provisionB = <<SCRIPT
+
+source "/etc/profile.d/rvm.sh"
 
 # Install Ruby
-rvm reinstall ruby-$RUBY_VERSION --disable-binary
+read RUBY_VERSION < /vagrant/.ruby-version
+rvm install ruby-$RUBY_VERSION --disable-binary
 
 # Configure database
 sudo -u postgres createuser -U postgres vagrant -s
 sudo -u postgres createdb -U postgres mastodon_development
 
-# Install gems and node modules
+cd /vagrant # This is where the host folder/repo is mounted
+
+# Install gems
 gem install bundler foreman
 bundle install
+
+# Install node modules
+sudo corepack enable
+yarn set version classic
 yarn install
 
 # Build Mastodon
@@ -72,18 +94,11 @@ echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
 
 SCRIPT
 
-$start = <<SCRIPT
-
-echo 'To start server'
-echo '  $ vagrant ssh -c "cd /vagrant && foreman start"'
-
-SCRIPT
-
 VAGRANTFILE_API_VERSION = "2"
 
 Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 
-  config.vm.box = "ubuntu/bionic64"
+  config.vm.box = "ubuntu/focal64"
 
   config.vm.provider :virtualbox do |vb|
     vb.name = "mastodon"
@@ -100,7 +115,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
     # Use "virtio" network interfaces for better performance.
     vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
     vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
-
   end
 
   # This uses the vagrant-hostsupdater plugin, and lets you
@@ -118,7 +132,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
   end
 
   if config.vm.networks.any? { |type, options| type == :private_network }
-    config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp', 'actimeo=1']
+    config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1']
   else
     config.vm.synced_folder ".", "/vagrant"
   end
@@ -129,9 +143,12 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
   config.vm.network :forwarded_port, guest: 8080, host: 8080
 
   # Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
-  config.vm.provision :shell, inline: $provision, privileged: false
+  config.vm.provision :shell, inline: $provisionA, privileged: false, reset: true
+  config.vm.provision :shell, inline: $provisionB, privileged: false
 
-  # Start up script, runs on every 'vagrant up'
-  config.vm.provision :shell, inline: $start, run: 'always', privileged: false
+  config.vm.post_up_message = <<MESSAGE
+To start server
+  $ vagrant ssh -c "cd /vagrant && foreman start"
+MESSAGE
 
 end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 40bf685c5..9beb8fde6 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -55,12 +55,14 @@ module Admin
     def approve
       authorize @account.user, :approve?
       @account.user.approve!
+      log_action :approve, @account.user
       redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
     end
 
     def reject
       authorize @account.user, :reject?
       DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
+      log_action :reject, @account.user
       redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
     end
 
diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb
index ae7f7d076..f48300072 100644
--- a/app/controllers/api/v1/admin/accounts_controller.rb
+++ b/app/controllers/api/v1/admin/accounts_controller.rb
@@ -54,12 +54,14 @@ class Api::V1::Admin::AccountsController < Api::BaseController
   def approve
     authorize @account.user, :approve?
     @account.user.approve!
+    log_action :approve, @account.user
     render json: @account, serializer: REST::Admin::AccountSerializer
   end
 
   def reject
     authorize @account.user, :reject?
     DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
+    log_action :reject, @account.user
     render_empty
   end
 
diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb
index 149139b40..772791b25 100644
--- a/app/controllers/api/v1/filters_controller.rb
+++ b/app/controllers/api/v1/filters_controller.rb
@@ -13,7 +13,7 @@ class Api::V1::FiltersController < Api::BaseController
 
   def create
     ApplicationRecord.transaction do
-      filter_category = current_account.custom_filters.create!(resource_params)
+      filter_category = current_account.custom_filters.create!(filter_params)
       @filter = filter_category.keywords.create!(keyword_params)
     end
 
@@ -52,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController
   end
 
   def resource_params
-    params.permit(:phrase, :expires_in, :irreversible, context: [])
+    params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
   end
 
   def filter_params
-    resource_params.slice(:expires_in, :irreversible, :context)
+    resource_params.slice(:phrase, :expires_in, :irreversible, :context)
   end
 
   def keyword_params
diff --git a/app/javascript/mastodon/features/ui/components/header.js b/app/javascript/mastodon/features/ui/components/header.js
index bbb0ca1c6..1384bebda 100644
--- a/app/javascript/mastodon/features/ui/components/header.js
+++ b/app/javascript/mastodon/features/ui/components/header.js
@@ -6,6 +6,7 @@ import { registrationsOpen, me } from 'mastodon/initial_state';
 import Avatar from 'mastodon/components/avatar';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
+import { openModal } from 'mastodon/actions/modal';
 
 const Account = connect(state => ({
   account: state.getIn(['accounts', me]),
@@ -15,7 +16,14 @@ const Account = connect(state => ({
   </Link>
 ));
 
-export default @withRouter
+const mapDispatchToProps = (dispatch) => ({
+  openClosedRegistrationsModal() {
+    dispatch(openModal('CLOSED_REGISTRATIONS'));
+  },
+});
+
+export default @connect(null, mapDispatchToProps)
+@withRouter
 class Header extends React.PureComponent {
 
   static contextTypes = {
@@ -23,12 +31,13 @@ class Header extends React.PureComponent {
   };
 
   static propTypes = {
+    openClosedRegistrationsModal: PropTypes.func,
     location: PropTypes.object,
   };
 
   render () {
     const { signedIn } = this.context.identity;
-    const { location } = this.props;
+    const { location, openClosedRegistrationsModal } = this.props;
 
     let content;
 
@@ -40,10 +49,26 @@ class Header extends React.PureComponent {
         </>
       );
     } else {
+      let signupButton;
+
+      if (registrationsOpen) {
+        signupButton = (
+          <a href='/auth/sign_up' className='button button-tertiary'>
+            <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+          </a>
+        );
+      } else {
+        signupButton = (
+          <button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
+            <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+          </button>
+        );
+      }
+
       content = (
         <>
           <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
-          <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a>
+          {signupButton}
         </>
       );
     }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8a2f64526..3650e3ec8 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2217,6 +2217,7 @@ $ui-header-height: 55px;
   z-index: 2;
   justify-content: space-between;
   align-items: center;
+  overflow: hidden;
 
   &__logo {
     display: inline-flex;
@@ -2233,10 +2234,15 @@ $ui-header-height: 55px;
     align-items: center;
     gap: 10px;
     padding: 0 10px;
+    overflow: hidden;
 
     .button {
       flex: 0 0 auto;
     }
+
+    .button-tertiary {
+      flex-shrink: 1;
+    }
   }
 }
 
@@ -7138,10 +7144,12 @@ noscript {
 
       .verified {
         border: 1px solid rgba($valid-value-color, 0.5);
+        margin-top: -1px;
 
         &:first-child {
           border-top-left-radius: 4px;
           border-top-right-radius: 4px;
+          margin-top: 0;
         }
 
         &:last-child {
@@ -7973,7 +7981,8 @@ noscript {
   width: 600px;
   background: $ui-base-color;
   border-radius: 8px;
-  overflow: hidden;
+  overflow-x: hidden;
+  overflow-y: auto;
   position: relative;
   display: block;
   padding: 20px;
diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb
index 16276158d..fa8cb6013 100644
--- a/app/models/account_migration.rb
+++ b/app/models/account_migration.rb
@@ -59,7 +59,7 @@ class AccountMigration < ApplicationRecord
 
   def set_target_account
     self.target_account = ResolveAccountService.new.call(acct, skip_cache: true)
-  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error, Addressable::URI::InvalidURIError
     # Validation will take care of it
   end
 
diff --git a/app/models/concerns/domain_materializable.rb b/app/models/concerns/domain_materializable.rb
index 798672213..0eac6878e 100644
--- a/app/models/concerns/domain_materializable.rb
+++ b/app/models/concerns/domain_materializable.rb
@@ -14,7 +14,6 @@ module DomainMaterializable
 
     Instance.refresh
     count_unique_subdomains!
-
   end
 
   def count_unique_subdomains!
diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb
index da2a91493..5a4a974be 100644
--- a/app/models/custom_filter.rb
+++ b/app/models/custom_filter.rb
@@ -54,7 +54,7 @@ class CustomFilter < ApplicationRecord
   end
 
   def irreversible=(value)
-    self.action = value ? :hide : :warn
+    self.action = ActiveModel::Type::Boolean.new.cast(value) ? :hide : :warn
   end
 
   def irreversible?
diff --git a/app/models/form/redirect.rb b/app/models/form/redirect.rb
index 795fdd057..3cd5731e6 100644
--- a/app/models/form/redirect.rb
+++ b/app/models/form/redirect.rb
@@ -32,7 +32,7 @@ class Form::Redirect
 
   def set_target_account
     @target_account = ResolveAccountService.new.call(acct, skip_cache: true)
-  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error, Addressable::URI::InvalidURIError
     # Validation will take care of it
   end
 
diff --git a/config/application.rb b/config/application.rb
index 6fa3654d8..83124cfda 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -71,6 +71,7 @@ module Mastodon
       :af,
       :ar,
       :ast,
+      :be,
       :bg,
       :bn,
       :br,
diff --git a/config/database.yml b/config/database.yml
index 127a78abf..bfb53f21b 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -2,6 +2,7 @@ default: &default
   adapter: postgresql
   pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
   timeout: 5000
+  connect_timeout: 15
   encoding: unicode
   sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
 
diff --git a/db/post_migrate/20221101190723_backfill_admin_action_logs.rb b/db/post_migrate/20221101190723_backfill_admin_action_logs.rb
index 9a64d1715..48ef1e6e3 100644
--- a/db/post_migrate/20221101190723_backfill_admin_action_logs.rb
+++ b/db/post_migrate/20221101190723_backfill_admin_action_logs.rb
@@ -79,69 +79,72 @@ class BackfillAdminActionLogs < ActiveRecord::Migration[6.1]
     safety_assured do
       AdminActionLog.includes(:account).where(target_type: 'Account', human_identifier: nil).find_each do |log|
         next if log.account.nil?
-        log.update(human_identifier: log.account.acct)
+        log.update_attribute('human_identifier', log.account.acct)
       end
 
       AdminActionLog.includes(user: :account).where(target_type: 'User', human_identifier: nil).find_each do |log|
         next if log.user.nil?
-        log.update(human_identifier: log.user.account.acct, route_param: log.user.account_id)
+        log.update_attribute('human_identifier', log.user.account.acct)
+        log.update_attribute('route_param', log.user.account_id)
       end
 
       Admin::ActionLog.where(target_type: 'Report', human_identifier: nil).in_batches.update_all('human_identifier = target_id::text')
 
       AdminActionLog.includes(:domain_block).where(target_type: 'DomainBlock').find_each do |log|
         next if log.domain_block.nil?
-        log.update(human_identifier: log.domain_block.domain)
+        log.update_attribute('human_identifier', log.domain_block.domain)
       end
 
       AdminActionLog.includes(:domain_allow).where(target_type: 'DomainAllow').find_each do |log|
         next if log.domain_allow.nil?
-        log.update(human_identifier: log.domain_allow.domain)
+        log.update_attribute('human_identifier', log.domain_allow.domain)
       end
 
       AdminActionLog.includes(:email_domain_block).where(target_type: 'EmailDomainBlock').find_each do |log|
         next if log.email_domain_block.nil?
-        log.update(human_identifier: log.email_domain_block.domain)
+        log.update_attribute('human_identifier', log.email_domain_block.domain)
       end
 
       AdminActionLog.includes(:unavailable_domain).where(target_type: 'UnavailableDomain').find_each do |log|
         next if log.unavailable_domain.nil?
-        log.update(human_identifier: log.unavailable_domain.domain)
+        log.update_attribute('human_identifier', log.unavailable_domain.domain)
       end
 
       AdminActionLog.includes(status: :account).where(target_type: 'Status', human_identifier: nil).find_each do |log|
         next if log.status.nil?
-        log.update(human_identifier: log.status.account.acct, permalink: log.status.uri)
+        log.update_attribute('human_identifier', log.status.account.acct)
+        log.update_attribute('permalink', log.status.uri)
       end
 
       AdminActionLog.includes(account_warning: :account).where(target_type: 'AccountWarning', human_identifier: nil).find_each do |log|
         next if log.account_warning.nil?
-        log.update(human_identifier: log.account_warning.account.acct)
+        log.update_attribute('human_identifier', log.account_warning.account.acct)
       end
 
       AdminActionLog.includes(:announcement).where(target_type: 'Announcement', human_identifier: nil).find_each do |log|
         next if log.announcement.nil?
-        log.update(human_identifier: log.announcement.text)
+        log.update_attribute('human_identifier', log.announcement.text)
       end
 
       AdminActionLog.includes(:ip_block).where(target_type: 'IpBlock', human_identifier: nil).find_each do |log|
         next if log.ip_block.nil?
-        log.update(human_identifier: "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}")
+        log.update_attribute('human_identifier', "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}")
       end
 
       AdminActionLog.includes(:custom_emoji).where(target_type: 'CustomEmoji', human_identifier: nil).find_each do |log|
         next if log.custom_emoji.nil?
-        log.update(human_identifier: log.custom_emoji.shortcode)
+        log.update_attribute('human_identifier', log.custom_emoji.shortcode)
       end
 
       AdminActionLog.includes(:canonical_email_block).where(target_type: 'CanonicalEmailBlock', human_identifier: nil).find_each do |log|
         next if log.canonical_email_block.nil?
-        log.update(human_identifier: log.canonical_email_block.canonical_email_hash)
+        log.update_attribute('human_identifier', log.canonical_email_block.canonical_email_hash)
       end
 
       AdminActionLog.includes(appeal: :account).where(target_type: 'Appeal', human_identifier: nil).find_each do |log|
         next if log.appeal.nil?
-        log.update(human_identifier: log.appeal.account.acct, route_param: log.appeal.account_warning_id)
+        log.update_attribute('human_identifier', log.appeal.account.acct)
+        log.update_attribute('route_param', log.appeal.account_warning_id)
       end
     end
   end
diff --git a/db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb b/db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb
new file mode 100644
index 000000000..279053ab9
--- /dev/null
+++ b/db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+class BackfillAdminActionLogsAgain < ActiveRecord::Migration[6.1]
+  disable_ddl_transaction!
+
+  class Account < ApplicationRecord
+    # Dummy class, to make migration possible across version changes
+    has_one :user, inverse_of: :account
+
+    def local?
+      domain.nil?
+    end
+
+    def acct
+      local? ? username : "#{username}@#{domain}"
+    end
+  end
+
+  class User < ApplicationRecord
+    # Dummy class, to make migration possible across version changes
+    belongs_to :account
+  end
+
+  class Status < ApplicationRecord
+    include RoutingHelper
+
+    # Dummy class, to make migration possible across version changes
+    belongs_to :account
+
+    def local?
+      attributes['local'] || attributes['uri'].nil?
+    end
+
+    def uri
+      local? ? activity_account_status_url(account, self) : attributes['uri']
+    end
+  end
+
+  class DomainBlock < ApplicationRecord; end
+  class DomainAllow < ApplicationRecord; end
+  class EmailDomainBlock < ApplicationRecord; end
+  class UnavailableDomain < ApplicationRecord; end
+
+  class AccountWarning < ApplicationRecord
+    # Dummy class, to make migration possible across version changes
+    belongs_to :account
+  end
+
+  class Announcement < ApplicationRecord; end
+  class IpBlock < ApplicationRecord; end
+  class CustomEmoji < ApplicationRecord; end
+  class CanonicalEmailBlock < ApplicationRecord; end
+
+  class Appeal < ApplicationRecord
+    # Dummy class, to make migration possible across version changes
+    belongs_to :account
+  end
+
+  class AdminActionLog < ApplicationRecord
+    # Dummy class, to make migration possible across version changes
+
+    # Cannot use usual polymorphic support because of namespacing issues
+    belongs_to :status, foreign_key: :target_id
+    belongs_to :account, foreign_key: :target_id
+    belongs_to :user, foreign_key: :user_id
+    belongs_to :domain_block, foreign_key: :target_id
+    belongs_to :domain_allow, foreign_key: :target_id
+    belongs_to :email_domain_block, foreign_key: :target_id
+    belongs_to :unavailable_domain, foreign_key: :target_id
+    belongs_to :account_warning, foreign_key: :target_id
+    belongs_to :announcement, foreign_key: :target_id
+    belongs_to :ip_block, foreign_key: :target_id
+    belongs_to :custom_emoji, foreign_key: :target_id
+    belongs_to :canonical_email_block, foreign_key: :target_id
+    belongs_to :appeal, foreign_key: :target_id
+  end
+
+  def up
+    safety_assured do
+      AdminActionLog.includes(:account).where(target_type: 'Account', human_identifier: nil).find_each do |log|
+        next if log.account.nil?
+        log.update_attribute('human_identifier', log.account.acct)
+      end
+
+      AdminActionLog.includes(user: :account).where(target_type: 'User', human_identifier: nil).find_each do |log|
+        next if log.user.nil?
+        log.update_attribute('human_identifier', log.user.account.acct)
+        log.update_attribute('route_param', log.user.account_id)
+      end
+
+      Admin::ActionLog.where(target_type: 'Report', human_identifier: nil).in_batches.update_all('human_identifier = target_id::text')
+
+      AdminActionLog.includes(:domain_block).where(target_type: 'DomainBlock').find_each do |log|
+        next if log.domain_block.nil?
+        log.update_attribute('human_identifier', log.domain_block.domain)
+      end
+
+      AdminActionLog.includes(:domain_allow).where(target_type: 'DomainAllow').find_each do |log|
+        next if log.domain_allow.nil?
+        log.update_attribute('human_identifier', log.domain_allow.domain)
+      end
+
+      AdminActionLog.includes(:email_domain_block).where(target_type: 'EmailDomainBlock').find_each do |log|
+        next if log.email_domain_block.nil?
+        log.update_attribute('human_identifier', log.email_domain_block.domain)
+      end
+
+      AdminActionLog.includes(:unavailable_domain).where(target_type: 'UnavailableDomain').find_each do |log|
+        next if log.unavailable_domain.nil?
+        log.update_attribute('human_identifier', log.unavailable_domain.domain)
+      end
+
+      AdminActionLog.includes(status: :account).where(target_type: 'Status', human_identifier: nil).find_each do |log|
+        next if log.status.nil?
+        log.update_attribute('human_identifier', log.status.account.acct)
+        log.update_attribute('permalink', log.status.uri)
+      end
+
+      AdminActionLog.includes(account_warning: :account).where(target_type: 'AccountWarning', human_identifier: nil).find_each do |log|
+        next if log.account_warning.nil?
+        log.update_attribute('human_identifier', log.account_warning.account.acct)
+      end
+
+      AdminActionLog.includes(:announcement).where(target_type: 'Announcement', human_identifier: nil).find_each do |log|
+        next if log.announcement.nil?
+        log.update_attribute('human_identifier', log.announcement.text)
+      end
+
+      AdminActionLog.includes(:ip_block).where(target_type: 'IpBlock', human_identifier: nil).find_each do |log|
+        next if log.ip_block.nil?
+        log.update_attribute('human_identifier', "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}")
+      end
+
+      AdminActionLog.includes(:custom_emoji).where(target_type: 'CustomEmoji', human_identifier: nil).find_each do |log|
+        next if log.custom_emoji.nil?
+        log.update_attribute('human_identifier', log.custom_emoji.shortcode)
+      end
+
+      AdminActionLog.includes(:canonical_email_block).where(target_type: 'CanonicalEmailBlock', human_identifier: nil).find_each do |log|
+        next if log.canonical_email_block.nil?
+        log.update_attribute('human_identifier', log.canonical_email_block.canonical_email_hash)
+      end
+
+      AdminActionLog.includes(appeal: :account).where(target_type: 'Appeal', human_identifier: nil).find_each do |log|
+        next if log.appeal.nil?
+        log.update_attribute('human_identifier', log.appeal.account.acct)
+        log.update_attribute('route_param', log.appeal.account_warning_id)
+      end
+    end
+  end
+
+  def down; end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 01a95c0f2..7462bf2c7 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_11_04_133904) do
+ActiveRecord::Schema.define(version: 2022_12_06_114142) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake
index 96d2f7112..1dd25abb9 100644
--- a/lib/tasks/tests.rake
+++ b/lib/tasks/tests.rake
@@ -43,6 +43,16 @@ namespace :tests do
         puts 'CustomFilterKeyword records not created as expected'
         exit(1)
       end
+
+      unless Admin::ActionLog.find_by(target_type: 'DomainBlock', target_id: 1).human_identifier == 'example.org'
+        puts 'Admin::ActionLog domain block records not updated as expected'
+        exit(1)
+      end
+
+      unless Admin::ActionLog.find_by(target_type: 'EmailDomainBlock', target_id: 1).human_identifier == 'example.org'
+        puts 'Admin::ActionLog email domain block records not updated as expected'
+        exit(1)
+      end
     end
 
     desc 'Populate the database with test data for 2.4.3'
@@ -84,8 +94,8 @@ namespace :tests do
         VALUES
           (1, 'destroy', 'Account', 1, now(), now()),
           (1, 'destroy', 'User', 1, now(), now()),
-          (1, 'destroy', 'DomainBlock', 1312, now(), now()),
-          (1, 'destroy', 'EmailDomainBlock', 1312, now(), now()),
+          (1, 'destroy', 'DomainBlock', 1, now(), now()),
+          (1, 'destroy', 'EmailDomainBlock', 1, now(), now()),
           (1, 'destroy', 'Status', 1, now(), now()),
           (1, 'destroy', 'CustomEmoji', 3, now(), now());
       SQL
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 1bd51a0c8..81d592ddd 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -147,6 +147,87 @@ RSpec.describe Admin::AccountsController, type: :controller do
     end
   end
 
+  describe 'POST #approve' do
+    subject { post :approve, params: { id: account.id } }
+
+    let(:current_user) { Fabricate(:user, role: role) }
+    let(:account) { user.account }
+    let(:user) { Fabricate(:user) }
+
+    before do
+      account.user.update(approved: false)
+    end
+
+    context 'when user is admin' do
+      let(:role) { UserRole.find_by(name: 'Admin') }
+
+      it 'succeeds in approving account' do
+        is_expected.to redirect_to admin_accounts_path(status: 'pending')
+        expect(user.reload).to be_approved
+      end
+
+      it 'logs action' do
+        is_expected.to have_http_status :found
+
+        log_item = Admin::ActionLog.last
+
+        expect(log_item).to_not be_nil
+        expect(log_item.action).to eq :approve
+        expect(log_item.account_id).to eq current_user.account_id
+        expect(log_item.target_id).to eq account.user.id
+      end
+    end
+
+    context 'when user is not admin' do
+      let(:role) { UserRole.everyone }
+
+      it 'fails to approve account' do
+        is_expected.to have_http_status :forbidden
+        expect(user.reload).not_to be_approved
+      end
+    end
+  end
+
+  describe 'POST #reject' do
+    subject { post :reject, params: { id: account.id } }
+
+    let(:current_user) { Fabricate(:user, role: role) }
+    let(:account) { user.account }
+    let(:user) { Fabricate(:user) }
+
+    before do
+      account.user.update(approved: false)
+    end
+
+    context 'when user is admin' do
+      let(:role) { UserRole.find_by(name: 'Admin') }
+
+      it 'succeeds in rejecting account' do
+        is_expected.to redirect_to admin_accounts_path(status: 'pending')
+      end
+
+      it 'logs action' do
+        is_expected.to have_http_status :found
+
+        log_item = Admin::ActionLog.last
+
+        expect(log_item).to_not be_nil
+        expect(log_item.action).to eq :reject
+        expect(log_item.account_id).to eq current_user.account_id
+        expect(log_item.target_id).to eq account.user.id
+      end
+    end
+
+    context 'when user is not admin' do
+      let(:role) { UserRole.everyone }
+
+      it 'fails to reject account' do
+        is_expected.to have_http_status :forbidden
+        expect(user.reload).not_to be_approved
+      end
+    end
+  end
+
   describe 'POST #redownload' do
     subject { post :redownload, params: { id: account.id } }
 
diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
index cd38030e0..8d35b86cb 100644
--- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
@@ -100,6 +100,15 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
     it 'approves user' do
       expect(account.reload.user_approved?).to be true
     end
+
+    it 'logs action' do
+      log_item = Admin::ActionLog.last
+
+      expect(log_item).to_not be_nil
+      expect(log_item.action).to eq :approve
+      expect(log_item.account_id).to eq user.account_id
+      expect(log_item.target_id).to eq account.user.id
+    end
   end
 
   describe 'POST #reject' do
@@ -118,6 +127,15 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
     it 'removes user' do
       expect(User.where(id: account.user.id).count).to eq 0
     end
+
+    it 'logs action' do
+      log_item = Admin::ActionLog.last
+
+      expect(log_item).to_not be_nil
+      expect(log_item.action).to eq :reject
+      expect(log_item.account_id).to eq user.account_id
+      expect(log_item.target_id).to eq account.user.id
+    end
   end
 
   describe 'POST #enable' do
diff --git a/spec/controllers/api/v1/filters_controller_spec.rb b/spec/controllers/api/v1/filters_controller_spec.rb
index af1951f0b..8acb46a00 100644
--- a/spec/controllers/api/v1/filters_controller_spec.rb
+++ b/spec/controllers/api/v1/filters_controller_spec.rb
@@ -22,9 +22,11 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
 
   describe 'POST #create' do
     let(:scopes) { 'write:filters' }
+    let(:irreversible) { true }
+    let(:whole_word)   { false }
 
     before do
-      post :create, params: { phrase: 'magic', context: %w(home), irreversible: true }
+      post :create, params: { phrase: 'magic', context: %w(home), irreversible: irreversible, whole_word: whole_word }
     end
 
     it 'returns http success' do
@@ -34,11 +36,29 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
     it 'creates a filter' do
       filter = user.account.custom_filters.first
       expect(filter).to_not be_nil
-      expect(filter.keywords.pluck(:keyword)).to eq ['magic']
+      expect(filter.keywords.pluck(:keyword, :whole_word)).to eq [['magic', whole_word]]
       expect(filter.context).to eq %w(home)
-      expect(filter.irreversible?).to be true
+      expect(filter.irreversible?).to be irreversible
       expect(filter.expires_at).to be_nil
     end
+
+    context 'with different parameters' do
+      let(:irreversible) { false }
+      let(:whole_word)   { true }
+
+      it 'returns http success' do
+        expect(response).to have_http_status(200)
+      end
+
+      it 'creates a filter' do
+        filter = user.account.custom_filters.first
+        expect(filter).to_not be_nil
+        expect(filter.keywords.pluck(:keyword, :whole_word)).to eq [['magic', whole_word]]
+        expect(filter.context).to eq %w(home)
+        expect(filter.irreversible?).to be irreversible
+        expect(filter.expires_at).to be_nil
+      end
+    end
   end
 
   describe 'GET #show' do
diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb
index 8461b4b28..5f66fe8da 100644
--- a/spec/models/account_migration_spec.rb
+++ b/spec/models/account_migration_spec.rb
@@ -1,5 +1,48 @@
 require 'rails_helper'
 
 RSpec.describe AccountMigration, type: :model do
+  describe 'validations' do
+    let(:source_account) { Fabricate(:account) }
+    let(:target_acct)    { target_account.acct }
 
+    let(:subject) { AccountMigration.new(account: source_account, acct: target_acct) }
+
+    context 'with valid properties' do
+      let(:target_account) { Fabricate(:account, username: 'target', domain: 'remote.org') }
+
+      before do
+        target_account.aliases.create!(acct: source_account.acct)
+
+        service_double = double
+        allow(ResolveAccountService).to receive(:new).and_return(service_double)
+        allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
+      end
+
+      it 'passes validations' do
+        expect(subject).to be_valid
+      end
+    end
+
+    context 'with unresolveable account' do
+      let(:target_acct) { 'target@remote' }
+
+      before do
+        service_double = double
+        allow(ResolveAccountService).to receive(:new).and_return(service_double)
+        allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
+      end
+
+      it 'has errors on acct field' do
+        expect(subject).to model_have_error_on_field(:acct)
+      end
+    end
+
+    context 'with a space in the domain part' do
+      let(:target_acct) { 'target@remote. org' }
+
+      it 'has errors on acct field' do
+        expect(subject).to model_have_error_on_field(:acct)
+      end
+    end
+  end
 end