From a62f17da17fbebf817796b0278060abe2829c903 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Sun, 12 Jul 2020 19:11:30 -0500
Subject: [PATCH 01/24] Add `approval_pending` field to User

---
 lib/pleroma/user.ex                                      | 2 ++
 .../20200712234852_add_approval_pending_to_users.exs     | 9 +++++++++
 test/user_test.exs                                       | 5 +++++
 3 files changed, 16 insertions(+)
 create mode 100644 priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index b9989f901..25c63fc44 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -106,6 +106,7 @@ defmodule Pleroma.User do
     field(:locked, :boolean, default: false)
     field(:confirmation_pending, :boolean, default: false)
     field(:password_reset_pending, :boolean, default: false)
+    field(:approval_pending, :boolean, default: false)
     field(:confirmation_token, :string, default: nil)
     field(:default_scope, :string, default: "public")
     field(:domain_blocks, {:array, :string}, default: [])
@@ -262,6 +263,7 @@ def binary_id(%User{} = user), do: binary_id(user.id)
   @spec account_status(User.t()) :: account_status()
   def account_status(%User{deactivated: true}), do: :deactivated
   def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
+  def account_status(%User{approval_pending: true}), do: :approval_pending
 
   def account_status(%User{confirmation_pending: true}) do
     if Config.get([:instance, :account_activation_required]) do
diff --git a/priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs b/priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs
new file mode 100644
index 000000000..f7eb8179b
--- /dev/null
+++ b/priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.AddApprovalPendingToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add(:approval_pending, :boolean)
+    end
+  end
+end
diff --git a/test/user_test.exs b/test/user_test.exs
index 9788e09d9..040f532fe 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1342,6 +1342,11 @@ test "returns :deactivated for deactivated user" do
       user = insert(:user, local: true, confirmation_pending: false, deactivated: true)
       assert User.account_status(user) == :deactivated
     end
+
+    test "returns :approval_pending for unapproved user" do
+      user = insert(:user, local: true, confirmation_pending: false, approval_pending: true)
+      assert User.account_status(user) == :approval_pending
+    end
   end
 
   describe "superuser?/1" do

From 51ab8d0128970dd7458e93578acb36c20b1c185c Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Sun, 12 Jul 2020 20:14:57 -0500
Subject: [PATCH 02/24] Add `account_approval_required` instance setting

---
 config/config.exs                                            | 1 +
 config/description.exs                                       | 5 +++++
 docs/configuration/cheatsheet.md                             | 1 +
 lib/pleroma/web/mastodon_api/views/instance_view.ex          | 1 +
 .../mastodon_api/controllers/instance_controller_test.exs    | 1 +
 5 files changed, 9 insertions(+)

diff --git a/config/config.exs b/config/config.exs
index 6fc84efc2..791740663 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -205,6 +205,7 @@
   registrations_open: true,
   invites_enabled: false,
   account_activation_required: false,
+  account_approval_required: false,
   federating: true,
   federation_incoming_replies_max_depth: 100,
   federation_reachability_timeout_days: 7,
diff --git a/config/description.exs b/config/description.exs
index b0cc8d527..e57379dee 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -665,6 +665,11 @@
         type: :boolean,
         description: "Require users to confirm their emails before signing in"
       },
+      %{
+        key: :account_approval_required,
+        type: :boolean,
+        description: "Require users to be manually approved by an admin before signing in"
+      },
       %{
         key: :federating,
         type: :boolean,
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index f796330f1..94389152e 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -33,6 +33,7 @@ To add configuration to your config file, you can copy it from the base config.
 * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
 * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
 * `account_activation_required`: Require users to confirm their emails before signing in.
+* `account_approval_required`: Require users to be manually approved by an admin before signing in.
 * `federating`: Enable federation with other instances.
 * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
 * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 5deb0d7ed..243067a73 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -39,6 +39,7 @@ def render("show.json", _) do
       pleroma: %{
         metadata: %{
           account_activation_required: Keyword.get(instance, :account_activation_required),
+          account_approval_required: Keyword.get(instance, :account_approval_required),
           features: features(),
           federation: federation(),
           fields_limits: fields_limits()
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
index cc880d82c..8a4183283 100644
--- a/test/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -38,6 +38,7 @@ test "get instance information", %{conn: conn} do
            } = result
 
     assert result["pleroma"]["metadata"]["account_activation_required"] != nil
+    assert result["pleroma"]["metadata"]["account_approval_required"] != nil
     assert result["pleroma"]["metadata"]["features"]
     assert result["pleroma"]["metadata"]["federation"]
     assert result["pleroma"]["metadata"]["fields_limits"]

From e4e557781877c7c3e4f6197cc52963025485dbb3 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Sun, 12 Jul 2020 20:15:27 -0500
Subject: [PATCH 03/24] Prevent unapproved users from logging in

---
 lib/pleroma/web/oauth/oauth_controller.ex | 10 ++++++++
 test/web/oauth/oauth_controller_test.exs  | 30 ++++++++++++++++++++++-
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 7683589cf..61fe81d33 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -337,6 +337,16 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirm
     )
   end
 
+  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
+    render_error(
+      conn,
+      :forbidden,
+      "Your account is awaiting approval.",
+      %{},
+      "awaiting_approval"
+    )
+  end
+
   defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
     render_invalid_credentials_error(conn)
   end
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index d389e4ce0..ec5b78750 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -19,7 +19,10 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
     key: "_test",
     signing_salt: "cooldude"
   ]
-  setup do: clear_config([:instance, :account_activation_required])
+  setup do
+    clear_config([:instance, :account_activation_required])
+    clear_config([:instance, :account_approval_required])
+  end
 
   describe "in OAuth consumer mode, " do
     setup do
@@ -995,6 +998,31 @@ test "rejects token exchange for user with confirmation_pending set to true" do
              }
     end
 
+    test "rejects token exchange for valid credentials belonging to an unapproved user and approval is required" do
+      Pleroma.Config.put([:instance, :account_approval_required], true)
+      password = "testpassword"
+
+      user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true)
+
+      refute Pleroma.User.account_status(user) == :active
+
+      app = insert(:oauth_app)
+
+      conn =
+        build_conn()
+        |> post("/oauth/token", %{
+          "grant_type" => "password",
+          "username" => user.nickname,
+          "password" => password,
+          "client_id" => app.client_id,
+          "client_secret" => app.client_secret
+        })
+
+      assert resp = json_response(conn, 403)
+      assert %{"error" => _} = resp
+      refute Map.has_key?(resp, "access_token")
+    end
+
     test "rejects an invalid authorization code" do
       app = insert(:oauth_app)
 

From bcfd38c8f3ecd2620bae7fc756ffc3f4bbe2b89e Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Sun, 12 Jul 2020 21:31:13 -0500
Subject: [PATCH 04/24] Make a user unapproved when registering with
 `account_approval_required` on

---
 lib/pleroma/user.ex | 14 ++++++++++++++
 test/user_test.exs  | 21 +++++++++++++++++++++
 2 files changed, 35 insertions(+)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 25c63fc44..e84900c4f 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -634,8 +634,16 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
         opts[:need_confirmation]
       end
 
+    need_approval? =
+      if is_nil(opts[:need_approval]) do
+        Config.get([:instance, :account_approval_required])
+      else
+        opts[:need_approval]
+      end
+
     struct
     |> confirmation_changeset(need_confirmation: need_confirmation?)
