Merge remote-tracking branch 'upstream/develop' into by-approval
authorAlex Gleason <alex@alexgleason.me>
Sun, 26 Jul 2020 20:46:14 +0000 (15:46 -0500)
committerAlex Gleason <alex@alexgleason.me>
Sun, 26 Jul 2020 20:46:14 +0000 (15:46 -0500)
12 files changed:
1  2 
CHANGELOG.md
config/config.exs
config/description.exs
docs/configuration/cheatsheet.md
lib/pleroma/user.ex
lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/instance_view.ex
test/user_test.exs
test/web/admin_api/controllers/admin_api_controller_test.exs
test/web/mastodon_api/controllers/account_controller_test.exs
test/web/twitter_api/twitter_api_test.exs

diff --combined CHANGELOG.md
index a3689fc6bc320650307dd5d17c095fb186fb1eb4,9389df4bdfafea2cb78af0abc483d58aef859d12..c52d2eef5c67587d6f0f0beeada7697cf1e74c5d
@@@ -7,6 -7,7 +7,7 @@@ The format is based on [Keep a Changelo
  
  ### Changed
  - **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
+ - **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated.
  - In Conversations, return only direct messages as `last_status`
  - Using the `only_media` filter on timelines will now exclude reblog media
  - MFR policy to set global expiration for all local Create activities
  - Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
  - Mastodon API: On deletion, returns the original post text.
  - Mastodon API: Add `pleroma.unread_count` to the Marker entity.
+ - **Breaking:** Notification Settings API for suppressing notifications
+   has been simplified down to `block_from_strangers`.
+ - **Breaking:** Notification Settings API option for hiding push notification
+   contents has been renamed to `hide_notification_contents`
+ - Mastodon API: Added `pleroma.metadata.post_formats` to /api/v1/instance
+ - Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone`
  </details>
  
  <details>
  - 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>
- - Mastodon API: Add pleroma.parents_visible field to statuses.
+ - Mastodon API: Add pleroma.parent_visible field to statuses.
  - Mastodon API: Extended `/api/v1/instance`.
  - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
  - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
@@@ -88,6 -95,7 +96,7 @@@
  - Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
  - Fix CSP policy generation to include remote Captcha services
  - Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance.
+ - Emoji Packs could not be listed when instance was set to `public: false`
  
  ## [Unreleased (patch)]
  
  - Follow request notifications
  <details>
    <summary>API Changes</summary>
  - Admin API: `GET /api/pleroma/admin/need_reboot`.
  </details>
  
  - **Breaking**: Using third party engines for user recommendation
  <details>
    <summary>API Changes</summary>
  - **Breaking**: AdminAPI: migrate_from_db endpoint
  </details>
  
diff --combined config/config.exs
index 791740663db1b0b87698812dc7a9dae499ac8eea,406bf2a9bd367d1ba83f1bc3d60fd36968096d55..c5465ae2c062e84d5b1d1b9bad0f3d8e8f8169af
@@@ -172,7 -172,7 +172,7 @@@ config :mime, :types, %
    "application/ld+json" => ["activity+json"]
  }
  
- config :tesla, adapter: Tesla.Adapter.Hackney
+ config :tesla, adapter: Tesla.Adapter.Gun
  
  # Configures http settings, upstream proxy etc.
  config :pleroma, :http,
@@@ -205,7 -205,6 +205,7 @@@ config :pleroma, :instance
    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,
@@@ -513,6 -512,7 +513,7 @@@ config :pleroma, Oban
      attachments_cleanup: 5,
      new_users_digest: 1
    ],
+   plugins: [Oban.Plugins.Pruner],
    crontab: [
      {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
      {"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
@@@ -527,16 -527,14 +528,14 @@@ config :pleroma, :workers
      federator_outgoing: 5
    ]
  
- config :auto_linker,
-   opts: [
-     extra: true,
-     # TODO: Set to :no_scheme when it works properly
-     validate_tld: true,
-     class: false,
-     strip_prefix: false,
-     new_window: false,
-     rel: "ugc"
-   ]
+ config :pleroma, Pleroma.Formatter,
+   class: false,
+   rel: "ugc",
+   new_window: false,
+   truncate: false,
+   strip_prefix: false,
+   extra: true,
+   validate_tld: :no_scheme
  
  config :pleroma, :ldap,
    enabled: System.get_env("LDAP_ENABLED") == "true",
@@@ -648,32 -646,30 +647,30 @@@ config :pleroma, Pleroma.Repo
    prepare: :unnamed
  
  config :pleroma, :connections_pool,
-   checkin_timeout: 250,
+   reclaim_multiplier: 0.1,
+   connection_acquisition_wait: 250,
+   connection_acquisition_retries: 5,
    max_connections: 250,
-   retry: 1,
-   retry_timeout: 1000,
+   max_idle_time: 30_000,
+   retry0,
    await_up_timeout: 5_000
  
  config :pleroma, :pools,
    federation: [
      size: 50,
-     max_overflow: 10,
-     timeout: 150_000
+     max_waiting: 10
    ],
    media: [
      size: 50,
-     max_overflow: 10,
-     timeout: 150_000
+     max_waiting: 10
    ],
    upload: [
      size: 25,
-     max_overflow: 5,
-     timeout: 300_000
+     max_waiting: 5
    ],
    default: [
      size: 10,
-     max_overflow: 2,
-     timeout: 10_000
+     max_waiting: 2
    ]
  
  config :pleroma, :hackney_pools,
diff --combined config/description.exs
index 9aca26fbffbfea7fd3fa799459e384554d1259a4,e4850218e1b5ff11b1422f78f3d4e81fab6d71e3..f506affd4957c31d99691709d76d14d9f2238130
@@@ -661,11 -661,6 +661,11 @@@ config :pleroma, :config_description, 
          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,
      group: :pleroma,
      key: :mrf_simple,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
      label: "MRF Simple",
      type: :group,
      description: "Simple ingress policies",
      group: :pleroma,
      key: :mrf_activity_expiration,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
      label: "MRF Activity Expiration Policy",
      type: :group,
      description: "Adds automatic expiration to all local activities",
      group: :pleroma,
      key: :mrf_subchain,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
      label: "MRF Subchain",
      type: :group,
      description:
      group: :pleroma,
      key: :mrf_rejectnonpublic,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
      description: "RejectNonPublic drops posts with non-public visibility settings.",
      label: "MRF Reject Non Public",
      type: :group,
      group: :pleroma,
      key: :mrf_hellthread,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
      label: "MRF Hellthread",
      type: :group,
      description: "Block messages with excessive user mentions",
      group: :pleroma,
      key: :mrf_keyword,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
      label: "MRF Keyword",
      type: :group,
      description: "Reject or Word-Replace messages with a keyword or regex",
      group: :pleroma,
      key: :mrf_mention,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
      label: "MRF Mention",
      type: :group,
      description: "Block messages which mention a specific user",
      group: :pleroma,
      key: :mrf_vocabulary,
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
      label: "MRF Vocabulary",
      type: :group,
      description: "Filter messages which belong to certain activity vocabularies",
    # %{
    #   group: :pleroma,
    #   key: :mrf_user_allowlist,
+   #   tab: :mrf,
+   #   related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
    #   type: :map,
    #   description:
    #     "The keys in this section are the domain names that the policy should apply to." <>
      ]
    },
    %{
-     group: :auto_linker,
-     key: :opts,
+     group: :pleroma,
+     key: Pleroma.Formatter,
      label: "Auto Linker",
      type: :group,
-     description: "Configuration for the auto_linker library",
+     description:
+       "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.",
      children: [
        %{
          key: :class,
        %{
          key: :new_window,
          type: :boolean,
-         description: "Link URLs will open in new window/tab"
+         description: "Link URLs will open in a new window/tab."
        },
        %{
          key: :truncate,
          type: [:integer, false],
          description:
-           "Set to a number to truncate URLs longer then the number. Truncated URLs will end in `..`",
+           "Set to a number to truncate URLs longer than the number. Truncated URLs will end in `...`",
          suggestions: [15, false]
        },
        %{
          key: :strip_prefix,
          type: :boolean,
-         description: "Strip the scheme prefix"
+         description: "Strip the scheme prefix."
        },
        %{
          key: :extra,
          type: :boolean,
          description: "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
+       },
+       %{
+         key: :validate_tld,
+         type: [:atom, :boolean],
+         description:
+           "Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for URLs without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)",
+         suggestions: [:no_scheme, true]
        }
      ]
    },
    },
    %{
      group: :pleroma,
-     tab: :mrf,
      key: :mrf_normalize_markup,
+     tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
      label: "MRF Normalize Markup",
      description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
      type: :group,
    %{
      group: :pleroma,
      key: :mrf_object_age,
-     label: "MRF Object Age",
      tab: :mrf,
+     related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
+     label: "MRF Object Age",
      type: :group,
      description:
        "Rejects or delists posts based on their timestamp deviance from your server's clock.",
      description: "Advanced settings for `gun` connections pool",
      children: [
        %{
-         key: :checkin_timeout,
+         key: :connection_acquisition_wait,
          type: :integer,
-         description: "Timeout to checkin connection from pool. Default: 250ms.",
-         suggestions: [250]
-       },
-       %{
-         key: :max_connections,
-         type: :integer,
-         description: "Maximum number of connections in the pool. Default: 250 connections.",
+         description:
+           "Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries. Default: 250ms.",
          suggestions: [250]
        },
        %{
-         key: :retry,
+         key: :connection_acquisition_retries,
          type: :integer,
          description:
-           "Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.",
-         suggestions: [1]
+           "Number of attempts to acquire the connection from the pool if it is overloaded. Default: 5",
+         suggestions: [5]
        },
        %{
-         key: :retry_timeout,
+         key: :max_connections,
          type: :integer,
-         description:
-           "Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.",
-         suggestions: [1000]
+         description: "Maximum number of connections in the pool. Default: 250 connections.",
+         suggestions: [250]
        },
        %{
          key: :await_up_timeout,
          type: :integer,
          description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
          suggestions: [5000]
+       },
+       %{
+         key: :reclaim_multiplier,
+         type: :integer,
+         description:
+           "Multiplier for the number of idle connection to be reclaimed if the pool is full. For example if the pool maxes out at 250 connections and this setting is set to 0.3, the pool will reclaim at most 75 idle connections if it's overloaded. Default: 0.1",
+         suggestions: [0.1]
        }
      ]
    },
      key: :pools,
      type: :group,
      description: "Advanced settings for `gun` workers pools",
-     children: [
-       %{
-         key: :federation,
-         type: :keyword,
-         description: "Settings for federation pool.",
-         children: [
-           %{
-             key: :size,
-             type: :integer,
-             description: "Number workers in the pool.",
-             suggestions: [50]
-           },
-           %{
-             key: :max_overflow,
-             type: :integer,
-             description: "Number of additional workers if pool is under load.",
-             suggestions: [10]
-           },
-           %{
-             key: :timeout,
-             type: :integer,
-             description: "Timeout while `gun` will wait for response.",
-             suggestions: [150_000]
-           }
-         ]
-       },
-       %{
-         key: :media,
-         type: :keyword,
-         description: "Settings for media pool.",
-         children: [
-           %{
-             key: :size,
-             type: :integer,
-             description: "Number workers in the pool.",
-             suggestions: [50]
-           },
-           %{
-             key: :max_overflow,
-             type: :integer,
-             description: "Number of additional workers if pool is under load.",
-             suggestions: [10]
-           },
-           %{
-             key: :timeout,
-             type: :integer,
-             description: "Timeout while `gun` will wait for response.",
-             suggestions: [150_000]
-           }
-         ]
-       },
-       %{
-         key: :upload,
-         type: :keyword,
-         description: "Settings for upload pool.",
-         children: [
-           %{
-             key: :size,
-             type: :integer,
-             description: "Number workers in the pool.",
-             suggestions: [25]
-           },
-           %{
-             key: :max_overflow,
-             type: :integer,
-             description: "Number of additional workers if pool is under load.",
-             suggestions: [5]
-           },
-           %{
-             key: :timeout,
-             type: :integer,
-             description: "Timeout while `gun` will wait for response.",
-             suggestions: [300_000]
-           }
-         ]
-       },
-       %{
-         key: :default,
-         type: :keyword,
-         description: "Settings for default pool.",
-         children: [
-           %{
-             key: :size,
-             type: :integer,
-             description: "Number workers in the pool.",
-             suggestions: [10]
-           },
-           %{
-             key: :max_overflow,
-             type: :integer,
-             description: "Number of additional workers if pool is under load.",
-             suggestions: [2]
-           },
-           %{
-             key: :timeout,
-             type: :integer,
-             description: "Timeout while `gun` will wait for response.",
-             suggestions: [10_000]
-           }
-         ]
-       }
-     ]
+     children:
+       Enum.map([:federation, :media, :upload, :default], fn pool_name ->
+         %{
+           key: pool_name,
+           type: :keyword,
+           description: "Settings for #{pool_name} pool.",
+           children: [
+             %{
+               key: :size,
+               type: :integer,
+               description: "Maximum number of concurrent requests in the pool.",
+               suggestions: [50]
+             },
+             %{
+               key: :max_waiting,
+               type: :integer,
+               description:
+                 "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made",
+               suggestions: [10]
+             }
+           ]
+         }
+       end)
    },
    %{
      group: :pleroma,
index 78270133ff5e658dd01df0e096b8e16674d176bc,042ad30c97caf0c223855e23029de3c1beebb45c..32563a2888c1414711508c018bd4a37b18691d34
@@@ -33,7 -33,6 +33,7 @@@ To add configuration to your config fil
  * `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.
@@@ -449,36 -448,32 +449,32 @@@ For each pool, the options are
  
  *For `gun` adapter*
  
Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools.
Settings for HTTP connection pool.
  
- For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000.
- It will increase memory usage, but federation would work faster.
- * `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms.
- * `:max_connections` - maximum number of connections in the pool. Default: 250 connections.
- * `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.
- * `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.
- * `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms.
+ * `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries.
+ * `connection_acquisition_retries` - Number of attempts to acquire the connection from the pool if it is overloaded. Each attempt is timed `:connection_acquisition_wait` apart.
+ * `:max_connections` - Maximum number of connections in the pool.
+ * `:await_up_timeout` - Timeout to connect to the host.
+ * `:reclaim_multiplier` - Multiplied by `:max_connections` this will be the maximum number of idle connections that will be reclaimed in case the pool is overloaded.
  
  ### :pools
  
  *For `gun` adapter*
  
Advanced settings for workers pools.
Settings for request pools. These pools are limited on top of `:connections_pool`.
  
  There are four pools used:
  
- * `:federation` for the federation jobs.
-   You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
- * `:media` for rich media, media proxy
- * `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
- * `:default` for other requests
+ * `:federation` for the federation jobs. You may want this pool's max_connections to be at least equal to the number of federator jobs + retry queue jobs.
+ * `:media` - for rich media, media proxy.
+ * `:upload` - for proxying media when a remote uploader is used and `proxy_remote: true`.
+ * `:default` - for other requests.
  
  For each pool, the options are:
  
- * `:size` - how much workers the pool can hold
+ * `:size` - limit to how much requests can be concurrently executed.
  * `:timeout` - timeout while `gun` will wait for response
- * `:max_overflow` - additional workers if pool is under load
+ * `:max_waiting` - limit to how much requests can be waiting for others to finish, after this is reached, subsequent requests will be dropped.
  
  ## Captcha
  
@@@ -939,30 -934,29 +935,29 @@@ Configure OAuth 2 provider capabilities
  ### :uri_schemes
  * `valid_schemes`: List of the scheme part that is considered valid to be an URL.
  
- ### :auto_linker
+ ### Pleroma.Formatter
  
- Configuration for the `auto_linker` library:
+ Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.
  
- * `class: "auto-linker"` - specify the class to be added to the generated link. false to clear.
- * `rel: "noopener noreferrer"` - override the rel attribute. false to clear.
- * `new_window: true` - set to false to remove `target='_blank'` attribute.
- * `scheme: false` - Set to true to link urls with schema `http://google.com`.
- * `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`.
- * `strip_prefix: true` - Strip the scheme prefix.
- * `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.).
+ * `class` - specify the class to be added to the generated link (default: `false`)
+ * `rel` - specify the rel attribute (default: `ugc`)
+ * `new_window` - adds `target="_blank"` attribute (default: `false`)
+ * `truncate` - Set to a number to truncate URLs longer then the number. Truncated URLs will end in `...` (default: `false`)
+ * `strip_prefix` - Strip the scheme prefix (default: `false`)
+ * `extra` - link URLs with rarely used schemes (magnet, ipfs, irc, etc.) (default: `true`)
+ * `validate_tld` - Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for urls without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't) (default: `:no_scheme`)
  
  Example:
  
  ```elixir
- config :auto_linker,
-   opts: [
-     scheme: true,
-     extra: true,
-     class: false,
-     strip_prefix: false,
-     new_window: false,
-     rel: "ugc"
-   ]
+ config :pleroma, Pleroma.Formatter,
+   class: false,
+   rel: "ugc",
+   new_window: false,
+   truncate: false,
+   strip_prefix: false,
+   extra: true,
+   validate_tld: :no_scheme
  ```
  
  ## Custom Runtime Modules (`:modules`)
diff --combined lib/pleroma/user.ex
index 23288d434307fa744ffa43313a793aa2ddf9bc2d,714ec9a4bac3dd74964f13a5212bfe246bcbe6e5..0138775bc989b8aaeb3b9842591b0454322619e9
@@@ -42,12 -42,7 +42,12 @@@ defmodule Pleroma.User d
    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
      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(:registration_reason, :string, default: nil)
      field(:confirmation_token, :string, default: nil)
      field(:default_scope, :string, default: "public")
      field(:domain_blocks, {:array, :string}, default: [])
    @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
          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,
        :password,
        :password_confirmation,
        :emoji,
 -      :accepts_chat_messages
 +      :accepts_chat_messages,
 +      :registration_reason
      ])
      |> validate_required([:name, :nickname, :password, :password_confirmation])
      |> validate_confirmation(:password)
      end
    end
  
-   def try_send_confirmation_email(%User{} = user) do
-     if user.confirmation_pending &&
-          Config.get([:instance, :account_activation_required]) do
-       user
-       |> Pleroma.Emails.UserEmail.account_confirmation_email()
-       |> Pleroma.Emails.Mailer.deliver_async()
+   @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
+   def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
+     if Config.get([:instance, :account_activation_required]) do
+       send_confirmation_email(user)
        {:ok, :enqueued}
      else
        {:ok, :noop}
      end
    end
  
-   def try_send_confirmation_email(users) do
-     Enum.each(users, &try_send_confirmation_email/1)
+   def try_send_confirmation_email(_), do: {:ok, :noop}
+   @spec send_confirmation_email(Uset.t()) :: User.t()
+   def send_confirmation_email(%User{} = user) do
+     user
+     |> Pleroma.Emails.UserEmail.account_confirmation_email()
+     |> Pleroma.Emails.Mailer.deliver_async()
+     user
    end
  
    def needs_update?(%User{local: true}), do: false
      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}, [])
    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
  
      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)
index 53f71fcbf613b40f085bc2d7081c40bcd1300dbf,5101e28d62ed83ba83e5daf8b14f243e3f2763a0..aa2af1ab5495a4715f79d11c0a8a2bffc1ae01e5
@@@ -44,7 -44,6 +44,7 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
             :user_toggle_activation,
             :user_activate,
             :user_deactivate,
 +           :user_approve,
             :tag_users,
             :untag_users,
             :right_add,
      |> 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(%{
      with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
        json(
          conn,
-         AccountView.render("index.json", users: users, count: count, page_size: page_size)
+         AccountView.render("index.json",
+           users: users,
+           count: count,
+           page_size: page_size
+         )
        )
      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: %{}
    end
  
    def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
-     users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+     users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
  
      User.toggle_confirmation(users)
  
-     ModerationLog.insert_log(%{
-       actor: admin,
-       subject: users,
-       action: "confirm_email"
-     })
+     ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
  
      json(conn, "")
    end
  
    def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
-     users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
-     User.try_send_confirmation_email(users)
+     users =
+       Enum.map(nicknames, fn nickname ->
+         nickname
+         |> User.get_cached_by_nickname()
+         |> User.send_confirmation_email()
+       end)
  
-     ModerationLog.insert_log(%{
-       actor: admin,
-       subject: users,
-       action: "resend_confirmation_email"
-     })
+     ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
  
      json(conn, "")
    end
index bdab04ad2200098f4ad3f6c44c718b404bf9fcc7,88fbb53159a24abafc911fdd17665f97f808bd17..333e72e42ab083743378d14b969dfa5fee810183
@@@ -77,9 -77,7 +77,9 @@@ defmodule Pleroma.Web.AdminAPI.AccountV
        "roles" => User.roles(user),
        "tags" => user.tags || [],
        "confirmation_pending" => user.confirmation_pending,
 -      "url" => user.uri || user.ap_id
 +      "approval_pending" => user.approval_pending,
 +      "url" => user.uri || user.ap_id,
 +      "registration_reason" => user.registration_reason
      }
    end
  
    end
  
    def merge_account_views(%User{} = user) do
-     MastodonAPI.AccountView.render("show.json", %{user: user})
+     MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
      |> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
    end
  
index 5389d63cd61feea2c9b4a1ca7657cef815d8db8d,cd3bc7f00c525f5413a6be3e38613dc44e3b9167..ea2d3aa9c9603cb26ee30b4bb7c9bb6c42fd8a3c
@@@ -26,7 -26,6 +26,7 @@@ defmodule Pleroma.Web.MastodonAPI.Insta
        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),
@@@ -42,7 -41,8 +42,8 @@@
            account_activation_required: Keyword.get(instance, :account_activation_required),
            features: features(),
            federation: federation(),
-           fields_limits: fields_limits()
+           fields_limits: fields_limits(),
+           post_formats: Config.get([:instance, :allowed_post_formats])
          },
          vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
        }
diff --combined test/user_test.exs
index 57cc054af9d734c6d1a512b7838b89a4567a9dda,21c03b470fab5ff4c1e098e10610ff5ecf1f54bc..8cdf3a89118448760ae811ebedab6b581fddf559
@@@ -17,6 -17,7 +17,7 @@@ defmodule Pleroma.UserTest d
  
    import Pleroma.Factory
    import ExUnit.CaptureLog
+   import Swoosh.TestAssertions
  
    setup_all do
      Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
        password_confirmation: "test",
        email: "email@example.com"
      }
      setup do: clear_config([:instance, :autofollowed_nicknames])
      setup do: clear_config([:instance, :welcome_message])
      setup do: clear_config([:instance, :welcome_user_nickname])
+     setup do: clear_config([:instance, :account_activation_required])
  
      test "it autofollows accounts that are set for it" do
        user = insert(:user)
        assert activity.actor == welcome_user.ap_id
      end
  
-     setup do: clear_config([:instance, :account_activation_required])
+     test "it sends a confirm email" do
+       Pleroma.Config.put([:instance, :account_activation_required], true)
+       cng = User.register_changeset(%User{}, @full_user_data)
+       {:ok, registered_user} = User.register(cng)
+       ObanHelpers.perform_all()
+       assert_email_sent(Pleroma.Emails.UserEmail.account_confirmation_email(registered_user))
+     end
  
      test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do
        Pleroma.Config.put([:instance, :account_activation_required], true)
      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)
      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()
      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
        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, 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
  
    describe "superuser?/1" do
index 85529446f7edae0097b721173b162e276892c52a,6082441ee5e8e032494da8b8810d08ca2be22e0e..b5d5bd8c70e837bc6ca09825174a7c24cdf0ba5e
@@@ -9,6 -9,7 +9,7 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    import ExUnit.CaptureLog
    import Mock
    import Pleroma.Factory
+   import Swoosh.TestAssertions
  
    alias Pleroma.Activity
    alias Pleroma.Config
          "avatar" => User.avatar_url(user) |> MediaProxy.url(),
          "display_name" => HTML.strip_tags(user.name || user.nickname),
          "confirmation_pending" => false,
 -        "url" => user.ap_id
 +        "approval_pending" => false,
 +        "url" => user.ap_id,
 +        "registration_reason" => nil
        }
  
        assert expected == json_response(conn, 200)
    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"])
 +      user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude")
 +
        conn = get(conn, "/api/pleroma/admin/users?page=1")
  
        users =
              "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(admin.name || admin.nickname),
              "confirmation_pending" => false,
 -            "url" => admin.ap_id
 +            "approval_pending" => false,
 +            "url" => admin.ap_id,
 +            "registration_reason" => nil
            },
            %{
              "deactivated" => user.deactivated,
              "avatar" => User.avatar_url(user) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(user.name || user.nickname),
              "confirmation_pending" => false,
 -            "url" => user.ap_id
 +            "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
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user2.name || user2.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user2.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user2.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
              "avatar" => User.avatar_url(user) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(user.name || user.nickname),
              "confirmation_pending" => false,
 -            "url" => user.ap_id
 +            "approval_pending" => false,
 +            "url" => user.ap_id,
 +            "registration_reason" => nil
            },
            %{
              "deactivated" => admin.deactivated,
              "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(admin.name || admin.nickname),
              "confirmation_pending" => false,
 -            "url" => admin.ap_id
 +            "approval_pending" => false,
 +            "url" => admin.ap_id,
 +            "registration_reason" => nil
            },
            %{
              "deactivated" => false,
              "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
 +            "approval_pending" => false,
 +            "url" => old_admin.ap_id,
 +            "registration_reason" => nil
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
               }
      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)
              "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(admin.name || admin.nickname),
              "confirmation_pending" => false,
 -            "url" => admin.ap_id
 +            "approval_pending" => false,
 +            "url" => admin.ap_id,
 +            "registration_reason" => nil
            },
            %{
              "deactivated" => false,
              "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
 +            "approval_pending" => false,
 +            "url" => second_admin.ap_id,
 +            "registration_reason" => nil
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
                     "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
                     "confirmation_pending" => false,
 -                   "url" => moderator.ap_id
 +                   "approval_pending" => false,
 +                   "url" => moderator.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
              "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(user1.name || user1.nickname),
              "confirmation_pending" => false,
 -            "url" => user1.ap_id
 +            "approval_pending" => false,
 +            "url" => user1.ap_id,
 +            "registration_reason" => nil
            },
            %{
              "deactivated" => false,
              "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
              "display_name" => HTML.strip_tags(user2.name || user2.nickname),
              "confirmation_pending" => false,
 -            "url" => user2.ap_id
 +            "approval_pending" => false,
 +            "url" => user2.ap_id,
 +            "registration_reason" => nil
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
                     "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(user.name || user.nickname),
                     "confirmation_pending" => false,
 -                   "url" => user.ap_id
 +                   "approval_pending" => false,
 +                   "url" => user.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
                     "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
                     "display_name" => HTML.strip_tags(admin.name || admin.nickname),
                     "confirmation_pending" => false,
 -                   "url" => admin.ap_id
 +                   "approval_pending" => false,
 +                   "url" => admin.ap_id,
 +                   "registration_reason" => nil
                   }
                 ]
               }
               "@#{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)
  
                 "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                 "display_name" => HTML.strip_tags(user.name || user.nickname),
                 "confirmation_pending" => false,
 -               "url" => user.ap_id
 +               "approval_pending" => false,
 +               "url" => user.ap_id,
 +               "registration_reason" => nil
               }
  
      log_entry = Repo.one(ModerationLog)
                 "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
                   second_user.nickname
                 }"
+       ObanHelpers.perform_all()
+       assert_email_sent(Pleroma.Emails.UserEmail.account_confirmation_email(first_user))
      end
    end
  
index 28d21371af0b60da4212ac13958c267fa67b5a0c,c304487eae581373957b7c04ee475ee627c0ee7d..e6b283aab390790074cb911678363b60f21038fa
@@@ -583,6 -583,15 +583,15 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
                 |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}")
                 |> json_response_and_validate_schema(200)
  
+       assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] =
+                conn
+                |> get(
+                  "/api/v1/accounts/#{user.id}/followers?id=#{user.id}&limit=20&max_id=#{
+                    follower3_id
+                  }"
+                )
+                |> json_response_and_validate_schema(200)
        res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}")
  
        assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200)
        assert id2 == following2.id
        assert id1 == following1.id
  
+       res_conn =
+         get(
+           conn,
+           "/api/v1/accounts/#{user.id}/following?id=#{user.id}&limit=20&max_id=#{following3.id}"
+         )
+       assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200)
+       assert id2 == following2.id
+       assert id1 == following1.id
        res_conn =
          get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
  
      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 =
        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)
index df9d59f6a857a00f43ef2c54219bd420ef04eaee,5bb2d8d89e1902eeb7f5b1412f7898c0e4876964..20a45cb6f94d74caa2f65c6333844576e512a7ff
@@@ -4,12 -4,11 +4,11 @@@
  
  defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
    use Pleroma.DataCase
 -
 +  import Pleroma.Factory
    alias Pleroma.Repo
    alias Pleroma.Tests.ObanHelpers
    alias Pleroma.User
    alias Pleroma.UserInviteToken
-   alias Pleroma.Web.MastodonAPI.AccountView
    alias Pleroma.Web.TwitterAPI.TwitterAPI
  
    setup_all do
  
      {:ok, user} = TwitterAPI.register_user(data)
  
-     fetched_user = User.get_cached_by_nickname("lain")
-     assert AccountView.render("show.json", %{user: user}) ==
-              AccountView.render("show.json", %{user: fetched_user})
+     assert user == User.get_cached_by_nickname("lain")
    end
  
-   test "it registers a new user with empty string in bio and returns the user." do
+   test "it registers a new user with empty string in bio and returns the user" do
      data = %{
        :username => "lain",
        :email => "lain@wired.jp",
  
      {:ok, user} = TwitterAPI.register_user(data)
  
-     fetched_user = User.get_cached_by_nickname("lain")
-     assert AccountView.render("show.json", %{user: user}) ==
-              AccountView.render("show.json", %{user: fetched_user})
+     assert user == User.get_cached_by_nickname("lain")
    end
  
    test "it sends confirmation email if :account_activation_required is specified in instance config" do
      )
    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",
  
        {:ok, user} = TwitterAPI.register_user(data)
  
-       fetched_user = User.get_cached_by_nickname("vinny")
-       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       assert user == User.get_cached_by_nickname("vinny")
  
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
        assert invite.used == true
-       assert AccountView.render("show.json", %{user: user}) ==
-                AccountView.render("show.json", %{user: fetched_user})
      end
  
      test "returns error on invalid token" do
        check_fn = fn invite ->
          data = Map.put(data, :token, invite.token)
          {:ok, user} = TwitterAPI.register_user(data)
-         fetched_user = User.get_cached_by_nickname("vinny")
  
-         assert AccountView.render("show.json", %{user: user}) ==
-                  AccountView.render("show.json", %{user: fetched_user})
+         assert user == User.get_cached_by_nickname("vinny")
        end
  
        {:ok, data: data, check_fn: check_fn}
        }
  
        {:ok, user} = TwitterAPI.register_user(data)
-       fetched_user = User.get_cached_by_nickname("vinny")
-       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       assert user == User.get_cached_by_nickname("vinny")
  
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
        assert invite.used == true
  
-       assert AccountView.render("show.json", %{user: user}) ==
-                AccountView.render("show.json", %{user: fetched_user})
        data = %{
          :username => "GrimReaper",
          :email => "death@reapers.afterlife",
        }
  
        {:ok, user} = TwitterAPI.register_user(data)
-       fetched_user = User.get_cached_by_nickname("vinny")
-       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       assert user == User.get_cached_by_nickname("vinny")
  
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
        refute invite.used
-       assert AccountView.render("show.json", %{user: user}) ==
-                AccountView.render("show.json", %{user: fetched_user})
      end
  
      test "error after max uses" do
        }
  
        {:ok, user} = TwitterAPI.register_user(data)
-       fetched_user = User.get_cached_by_nickname("vinny")
+       assert user == User.get_cached_by_nickname("vinny")
        invite = Repo.get_by(UserInviteToken, token: invite.token)
        assert invite.used == true
  
-       assert AccountView.render("show.json", %{user: user}) ==
-                AccountView.render("show.json", %{user: fetched_user})
        data = %{
          :username => "GrimReaper",
          :email => "death@reapers.afterlife",