+    |> approval_changeset(need_approval: need_approval?)
     |> cast(params, [
       :bio,
       :raw_bio,
@@ -2145,6 +2153,12 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
     cast(user, params, [:confirmation_pending, :confirmation_token])
   end
 
+  @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
+  def approval_changeset(user, need_approval: need_approval?) do
+    params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
+    cast(user, params, [:approval_pending])
+  end
+
   def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
     if id not in user.pinned_activities do
       max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
diff --git a/test/user_test.exs b/test/user_test.exs
index 040f532fe..e57453982 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -516,6 +516,27 @@ test "it creates confirmed user if :confirmed option is given" do
     end
   end
 
+  describe "user registration, with :account_approval_required" do
+    @full_user_data %{
+      bio: "A guy",
+      name: "my name",
+      nickname: "nick",
+      password: "test",
+      password_confirmation: "test",
+      email: "email@example.com"
+    }
+    setup do: clear_config([:instance, :account_approval_required], true)
+
+    test "it creates unapproved user" do
+      changeset = User.register_changeset(%User{}, @full_user_data)
+      assert changeset.valid?
+
+      {:ok, user} = Repo.insert(changeset)
+
+      assert user.approval_pending
+    end
+  end
+
   describe "get_or_fetch/1" do
     test "gets an existing user by nickname" do
       user = insert(:user)

From 5ddf0415c4fd6021422eb38b4625c01ad27582c5 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 00:22:12 -0500
Subject: [PATCH 05/24] Accept `reason` in POST /api/v1/accounts and store in
 DB

---
 lib/pleroma/user.ex                           |  4 +-
 lib/pleroma/web/twitter_api/twitter_api.ex    |  1 +
 ...43918_add_registration_reason_to_users.exs |  9 +++
 .../controllers/account_controller_test.exs   | 70 +++++++++++++++++++
 4 files changed, 83 insertions(+), 1 deletion(-)
 create mode 100644 priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index e84900c4f..51ccf6ffa 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -107,6 +107,7 @@ defmodule Pleroma.User do
     field(:confirmation_pending, :boolean, default: false)
     field(:password_reset_pending, :boolean, default: false)
     field(:approval_pending, :boolean, default: false)
+    field(:registration_reason, :string, default: nil)
     field(:confirmation_token, :string, default: nil)
     field(:default_scope, :string, default: "public")
     field(:domain_blocks, {:array, :string}, default: [])
@@ -653,7 +654,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
       :password,
       :password_confirmation,
       :emoji,
-      :accepts_chat_messages
+      :accepts_chat_messages,
+      :registration_reason
     ])
     |> validate_required([:name, :nickname, :password, :password_confirmation])
     |> validate_confirmation(:password)
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 5cfb385ac..4ff021b82 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -19,6 +19,7 @@ def register_user(params, opts \\ []) do
       |> Map.put(:nickname, params[:username])
       |> Map.put(:name, Map.get(params, :fullname, params[:username]))
       |> Map.put(:password_confirmation, params[:password])
+      |> Map.put(:registration_reason, params[:reason])
 
     if Pleroma.Config.get([:instance, :registrations_open]) do
       create_user(params, opts)
diff --git a/priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs b/priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs
new file mode 100644
index 000000000..fa02fded4
--- /dev/null
+++ b/priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.AddRegistrationReasonToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add(:registration_reason, :string)
+    end
+  end
+end
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index 9c7b5e9b2..28d21371a 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -885,6 +885,7 @@ test "blocking / unblocking a user" do
     end
 
     setup do: clear_config([:instance, :account_activation_required])
+    setup do: clear_config([:instance, :account_approval_required])
 
     test "Account registration via Application", %{conn: conn} do
       conn =
@@ -949,6 +950,75 @@ test "Account registration via Application", %{conn: conn} do
       assert token_from_db.user.confirmation_pending
     end
 
+    test "Account registration via app with account_approval_required", %{conn: conn} do
+      Pleroma.Config.put([:instance, :account_approval_required], true)
+
+      conn =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/apps", %{
+          client_name: "client_name",
+          redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
+          scopes: "read, write, follow"
+        })
+
+      assert %{
+               "client_id" => client_id,
+               "client_secret" => client_secret,
+               "id" => _,
+               "name" => "client_name",
+               "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+               "vapid_key" => _,
+               "website" => nil
+             } = json_response_and_validate_schema(conn, 200)
+
+      conn =
+        post(conn, "/oauth/token", %{
+          grant_type: "client_credentials",
+          client_id: client_id,
+          client_secret: client_secret
+        })
+
+      assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
+               json_response(conn, 200)
+
+      assert token
+      token_from_db = Repo.get_by(Token, token: token)
+      assert token_from_db
+      assert refresh
+      assert scope == "read write follow"
+
+      conn =
+        build_conn()
+        |> put_req_header("content-type", "multipart/form-data")
+        |> put_req_header("authorization", "Bearer " <> token)
+        |> post("/api/v1/accounts", %{
+          username: "lain",
+          email: "lain@example.org",
+          password: "PlzDontHackLain",
+          bio: "Test Bio",
+          agreement: true,
+          reason: "I'm a cool dude, bro"
+        })
+
+      %{
+        "access_token" => token,
+        "created_at" => _created_at,
+        "scope" => ^scope,
+        "token_type" => "Bearer"
+      } = json_response_and_validate_schema(conn, 200)
+
+      token_from_db = Repo.get_by(Token, token: token)
+      assert token_from_db
+      token_from_db = Repo.preload(token_from_db, :user)
+      assert token_from_db.user
+
+      assert token_from_db.user.confirmation_pending
+      assert token_from_db.user.approval_pending
+
+      assert token_from_db.user.registration_reason == "I'm a cool dude, bro"
+    end
+
     test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do
       _user = insert(:user, email: "lain@example.org")
       app_token = insert(:oauth_token, user: nil)

From a1570ba6ad4c871df3783d06772b2eb8d2d6c4f1 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 13:04:57 -0500
Subject: [PATCH 06/24] AdminAPI: Return `registration_reason` with users

---
 docs/API/admin_api.md                         |  3 +-
 .../web/admin_api/views/account_view.ex       |  3 +-
 .../controllers/admin_api_controller_test.exs | 70 +++++++++++++------
 3 files changed, 51 insertions(+), 25 deletions(-)

diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index baf895d90..de4e36efa 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -46,7 +46,8 @@ Configuration options:
       "local": bool,
       "tags": array,
       "avatar": string,
-      "display_name": string
+      "display_name": string,
+      "registration_reason": string,
     },
     ...
   ]
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index e1e929632..78062e520 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -77,7 +77,8 @@ def render("show.json", %{user: user}) do
       "roles" => User.roles(user),
       "tags" => user.tags || [],
       "confirmation_pending" => user.confirmation_pending,
-      "url" => user.uri || user.ap_id
+      "url" => user.uri || user.ap_id,
+      "registration_reason" => user.registration_reason
     }
   end
 
diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs
index c2433f23c..556e8d97a 100644
--- a/test/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/web/admin_api/controllers/admin_api_controller_test.exs
@@ -338,7 +338,8 @@ test "Show", %{conn: conn} do
         "avatar" => User.avatar_url(user) |> MediaProxy.url(),
         "display_name" => HTML.strip_tags(user.name || user.nickname),
         "confirmation_pending" => false,
-        "url" => user.ap_id
+        "url" => user.ap_id,
+        "registration_reason" => nil
       }
 
       assert expected == json_response(conn, 200)
@@ -601,7 +602,9 @@ test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
 
   describe "GET /api/pleroma/admin/users" do
     test "renders users array for the first page", %{conn: conn, admin: admin} do
-      user = insert(:user, local: false, tags: ["foo", "bar"])
+      user =
+        insert(:user, local: false, tags: ["foo", "bar"], registration_reason: "I'm a chill dude")
+
       conn = get(conn, "/api/pleroma/admin/users?page=1")
 
       users =
@@ -616,7 +619,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
-            "url" => admin.ap_id
+            "url" => admin.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => user.deactivated,
@@ -628,7 +632,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
             "confirmation_pending" => false,
-            "url" => user.ap_id
+            "url" => user.ap_id,
+            "registration_reason" => "I'm a chill dude"
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -701,7 +706,8 @@ test "regular search", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -727,7 +733,8 @@ test "search by domain", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -753,7 +760,8 @@ test "search by full nickname", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -779,7 +787,8 @@ test "search by display name", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -805,7 +814,8 @@ test "search by email", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -831,7 +841,8 @@ test "regular search with page size", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -852,7 +863,8 @@ test "regular search with page size", %{conn: conn} do
                    "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user2.name || user2.nickname),
                    "confirmation_pending" => false,
-                   "url" => user2.ap_id
+                   "url" => user2.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -885,7 +897,8 @@ test "only local users" do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -911,7 +924,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
             "confirmation_pending" => false,
-            "url" => user.ap_id
+            "url" => user.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => admin.deactivated,
@@ -923,7 +937,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
-            "url" => admin.ap_id
+            "url" => admin.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => false,
@@ -935,7 +950,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
             "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
             "confirmation_pending" => false,
-            "url" => old_admin.ap_id
+            "url" => old_admin.ap_id,
+            "registration_reason" => nil
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -966,7 +982,8 @@ test "load only admins", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
-            "url" => admin.ap_id
+            "url" => admin.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => false,
@@ -978,7 +995,8 @@ test "load only admins", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
             "confirmation_pending" => false,
-            "url" => second_admin.ap_id
+            "url" => second_admin.ap_id,
+            "registration_reason" => nil
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1011,7 +1029,8 @@ test "load only moderators", %{conn: conn} do
                    "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
                    "confirmation_pending" => false,
-                   "url" => moderator.ap_id
+                   "url" => moderator.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -1037,7 +1056,8 @@ test "load users with tags list", %{conn: conn} do
             "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user1.name || user1.nickname),
             "confirmation_pending" => false,
-            "url" => user1.ap_id
+            "url" => user1.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => false,
@@ -1049,7 +1069,8 @@ test "load users with tags list", %{conn: conn} do
             "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user2.name || user2.nickname),
             "confirmation_pending" => false,
-            "url" => user2.ap_id
+            "url" => user2.ap_id,
+            "registration_reason" => nil
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1089,7 +1110,8 @@ test "it works with multiple filters" do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -1114,7 +1136,8 @@ test "it omits relay user", %{admin: admin, conn: conn} do
                    "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(admin.name || admin.nickname),
                    "confirmation_pending" => false,
-                   "url" => admin.ap_id
+                   "url" => admin.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -1177,7 +1200,8 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admi
                "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                "display_name" => HTML.strip_tags(user.name || user.nickname),
                "confirmation_pending" => false,
-               "url" => user.ap_id
+               "url" => user.ap_id,
+               "registration_reason" => nil
              }
 
     log_entry = Repo.one(ModerationLog)

From b750129da1434823746e3dbc237d0e04552fa753 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 13:47:05 -0500
Subject: [PATCH 07/24] AdminAPI: Return `approval_pending` with users

---
 docs/API/admin_api.md                         |  2 +
 .../web/admin_api/views/account_view.ex       |  1 +
 .../controllers/admin_api_controller_test.exs | 42 +++++++++++++++++--
 3 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index de4e36efa..fdd9df6c7 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -47,6 +47,8 @@ Configuration options:
       "tags": array,
       "avatar": string,
       "display_name": string,
+      "confirmation_pending": bool,
+      "approval_pending": bool,
       "registration_reason": string,
     },
     ...
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index 78062e520..bdab04ad2 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -77,6 +77,7 @@ def render("show.json", %{user: user}) do
       "roles" => User.roles(user),
       "tags" => user.tags || [],
       "confirmation_pending" => user.confirmation_pending,
+      "approval_pending" => user.approval_pending,
       "url" => user.uri || user.ap_id,
       "registration_reason" => user.registration_reason
     }
diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs
index 556e8d97a..ccda5df3f 100644
--- a/test/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/web/admin_api/controllers/admin_api_controller_test.exs
@@ -338,6 +338,7 @@ test "Show", %{conn: conn} do
         "avatar" => User.avatar_url(user) |> MediaProxy.url(),
         "display_name" => HTML.strip_tags(user.name || user.nickname),
         "confirmation_pending" => false,
+        "approval_pending" => false,
         "url" => user.ap_id,
         "registration_reason" => nil
       }
@@ -602,8 +603,8 @@ test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
 
   describe "GET /api/pleroma/admin/users" do
     test "renders users array for the first page", %{conn: conn, admin: admin} do
-      user =
-        insert(:user, local: false, tags: ["foo", "bar"], registration_reason: "I'm a chill dude")
+      user = insert(:user, local: false, tags: ["foo", "bar"])
+      user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude")
 
       conn = get(conn, "/api/pleroma/admin/users?page=1")
 
@@ -619,6 +620,7 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => admin.ap_id,
             "registration_reason" => nil
           },
@@ -632,14 +634,29 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => user.ap_id,
+            "registration_reason" => nil
+          },
+          %{
+            "deactivated" => user2.deactivated,
+            "id" => user2.id,
+            "nickname" => user2.nickname,
+            "roles" => %{"admin" => false, "moderator" => false},
+            "local" => true,
+            "tags" => [],
+            "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
+            "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+            "confirmation_pending" => false,
+            "approval_pending" => true,
+            "url" => user2.ap_id,
             "registration_reason" => "I'm a chill dude"
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
 
       assert json_response(conn, 200) == %{
-               "count" => 2,
+               "count" => 3,
                "page_size" => 50,
                "users" => users
              }
@@ -706,6 +723,7 @@ test "regular search", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -733,6 +751,7 @@ test "search by domain", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -760,6 +779,7 @@ test "search by full nickname", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -787,6 +807,7 @@ test "search by display name", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -814,6 +835,7 @@ test "search by email", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -841,6 +863,7 @@ test "regular search with page size", %{conn: conn} do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -863,6 +886,7 @@ test "regular search with page size", %{conn: conn} do
                    "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user2.name || user2.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user2.ap_id,
                    "registration_reason" => nil
                  }
@@ -897,6 +921,7 @@ test "only local users" do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -924,6 +949,7 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => user.ap_id,
             "registration_reason" => nil
           },
@@ -937,6 +963,7 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => admin.ap_id,
             "registration_reason" => nil
           },
@@ -950,6 +977,7 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
             "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => old_admin.ap_id,
             "registration_reason" => nil
           }
@@ -982,6 +1010,7 @@ test "load only admins", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => admin.ap_id,
             "registration_reason" => nil
           },
@@ -995,6 +1024,7 @@ test "load only admins", %{conn: conn, admin: admin} do
             "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => second_admin.ap_id,
             "registration_reason" => nil
           }
@@ -1029,6 +1059,7 @@ test "load only moderators", %{conn: conn} do
                    "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => moderator.ap_id,
                    "registration_reason" => nil
                  }
@@ -1056,6 +1087,7 @@ test "load users with tags list", %{conn: conn} do
             "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user1.name || user1.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => user1.ap_id,
             "registration_reason" => nil
           },
@@ -1069,6 +1101,7 @@ test "load users with tags list", %{conn: conn} do
             "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user2.name || user2.nickname),
             "confirmation_pending" => false,
+            "approval_pending" => false,
             "url" => user2.ap_id,
             "registration_reason" => nil
           }
@@ -1110,6 +1143,7 @@ test "it works with multiple filters" do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => user.ap_id,
                    "registration_reason" => nil
                  }
@@ -1136,6 +1170,7 @@ test "it omits relay user", %{admin: admin, conn: conn} do
                    "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(admin.name || admin.nickname),
                    "confirmation_pending" => false,
+                   "approval_pending" => false,
                    "url" => admin.ap_id,
                    "registration_reason" => nil
                  }
@@ -1200,6 +1235,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admi
                "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                "display_name" => HTML.strip_tags(user.name || user.nickname),
                "confirmation_pending" => false,
+               "approval_pending" => false,
                "url" => user.ap_id,
                "registration_reason" => nil
              }

From 33f1b29b2c9cfe6f09c6b088b8b6f7bf14379b9b Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 14:14:43 -0500
Subject: [PATCH 08/24] AdminAPI: Filter users by `need_approval`

---
 docs/API/admin_api.md                         |  1 +
 lib/pleroma/user/query.ex                     |  5 +++
 .../controllers/admin_api_controller.ex       |  2 +-
 .../controllers/admin_api_controller_test.exs | 38 +++++++++++++++++++
 test/web/admin_api/search_test.exs            | 11 ++++++
 5 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index fdd9df6c7..42071376e 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -19,6 +19,7 @@ Configuration options:
     - `local`: only local users
     - `external`: only external users
     - `active`: only active users
+    - `need_approval`: only unapproved users
     - `deactivated`: only deactivated users
     - `is_admin`: users with admin role
     - `is_moderator`: users with moderator role
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 66ffe9090..45553cb6c 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -42,6 +42,7 @@ defmodule Pleroma.User.Query do
             external: boolean(),
             active: boolean(),
             deactivated: boolean(),
+            need_approval: boolean(),
             is_admin: boolean(),
             is_moderator: boolean(),
             super_users: boolean(),
@@ -146,6 +147,10 @@ defp compose_query({:deactivated, true}, query) do
     |> where([u], not is_nil(u.nickname))
   end
 
+  defp compose_query({:need_approval, _}, query) do
+    where(query, [u], u.approval_pending)
+  end
+
   defp compose_query({:followers, %User{id: id}}, query) do
     query
     |> where([u], u.id != ^id)
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index e5f14269a..037a6f269 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -350,7 +350,7 @@ def list_users(conn, params) do
     end
   end
 
-  @filters ~w(local external active deactivated is_admin is_moderator)
+  @filters ~w(local external active deactivated need_approval is_admin is_moderator)
 
   @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
   defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs
index ccda5df3f..9cc8b1879 100644
--- a/test/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/web/admin_api/controllers/admin_api_controller_test.exs
@@ -991,6 +991,44 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do
              }
     end
 
+    test "only unapproved users", %{conn: conn} do
+      user =
+        insert(:user,
+          nickname: "sadboy",
+          approval_pending: true,
+          registration_reason: "Plz let me in!"
+        )
+
+      insert(:user, nickname: "happyboy", approval_pending: false)
+
+      conn = get(conn, "/api/pleroma/admin/users?filters=need_approval")
+
+      users =
+        [
+          %{
+            "deactivated" => user.deactivated,
+            "id" => user.id,
+            "nickname" => user.nickname,
+            "roles" => %{"admin" => false, "moderator" => false},
+            "local" => true,
+            "tags" => [],
+            "avatar" => User.avatar_url(user) |> MediaProxy.url(),
+            "display_name" => HTML.strip_tags(user.name || user.nickname),
+            "confirmation_pending" => false,
+            "approval_pending" => true,
+            "url" => user.ap_id,
+            "registration_reason" => "Plz let me in!"
+          }
+        ]
+        |> Enum.sort_by(& &1["nickname"])
+
+      assert json_response(conn, 200) == %{
+               "count" => 1,
+               "page_size" => 50,
+               "users" => users
+             }
+    end
+
     test "load only admins", %{conn: conn, admin: admin} do
       second_admin = insert(:user, is_admin: true)
       insert(:user)
diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs
index e0e3d4153..b974cedd5 100644
--- a/test/web/admin_api/search_test.exs
+++ b/test/web/admin_api/search_test.exs
@@ -166,5 +166,16 @@ test "it returns user by email" do
       assert total == 3
       assert count == 1
     end
+
+    test "it returns unapproved user" do
+      unapproved = insert(:user, approval_pending: true)
+      insert(:user)
+      insert(:user)
+
+      {:ok, _results, total} = Search.user()
+      {:ok, [^unapproved], count} = Search.user(%{need_approval: true})
+      assert total == 3
+      assert count == 1
+    end
   end
 end

From 20d24741af8ae755ce7f753680a55ca24ef7c1d4 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 18:02:44 -0500
Subject: [PATCH 09/24] AdminAPI: Add `PATCH /api/pleroma/admin/users/approve`
 endpoint

---
 docs/API/admin_api.md                         | 18 +++++++++++++
 lib/pleroma/moderation_log.ex                 | 11 ++++++++
 lib/pleroma/user.ex                           | 13 ++++++++++
 .../controllers/admin_api_controller.ex       | 16 ++++++++++++
 lib/pleroma/web/router.ex                     |  1 +
 test/user_test.exs                            | 25 +++++++++++++++++++
 .../controllers/admin_api_controller_test.exs | 20 +++++++++++++++
 7 files changed, 104 insertions(+)

diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index 42071376e..4b143e4ee 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -246,6 +246,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 }
 ```
 
+## `PATCH /api/pleroma/admin/users/approve`
+
+### Approve user
+
+- Params:
+  - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+  users: [
+    {
+      // user object
+    }
+  ]
+}
+```
+
 ## `GET /api/pleroma/admin/users/:nickname_or_id`
 
 ### Retrive the details of a user
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 7aacd9d80..31c9afe2a 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -409,6 +409,17 @@ def get_log_entry_message(%ModerationLog{
     "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
   end
 
+  @spec get_log_entry_message(ModerationLog) :: String.t()
+  def get_log_entry_message(%ModerationLog{
+        data: %{
+          "actor" => %{"nickname" => actor_nickname},
+          "action" => "approve",
+          "subject" => users
+        }
+      }) do
+    "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
+  end
+
   @spec get_log_entry_message(ModerationLog) :: String.t()
   def get_log_entry_message(%ModerationLog{
         data: %{
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 51ccf6ffa..439c2c9b6 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1471,6 +1471,19 @@ def deactivate(%User{} = user, status) do
     end
   end
 
+  def approve(users) when is_list(users) do
+    Repo.transaction(fn ->
+      Enum.map(users, fn user ->
+        with {:ok, user} <- approve(user), do: user
+      end)
+    end)
+  end
+
+  def approve(%User{} = user) do
+    change(user, approval_pending: false)
+    |> update_and_set_cache()
+  end
+
   def update_notification_settings(%User{} = user, settings) do
     user
     |> cast(%{notification_settings: settings}, [])
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 037a6f269..53f71fcbf 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -44,6 +44,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
            :user_toggle_activation,
            :user_activate,
            :user_deactivate,
+           :user_approve,
            :tag_users,
            :untag_users,
            :right_add,
@@ -303,6 +304,21 @@ def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nickname
     |> render("index.json", %{users: Keyword.values(updated_users)})
   end
 
+  def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+    {:ok, updated_users} = User.approve(users)
+
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "approve"
+    })
+
+    conn
+    |> put_view(AccountView)
+    |> render("index.json", %{users: updated_users})
+  end
+
   def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
     with {:ok, _} <- User.tag(nicknames, tags) do
       ModerationLog.insert_log(%{
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 386308362..c6433cc53 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -138,6 +138,7 @@ defmodule Pleroma.Web.Router do
     patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
     patch("/users/activate", AdminAPIController, :user_activate)
     patch("/users/deactivate", AdminAPIController, :user_deactivate)
+    patch("/users/approve", AdminAPIController, :user_approve)
     put("/users/tag", AdminAPIController, :tag_users)
     delete("/users/tag", AdminAPIController, :untag_users)
 
diff --git a/test/user_test.exs b/test/user_test.exs
index e57453982..9da2aa411 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1202,6 +1202,31 @@ test "hide a user's statuses from timelines and notifications" do
     end
   end
 
+  describe "approve" do
+    test "approves a user" do
+      user = insert(:user, approval_pending: true)
+      assert true == user.approval_pending
+      {:ok, user} = User.approve(user)
+      assert false == user.approval_pending
+    end
+
+    test "approves a list of users" do
+      unapproved_users = [
+        insert(:user, approval_pending: true),
+        insert(:user, approval_pending: true),
+        insert(:user, approval_pending: true)
+      ]
+
+      {:ok, users} = User.approve(unapproved_users)
+
+      assert Enum.count(users) == 3
+
+      Enum.each(users, fn user ->
+        assert false == user.approval_pending
+      end)
+    end
+  end
+
   describe "delete" do
     setup do
       {:ok, user} = insert(:user) |> User.set_cache()
diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs
index 9cc8b1879..351df8883 100644
--- a/test/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/web/admin_api/controllers/admin_api_controller_test.exs
@@ -1257,6 +1257,26 @@ test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
              "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
   end
 
+  test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do
+    user_one = insert(:user, approval_pending: true)
+    user_two = insert(:user, approval_pending: true)
+
+    conn =
+      patch(
+        conn,
+        "/api/pleroma/admin/users/approve",
+        %{nicknames: [user_one.nickname, user_two.nickname]}
+      )
+
+    response = json_response(conn, 200)
+    assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false]
+
+    log_entry = Repo.one(ModerationLog)
+
+    assert ModerationLog.get_log_entry_message(log_entry) ==
+             "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
+  end
+
   test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
     user = insert(:user)
 

From fab44f69703975c0f6182ed1c26c1dcdad221dc5 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 18:46:57 -0500
Subject: [PATCH 10/24] Test User with confirmation_pending: true,
 approval_pending: true

---
 test/user_test.exs | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/test/user_test.exs b/test/user_test.exs
index 9da2aa411..cd39e1623 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1390,7 +1390,10 @@ test "returns :deactivated for deactivated user" do
     end
 
     test "returns :approval_pending for unapproved user" do
-      user = insert(:user, local: true, confirmation_pending: false, approval_pending: true)
+      user = insert(:user, local: true, approval_pending: true)
+      assert User.account_status(user) == :approval_pending
+
+      user = insert(:user, local: true, confirmation_pending: true, approval_pending: true)
       assert User.account_status(user) == :approval_pending
     end
   end

From e82060c47253946b79685ebff9a38e2fe0ac360e Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 18:47:23 -0500
Subject: [PATCH 11/24] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5fed80a99..a0f529108 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -58,6 +58,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Support pagination in emoji packs API (for packs and for files in pack)
 - Support for viewing instances favicons next to posts and accounts
 - Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
+- "By approval" registrations mode.
 
 <details>
   <summary>API Changes</summary>

From df3d1bf5e57389e41a70676ccab1df81d83e3d74 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 18:48:17 -0500
Subject: [PATCH 12/24] Add :approval_pending to User @type account_status

---
 lib/pleroma/user.ex | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 439c2c9b6..9c3b46ae8 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -42,7 +42,12 @@ defmodule Pleroma.User do
   require Logger
 
   @type t :: %__MODULE__{}
-  @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
+  @type account_status ::
+          :active
+          | :deactivated
+          | :password_reset_pending
+          | :confirmation_pending
+          | :approval_pending
   @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
 
   # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength

From 0d004a9d046f279be8462e8c751b5f1bcec3d35b Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Tue, 14 Jul 2020 20:31:20 -0500
Subject: [PATCH 13/24] Email admins when a new unapproved account is up for
 review

---
 lib/pleroma/emails/admin_email.ex          | 14 ++++++++
 lib/pleroma/web/twitter_api/twitter_api.ex | 13 ++++++++
 test/emails/admin_email_test.exs           | 20 ++++++++++++
 test/web/twitter_api/twitter_api_test.exs  | 37 ++++++++++++++++++++++
 4 files changed, 84 insertions(+)

diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index aa0b2a66b..fae7faf00 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -82,4 +82,18 @@ def report(to, reporter, account, statuses, comment) do
     |> subject("#{instance_name()} Report")
     |> html_body(html_body)
   end
+
+  def new_unapproved_registration(to, account) do
+    html_body = """
+    <p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
+    <blockquote>#{account.registration_reason}</blockquote>
+    <a href="#{Pleroma.Web.base_url()}/pleroma/admin">Visit AdminFE</a>
+    """
+
+    new()
+    |> to({to.name, to.email})
+    |> from({instance_name(), instance_notify_email()})
+    |> subject("New account up for review on #{instance_name()} (@#{account.nickname})")
+    |> html_body(html_body)
+  end
 end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 4ff021b82..2294d9d0d 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -45,6 +45,7 @@ defp create_user(params, opts) do
 
     case User.register(changeset) do
       {:ok, user} ->
+        maybe_notify_admins(user)
         {:ok, user}
 
       {:error, changeset} ->
@@ -57,6 +58,18 @@ defp create_user(params, opts) do
     end
   end
 
+  defp maybe_notify_admins(%User{} = account) do
+    if Pleroma.Config.get([:instance, :account_approval_required]) do
+      User.all_superusers()
+      |> Enum.filter(fn user -> not is_nil(user.email) end)
+      |> Enum.each(fn superuser ->
+        superuser
+        |> Pleroma.Emails.AdminEmail.new_unapproved_registration(account)
+        |> Pleroma.Emails.Mailer.deliver_async()
+      end)
+    end
+  end
+
   def password_reset(nickname_or_email) do
     with true <- is_binary(nickname_or_email),
          %User{local: true, email: email} = user when is_binary(email) <-
diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs
index 9082ae5a7..e24231e27 100644
--- a/test/emails/admin_email_test.exs
+++ b/test/emails/admin_email_test.exs
@@ -46,4 +46,24 @@ test "it works when the reporter is a remote user without email" do
     assert res.to == [{to_user.name, to_user.email}]
     assert res.from == {config[:name], config[:notify_email]}
   end
+
+  test "new unapproved registration email" do
+    config = Pleroma.Config.get(:instance)
+    to_user = insert(:user)
+    account = insert(:user, registration_reason: "Plz let me in")
+
+    res = AdminEmail.new_unapproved_registration(to_user, account)
+
+    account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
+
+    assert res.to == [{to_user.name, to_user.email}]
+    assert res.from == {config[:name], config[:notify_email]}
+    assert res.subject == "New account up for review on #{config[:name]} (@#{account.nickname})"
+
+    assert res.html_body == """
+           <p>New account for review: <a href="#{account_url}">@#{account.nickname}</a></p>
+           <blockquote>Plz let me in</blockquote>
+           <a href="http://localhost:4001/pleroma/admin">Visit AdminFE</a>
+           """
+  end
 end
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 368533292..df9d59f6a 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
   use Pleroma.DataCase
+  import Pleroma.Factory
   alias Pleroma.Repo
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
@@ -85,6 +86,42 @@ test "it sends confirmation email if :account_activation_required is specified i
     )
   end
 
+  test "it sends an admin email if :account_approval_required is specified in instance config" do
+    admin = insert(:user, is_admin: true)
+    setting = Pleroma.Config.get([:instance, :account_approval_required])
+
+    unless setting do
+      Pleroma.Config.put([:instance, :account_approval_required], true)
+      on_exit(fn -> Pleroma.Config.put([:instance, :account_approval_required], setting) end)
+    end
+
+    data = %{
+      :username => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :bio => "",
+      :password => "bear",
+      :confirm => "bear",
+      :reason => "I love anime"
+    }
+
+    {:ok, user} = TwitterAPI.register_user(data)
+    ObanHelpers.perform_all()
+
+    assert user.approval_pending
+
+    email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user)
+
+    notify_email = Pleroma.Config.get([:instance, :notify_email])
+    instance_name = Pleroma.Config.get([:instance, :name])
+
+    Swoosh.TestAssertions.assert_email_sent(
+      from: {instance_name, notify_email},
+      to: {admin.name, admin.email},
+      html_body: email.html_body
+    )
+  end
+
   test "it registers a new user and parses mentions in the bio" do
     data1 = %{
       :username => "john",

From 9ce95fa68f933c911c10e640b9a32ddae8a631c9 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Wed, 15 Jul 2020 17:04:30 -0500
Subject: [PATCH 14/24] Use `approval_required` in /api/v1/instance

---
 lib/pleroma/web/mastodon_api/views/instance_view.ex            | 2 +-
 test/web/mastodon_api/controllers/instance_controller_test.exs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 243067a73..5389d63cd 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -26,6 +26,7 @@ def render("show.json", _) do
       thumbnail: Keyword.get(instance, :instance_thumbnail),
       languages: ["en"],
       registrations: Keyword.get(instance, :registrations_open),
+      approval_required: Keyword.get(instance, :account_approval_required),
       # Extra (not present in Mastodon):
       max_toot_chars: Keyword.get(instance, :limit),
       poll_limits: Keyword.get(instance, :poll_limits),
@@ -39,7 +40,6 @@ def render("show.json", _) do
       pleroma: %{
         metadata: %{
           account_activation_required: Keyword.get(instance, :account_activation_required),
-          account_approval_required: Keyword.get(instance, :account_approval_required),
           features: features(),
           federation: federation(),
           fields_limits: fields_limits()
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
index 8a4183283..6a9ccd979 100644
--- a/test/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -27,6 +27,7 @@ test "get instance information", %{conn: conn} do
              "thumbnail" => _,
              "languages" => _,
              "registrations" => _,
+             "approval_required" => _,
              "poll_limits" => _,
              "upload_limit" => _,
              "avatar_upload_limit" => _,
@@ -38,7 +39,6 @@ test "get instance information", %{conn: conn} do
            } = result
 
     assert result["pleroma"]["metadata"]["account_activation_required"] != nil
-    assert result["pleroma"]["metadata"]["account_approval_required"] != nil
     assert result["pleroma"]["metadata"]["features"]
     assert result["pleroma"]["metadata"]["federation"]
     assert result["pleroma"]["metadata"]["fields_limits"]

From 02cc42e72c5f7dde78c705c3cbc83d2c13fb7a71 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Wed, 15 Jul 2020 17:08:46 -0500
Subject: [PATCH 15/24] Squash User approval migrations

---
 ...s => 20200712234852_add_approval_fields_to_users.exs} | 3 ++-
 .../20200712234852_add_approval_pending_to_users.exs     | 9 ---------
 2 files changed, 2 insertions(+), 10 deletions(-)
 rename priv/repo/migrations/{20200714043918_add_registration_reason_to_users.exs => 20200712234852_add_approval_fields_to_users.exs} (55%)
 delete mode 100644 priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs

diff --git a/priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs b/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
similarity index 55%
rename from priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs
rename to priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
index fa02fded4..559640f01 100644
--- a/priv/repo/migrations/20200714043918_add_registration_reason_to_users.exs
+++ b/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
@@ -1,8 +1,9 @@
-defmodule Pleroma.Repo.Migrations.AddRegistrationReasonToUsers do
+defmodule Pleroma.Repo.Migrations.AddApprovalFieldsToUsers do
   use Ecto.Migration
 
   def change do
     alter table(:users) do
+      add(:approval_pending, :boolean)
       add(:registration_reason, :string)
     end
   end
diff --git a/priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs b/priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs
deleted file mode 100644
index f7eb8179b..000000000
--- a/priv/repo/migrations/20200712234852_add_approval_pending_to_users.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule Pleroma.Repo.Migrations.AddApprovalPendingToUsers do
-  use Ecto.Migration
-
-  def change do
-    alter table(:users) do
-      add(:approval_pending, :boolean)
-    end
-  end
-end

From 5e745567031e87ee0854dca8d10065449af27d9c Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Thu, 16 Jul 2020 20:25:53 -0500
Subject: [PATCH 16/24] Sanitize `reason` param in POST /api/v1/accounts

---
 lib/pleroma/web/twitter_api/twitter_api.ex | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 2294d9d0d..424a705dd 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
 
   alias Pleroma.Emails.Mailer
   alias Pleroma.Emails.UserEmail
+  alias Pleroma.HTML
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.UserInviteToken
@@ -19,7 +20,7 @@ def register_user(params, opts \\ []) do
       |> Map.put(:nickname, params[:username])
       |> Map.put(:name, Map.get(params, :fullname, params[:username]))
       |> Map.put(:password_confirmation, params[:password])
-      |> Map.put(:registration_reason, params[:reason])
+      |> Map.put(:registration_reason, HTML.strip_tags(params[:reason]))
 
     if Pleroma.Config.get([:instance, :registrations_open]) do
       create_user(params, opts)

From 57568437361dd14151e3aa0590c7d1da05141cf4 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Fri, 17 Jul 2020 12:19:41 -0500
Subject: [PATCH 17/24] Fully delete users with status :approval_pending

---
 lib/pleroma/user.ex | 13 +++++++------
 test/user_test.exs  | 11 +++++++++++
 2 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 8e2c9fbe2..23288d434 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1525,12 +1525,13 @@ defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate
   defp delete_or_deactivate(%User{local: true} = user) do
     status = account_status(user)
 
-    if status == :confirmation_pending do
-      delete_and_invalidate_cache(user)
-    else
-      user
-      |> change(%{deactivated: true, email: nil})
-      |> update_and_set_cache()
+    case status do
+      :confirmation_pending -> delete_and_invalidate_cache(user)
+      :approval_pending -> delete_and_invalidate_cache(user)
+      _ ->
+        user
+        |> change(%{deactivated: true, email: nil})
+        |> update_and_set_cache()
     end
   end
 
diff --git a/test/user_test.exs b/test/user_test.exs
index cd39e1623..57cc054af 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1314,6 +1314,17 @@ test "deactivates user when activation is not required", %{user: user} do
     end
   end
 
+  test "delete/1 when approval is pending deletes the user" do
+    user = insert(:user, approval_pending: true)
+    {:ok, user: user}
+
+    {:ok, job} = User.delete(user)
+    {:ok, _} = ObanHelpers.perform(job)
+
+    refute User.get_cached_by_id(user.id)
+    refute User.get_by_id(user.id)
+  end
+
   test "get_public_key_for_ap_id fetches a user that's not in the db" do
     assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
   end

From 15f8921b111bc38d0d9eb9ccd1fd09e41cdbc85e Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Fri, 17 Jul 2020 12:26:52 -0500
Subject: [PATCH 18/24] Test that unapproved users can never log in regardless
 of admin settings

---
 test/web/oauth/oauth_controller_test.exs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index ec5b78750..1200126b8 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -998,8 +998,7 @@ test "rejects token exchange for user with confirmation_pending set to true" do
              }
     end
 
-    test "rejects token exchange for valid credentials belonging to an unapproved user and approval is required" do
-      Pleroma.Config.put([:instance, :account_approval_required], true)
+    test "rejects token exchange for valid credentials belonging to an unapproved user" do
       password = "testpassword"
 
       user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true)

From 6f44a0ee84a8dca7a94a38b45493a444390f13ec Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Mon, 27 Jul 2020 15:13:34 -0500
Subject: [PATCH 19/24] Add configurable registration_reason limit

---
 config/config.exs                              |  1 +
 lib/pleroma/user.ex                            |  2 ++
 ...0712234852_add_approval_fields_to_users.exs |  2 +-
 test/user_test.exs                             | 18 +++++++++++++++++-
 4 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/config/config.exs b/config/config.exs
index d8bf921bb..1dc196a6b 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -238,6 +238,7 @@
   max_remote_account_fields: 20,
   account_field_name_length: 512,
   account_field_value_length: 2048,
+  registration_reason_length: 500,
   external_user_synchronization: true,
   extended_nickname_format: true,
   cleanup_attachments: false,
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index a78123fe4..913b6afd1 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -641,6 +641,7 @@ def force_password_reset(user), do: update_password_reset_pending(user, true)
   def register_changeset(struct, params \\ %{}, opts \\ []) do
     bio_limit = Config.get([:instance, :user_bio_length], 5000)
     name_limit = Config.get([:instance, :user_name_length], 100)
+    reason_limit = Config.get([:instance, :registration_reason_length], 500)
     params = Map.put_new(params, :accepts_chat_messages, true)
 
     need_confirmation? =
@@ -681,6 +682,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
     |> validate_format(:email, @email_regex)
     |> validate_length(:bio, max: bio_limit)
     |> validate_length(:name, min: 1, max: name_limit)
+    |> validate_length(:registration_reason, max: reason_limit)
     |> maybe_validate_required_email(opts[:external])
     |> put_password_hash
     |> put_ap_id()
diff --git a/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs b/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
index 559640f01..43f741a5b 100644
--- a/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
+++ b/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.AddApprovalFieldsToUsers do
   def change do
     alter table(:users) do
       add(:approval_pending, :boolean)
-      add(:registration_reason, :string)
+      add(:registration_reason, :text)
     end
   end
 end
diff --git a/test/user_test.exs b/test/user_test.exs
index 5da86bcec..5bf677666 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -550,7 +550,8 @@ test "it creates confirmed user if :confirmed option is given" do
       nickname: "nick",
       password: "test",
       password_confirmation: "test",
-      email: "email@example.com"
+      email: "email@example.com",
+      registration_reason: "I'm a cool guy :)"
     }
     setup do: clear_config([:instance, :account_approval_required], true)
 
@@ -561,6 +562,21 @@ test "it creates unapproved user" do
       {:ok, user} = Repo.insert(changeset)
 
       assert user.approval_pending
+      assert user.registration_reason == "I'm a cool guy :)"
+    end
+
+    test "it restricts length of registration reason" do
+      reason_limit = Pleroma.Config.get([:instance, :registration_reason_length])
+
+      assert is_integer(reason_limit)
+
+      params =
+        @full_user_data
+        |> Map.put(:registration_reason, "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in.")
+
+      changeset = User.register_changeset(%User{}, params)
+
+      refute changeset.valid?
     end
   end
 

From 520dce857e4a6d3cdce275c46b3ad7b46a582c76 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Mon, 27 Jul 2020 15:24:20 -0500
Subject: [PATCH 20/24] Add :registration_reason_length to description.exs

---
 config/description.exs           | 8 ++++++++
 docs/configuration/cheatsheet.md | 1 +
 2 files changed, 9 insertions(+)

diff --git a/config/description.exs b/config/description.exs
index 509effbc3..df9f256ef 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -879,6 +879,14 @@
           2048
         ]
       },
+      %{
+        key: :registration_reason_length,
+        type: :integer,
+        description: "Maximum registration reason length. Default: 500.",
+        suggestions: [
+          500
+        ]
+      },
       %{
         key: :external_user_synchronization,
         type: :boolean,
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 5cf073293..c89df24cc 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -59,6 +59,7 @@ To add configuration to your config file, you can copy it from the base config.
 * `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`).
 * `account_field_name_length`: An account field name maximum length (default: `512`).
 * `account_field_value_length`: An account field value maximum length (default: `2048`).
+* `registration_reason_length`: Maximum registration reason length (default: `500`).
 * `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
 * `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
 

From f43518eb7433a6c50d635d6536c3fbe3a37ea82b Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Mon, 27 Jul 2020 19:19:14 -0500
Subject: [PATCH 21/24] Lint, fix test

---
 lib/pleroma/user.ex                                       | 8 ++++++--
 test/user_test.exs                                        | 5 ++++-
 .../mastodon_api/controllers/account_controller_test.exs  | 4 ++--
 3 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 913b6afd1..dcf6ebee2 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1553,8 +1553,12 @@ defp delete_or_deactivate(%User{local: true} = user) do
     status = account_status(user)
 
     case status do
-      :confirmation_pending -> delete_and_invalidate_cache(user)
-      :approval_pending -> delete_and_invalidate_cache(user)
+      :confirmation_pending ->
+        delete_and_invalidate_cache(user)
+
+      :approval_pending ->
+        delete_and_invalidate_cache(user)
+
       _ ->
         user
         |> change(%{deactivated: true, email: nil})
diff --git a/test/user_test.exs b/test/user_test.exs
index 5bf677666..624baf8ad 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -572,7 +572,10 @@ test "it restricts length of registration reason" do
 
       params =
         @full_user_data
-        |> Map.put(:registration_reason, "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in.")
+        |> Map.put(
+          :registration_reason,
+          "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in."
+        )
 
       changeset = User.register_changeset(%User{}, params)
 
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index e6b283aab..1ba5bc964 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -1017,7 +1017,7 @@ test "Account registration via app with account_approval_required", %{conn: conn
           password: "PlzDontHackLain",
           bio: "Test Bio",
           agreement: true,
-          reason: "I'm a cool dude, bro"
+          reason: "I am a cool dude, bro"
         })
 
       %{
@@ -1035,7 +1035,7 @@ test "Account registration via app with account_approval_required", %{conn: conn
       assert token_from_db.user.confirmation_pending
       assert token_from_db.user.approval_pending
 
-      assert token_from_db.user.registration_reason == "I'm a cool dude, bro"
+      assert token_from_db.user.registration_reason == "I am a cool dude, bro"
     end
 
     test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do

From f688c8df82b955b50552b3198ddc153a716451c2 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Mon, 27 Jul 2020 20:36:31 -0500
Subject: [PATCH 22/24] Fix User.registration_reason HTML sanitizing issues

---
 lib/pleroma/emails/admin_email.ex                             | 3 ++-
 lib/pleroma/web/twitter_api/twitter_api.ex                    | 3 +--
 test/web/mastodon_api/controllers/account_controller_test.exs | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index fae7faf00..c27ad1065 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Emails.AdminEmail do
   import Swoosh.Email
 
   alias Pleroma.Config
+  alias Pleroma.HTML
   alias Pleroma.Web.Router.Helpers
 
   defp instance_config, do: Config.get(:instance)
@@ -86,7 +87,7 @@ def report(to, reporter, account, statuses, comment) do
   def new_unapproved_registration(to, account) do
     html_body = """
     <p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
-    <blockquote>#{account.registration_reason}</blockquote>
+    <blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
     <a href="#{Pleroma.Web.base_url()}/pleroma/admin">Visit AdminFE</a>
     """
 
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 424a705dd..2294d9d0d 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
 
   alias Pleroma.Emails.Mailer
   alias Pleroma.Emails.UserEmail
-  alias Pleroma.HTML
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.UserInviteToken
@@ -20,7 +19,7 @@ def register_user(params, opts \\ []) do
       |> Map.put(:nickname, params[:username])
       |> Map.put(:name, Map.get(params, :fullname, params[:username]))
       |> Map.put(:password_confirmation, params[:password])
-      |> Map.put(:registration_reason, HTML.strip_tags(params[:reason]))
+      |> Map.put(:registration_reason, params[:reason])
 
     if Pleroma.Config.get([:instance, :registrations_open]) do
       create_user(params, opts)
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index 1ba5bc964..e6b283aab 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -1017,7 +1017,7 @@ test "Account registration via app with account_approval_required", %{conn: conn
           password: "PlzDontHackLain",
           bio: "Test Bio",
           agreement: true,
-          reason: "I am a cool dude, bro"
+          reason: "I'm a cool dude, bro"
         })
 
       %{
@@ -1035,7 +1035,7 @@ test "Account registration via app with account_approval_required", %{conn: conn
       assert token_from_db.user.confirmation_pending
       assert token_from_db.user.approval_pending
 
-      assert token_from_db.user.registration_reason == "I am a cool dude, bro"
+      assert token_from_db.user.registration_reason == "I'm a cool dude, bro"
     end
 
     test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do

From edf8b6abfeba487406db756254a00524e3a9dce2 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 29 Jul 2020 10:53:08 +0200
Subject: [PATCH 23/24] EnsureRePrepended: Don't break on chat messages.

---
 .../web/activity_pub/mrf/ensure_re_prepended.ex        |  3 ++-
 test/web/activity_pub/mrf/ensure_re_prepended_test.exs | 10 ++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
index 2627a0007..3bf70b894 100644
--- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -27,7 +27,8 @@ def filter_by_summary(
 
   def filter_by_summary(_in_reply_to, child), do: child
 
-  def filter(%{"type" => "Create", "object" => child_object} = object) do
+  def filter(%{"type" => "Create", "object" => child_object} = object)
+      when is_map(child_object) do
     child =
       child_object["inReplyTo"]
       |> Object.normalize(child_object["inReplyTo"])
diff --git a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/web/activity_pub/mrf/ensure_re_prepended_test.exs
index 38ddec5bb..9a283f27d 100644
--- a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs
+++ b/test/web/activity_pub/mrf/ensure_re_prepended_test.exs
@@ -78,5 +78,15 @@ test "it skip if parent and child summary isn't equal" do
       assert {:ok, res} = EnsureRePrepended.filter(message)
       assert res == message
     end
+
+    test "it skips if the object is only a reference" do
+      message = %{
+        "type" => "Create",
+        "object" => "somereference"
+      }
+
+      assert {:ok, res} = EnsureRePrepended.filter(message)
+      assert res == message
+    end
   end
 end

From 2e27847573e91946e6777f9aa18b9cf9ccaf820d Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Wed, 29 Jul 2020 14:02:02 +0200
Subject: [PATCH 24/24] feed/user_controller: Return 404 when the user is
 remote

---
 lib/pleroma/web/feed/user_controller.ex |  3 ++-
 test/web/feed/user_controller_test.exs  | 11 +++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index d56f43818..9cd334a33 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -47,7 +47,7 @@ def feed(conn, %{"nickname" => nickname} = params) do
         "atom"
       end
 
-    with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
+    with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
       activities =
         %{
           type: ["Create"],
@@ -71,6 +71,7 @@ def errors(conn, {:error, :not_found}) do
     render_error(conn, :not_found, "Not found")
   end
 
+  def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found})
   def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
 
   def errors(conn, _) do
diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs
index fa2ed1ea5..0d2a61967 100644
--- a/test/web/feed/user_controller_test.exs
+++ b/test/web/feed/user_controller_test.exs
@@ -181,6 +181,17 @@ test "returns feed with public and unlisted activities", %{conn: conn} do
 
       assert activity_titles == ['public', 'unlisted']
     end
+
+    test "returns 404 when the user is remote", %{conn: conn} do
+      user = insert(:user, local: false)
+
+      {:ok, _} = CommonAPI.post(user, %{status: "test"})
+
+      assert conn
+             |> put_req_header("accept", "application/atom+xml")
+             |> get(user_feed_path(conn, :feed, user.nickname))
+             |> response(404)
+    end
   end
 
   # Note: see ActivityPubControllerTest for JSON format tests