Merge remote-tracking branch 'upstream/develop' into aliases
authorAlex Gleason <alex@alexgleason.me>
Fri, 7 Aug 2020 21:35:15 +0000 (16:35 -0500)
committerAlex Gleason <alex@alexgleason.me>
Fri, 7 Aug 2020 21:35:15 +0000 (16:35 -0500)
1  2 
CHANGELOG.md
docs/API/pleroma_api.md
lib/pleroma/user.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/router.ex
test/user_test.exs
test/web/mastodon_api/views/account_view_test.exs

diff --combined CHANGELOG.md
index ef32358049b7834f9c5bb57921e57e44f43b107f,a8e80eb3c189be2403f9326a2d467e807d55815a..71c2c03171c5ff2bae505de110e24e8d19c6ef25
@@@ -6,13 -6,17 +6,17 @@@ The format is based on [Keep a Changelo
  ## [unreleased]
  
  ### Changed
+ - **Breaking:** Added the ObjectAgePolicy to the default set of MRFs. This will delist and strip the follower collection of any message received that is older than 7 days. This will stop users from seeing very old messages in the timelines. The messages can still be viewed on the user's page and in conversations. They also still trigger notifications.
  - **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
  - OGP rich media parser merged with TwitterCard
  - Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
  - Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
+ - **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated.
+ - **Breaking:** LDAP: Fallback to local database authentication has been removed for security reasons and lack of a mechanism to ensure the passwords are synchronized when LDAP passwords are updated.
  
  <details>
    <summary>API Changes</summary>
@@@ -29,6 -33,9 +33,9 @@@
    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`
+ - Pleroma API: `/api/pleroma/captcha` responses now include `seconds_valid` with an integer value.
  </details>
  
  <details>
@@@ -44,6 -51,7 +51,7 @@@
  
  ### Added
  
+ - Configuration: Added a blacklist for email servers.
  - Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation.
  - Chats: Added support for federated chats. For details, see the docs.
  - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
  - 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.
 +- Ability to set ActivityPub aliases for follower migration.
+ - "By approval" registrations mode.
+ - Configuration: Added `:welcome` settings for the welcome message to newly registered users. You can send a welcome message as a direct message, chat or email.
+ - Ability to hide favourites and emoji reactions in the API with `[:instance, :show_reactions]` config.
  
  <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.
  - 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`
+ - Fix whole_word always returning false on filter get requests
  
  ## [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 docs/API/pleroma_api.md
index 8a937fdfdcf91781acbfc52582be6fd6d1967e18,4e97d26c052d0e0969cbe33f21db507139a9880e..c1aa4d204148b536141cbf1ec11ef6f5de88e336
@@@ -50,7 -50,7 +50,7 @@@ Request parameters can be passed via [q
  * Authentication: not required
  * Params: none
  * Response: Provider specific JSON, the only guaranteed parameter is `type`
- * Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint"}`
+ * Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint", "seconds_valid": 300}`
  
  ## `/api/pleroma/delete_account`
  ### Delete an account
@@@ -570,23 -570,3 +570,23 @@@ Emoji reactions work a lot like favouri
    {"name": "😀", "count": 2, "me": true, "accounts": [{"id" => "xyz.."...}, {"id" => "zyx..."}]}
  ]
  ```
 +
 +# Account aliases
 +
 +Set and delete ActivityPub aliases for follower move.
 +
 +## `POST /api/v1/pleroma/accounts/ap_aliases`
 +### Add account aliases
 +* Method: `POST`
 +* Authentication: required
 +* Params:
 +  * `aliases`: array of ActivityPub IDs to add
 +* Response: JSON, the user's account
 +
 +## `DELETE /api/v1/pleroma/accounts/ap_aliases`
 +### Delete account aliases
 +* Method: `DELETE`
 +* Authentication: required
 +* Params:
 +  * `aliases`: array of ActivityPub IDs to delete
 +* Response: JSON, the user's account
diff --combined lib/pleroma/user.ex
index 66664235b09c29d6190b2cd9cfcbf12ca7e92865,d1436a688455b2eeab07c883d612a56eeb1103c3..ad7a04f62cd3db05181adf7d66e335038650846a
@@@ -42,13 -42,16 +42,18 @@@ 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
    @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
 +  # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
 +  @url_regex ~r/https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/
  
    @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
    @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
@@@ -91,7 -94,6 +96,7 @@@
      field(:keys, :string)
      field(:public_key, :string)
      field(:ap_id, :string)
 +    field(:ap_aliases, {:array, :string}, default: [])
      field(:avatar, :map, default: %{})
      field(:local, :boolean, default: true)
      field(:follower_address, :string)
      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
    @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
    def force_password_reset(user), do: update_password_reset_pending(user, true)
  
+   # Used to auto-register LDAP accounts which won't have a password hash stored locally
+   def register_changeset_ldap(struct, params = %{password: password})
+       when is_nil(password) do
+     params = Map.put_new(params, :accepts_chat_messages, true)
+     params =
+       if Map.has_key?(params, :email) do
+         Map.put_new(params, :email, params[:email])
+       else
+         params
+       end
+     struct
+     |> cast(params, [
+       :name,
+       :nickname,
+       :email,
+       :accepts_chat_messages
+     ])
+     |> validate_required([:name, :nickname])
+     |> unique_constraint(:nickname)
+     |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+     |> validate_format(:nickname, local_nickname_regex())
+     |> put_ap_id()
+     |> unique_constraint(:ap_id)
+     |> put_following_and_follower_address()
+   end
    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? =
          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)
      |> unique_constraint(:email)
+     |> validate_format(:email, @email_regex)
+     |> validate_change(:email, fn :email, email ->
+       valid? =
+         Config.get([User, :email_blacklist])
+         |> Enum.all?(fn blacklisted_domain ->
+           !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
+         end)
+       if valid?, do: [], else: [email: "Invalid email"]
+     end)
      |> unique_constraint(:nickname)
      |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
      |> validate_format(:nickname, local_nickname_regex())
-     |> 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()
    def post_register_action(%User{} = user) do
      with {:ok, user} <- autofollow_users(user),
           {:ok, user} <- set_cache(user),
-          {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
+          {:ok, _} <- send_welcome_email(user),
+          {:ok, _} <- send_welcome_message(user),
+          {:ok, _} <- send_welcome_chat_message(user),
           {:ok, _} <- try_send_confirmation_email(user) do
        {:ok, user}
      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()
+   def send_welcome_message(user) do
+     if User.WelcomeMessage.enabled?() do
+       User.WelcomeMessage.post_message(user)
+       {:ok, :enqueued}
+     else
+       {:ok, :noop}
+     end
+   end
+   def send_welcome_chat_message(user) do
+     if User.WelcomeChatMessage.enabled?() do
+       User.WelcomeChatMessage.post_message(user)
+       {:ok, :enqueued}
+     else
+       {:ok, :noop}
+     end
+   end
+   def send_welcome_email(%User{email: email} = user) when is_binary(email) do
+     if User.WelcomeEmail.enabled?() do
+       User.WelcomeEmail.send_email(user)
+       {:ok, :enqueued}
+     else
+       {:ok, :noop}
+     end
+   end
+   def send_welcome_email(_), do: {:ok, :noop}
  
+   @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)
      |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
      |> Map.put(:fields, fields)
    end
 +
 +  def add_aliases(%User{} = user, aliases) when is_list(aliases) do
 +    alias_set =
 +      (user.ap_aliases ++ aliases)
 +      |> MapSet.new()
 +      |> MapSet.to_list()
 +
 +    user
 +    |> change(%{ap_aliases: alias_set})
 +    |> validate_ap_aliases()
 +    |> Repo.update()
 +  end
 +
 +  def delete_aliases(%User{} = user, aliases) when is_list(aliases) do
 +    alias_set =
 +      user.ap_aliases
 +      |> MapSet.new()
 +      |> MapSet.difference(MapSet.new(aliases))
 +      |> MapSet.to_list()
 +
 +    user
 +    |> change(%{ap_aliases: alias_set})
 +    |> validate_ap_aliases()
 +    |> Repo.update()
 +  end
 +
 +  defp validate_ap_aliases(changeset) do
 +    validate_change(changeset, :ap_aliases, fn :ap_aliases, ap_aliases ->
 +      case Enum.all?(ap_aliases, fn a -> Regex.match?(@url_regex, a) end) do
 +        true -> []
 +        false -> [ap_aliases: "Invalid ap_id format. Must be a URL."]
 +      end
 +    end)
 +  end
  end
index e2912031ad6a9dc795f5fc41f77966d51825d12d,864c0417f14a2694ac9cb0583d118ceb4d4806c4..4f29a80fbf66374dea1a2db6a2fc5f7a60364bb9
@@@ -27,21 -27,40 +27,40 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
            UserRelationship.view_relationships_option(reading_user, users)
        end
  
-     opts = Map.put(opts, :relationships, relationships_opt)
+     opts =
+       opts
+       |> Map.merge(%{relationships: relationships_opt, as: :user})
+       |> Map.delete(:users)
  
      users
      |> render_many(AccountView, "show.json", opts)
      |> Enum.filter(&Enum.any?/1)
    end
  
-   def render("show.json", %{user: user} = opts) do
-     if User.visible_for(user, opts[:for]) == :visible do
+   @doc """
+   Renders specified user account.
+     :skip_visibility_check option skips visibility check and renders any user (local or remote)
+       regardless of [:pleroma, :restrict_unauthenticated] setting.
+     :for option specifies the requester and can be a User record or nil.
+       Only use `user: user, for: user` when `user` is the actual requester of own profile.
+   """
+   def render("show.json", %{user: _user, skip_visibility_check: true} = opts) do
+     do_render("show.json", opts)
+   end
+   def render("show.json", %{user: user, for: for_user_or_nil} = opts) do
+     if User.visible_for(user, for_user_or_nil) == :visible do
        do_render("show.json", opts)
      else
        %{}
      end
    end
  
+   def render("show.json", _) do
+     raise "In order to prevent account accessibility issues, " <>
+             ":skip_visibility_check or :for option is required."
+   end
    def render("mention.json", %{user: user}) do
      %{
        id: to_string(user.id),
        # Pleroma extension
        pleroma: %{
          ap_id: user.ap_id,
 +        ap_aliases: user.ap_aliases,
          confirmation_pending: user.confirmation_pending,
          tags: user.tags,
          hide_followers_count: user.hide_followers_count,
index dea95cd7790f0c641626b689db297be67ccdc1bc,c6433cc5325f3e923dc9150632c82ec5945d7d5a..fbab0fc272abd43da8fc3d7b28d97755113469da
@@@ -138,6 -138,7 +138,7 @@@ defmodule Pleroma.Web.Router d
      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)
  
  
        post("/accounts/:id/subscribe", AccountController, :subscribe)
        post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
 +
 +      post("/accounts/ap_aliases", AccountController, :add_aliases)
 +      delete("/accounts/ap_aliases", AccountController, :delete_aliases)
      end
  
      post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
diff --combined test/user_test.exs
index 29855b9cdc9a0ebbbf1ebaaf8b0b053421460816,b4740589593bb8e673b6914ac63e51102ba4e566..941e484086887c767809a1c822838b83f0987b2f
@@@ -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([:welcome])
+     setup do: clear_config([:instance, :account_activation_required])
  
      test "it autofollows accounts that are set for it" do
        user = insert(:user)
  
      test "it sends a welcome message if it is set" do
        welcome_user = insert(:user)
+       Pleroma.Config.put([:welcome, :direct_message, :enabled], true)
+       Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname)
+       Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message")
+       cng = User.register_changeset(%User{}, @full_user_data)
+       {:ok, registered_user} = User.register(cng)
+       ObanHelpers.perform_all()
+       activity = Repo.one(Pleroma.Activity)
+       assert registered_user.ap_id in activity.recipients
+       assert Object.normalize(activity).data["content"] =~ "direct message"
+       assert activity.actor == welcome_user.ap_id
+     end
  
-       Pleroma.Config.put([:instance, :welcome_user_nickname], welcome_user.nickname)
-       Pleroma.Config.put([:instance, :welcome_message], "Hello, this is a cool site")
+     test "it sends a welcome chat message if it is set" do
+       welcome_user = insert(:user)
+       Pleroma.Config.put([:welcome, :chat_message, :enabled], true)
+       Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
+       Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message")
  
        cng = User.register_changeset(%User{}, @full_user_data)
        {:ok, registered_user} = User.register(cng)
+       ObanHelpers.perform_all()
  
        activity = Repo.one(Pleroma.Activity)
        assert registered_user.ap_id in activity.recipients
-       assert Object.normalize(activity).data["content"] =~ "cool site"
+       assert Object.normalize(activity).data["content"] =~ "chat message"
        assert activity.actor == welcome_user.ap_id
      end
  
-     setup do: clear_config([:instance, :account_activation_required])
+     test "it sends a welcome email message if it is set" do
+       welcome_user = insert(:user)
+       Pleroma.Config.put([:welcome, :email, :enabled], true)
+       Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email)
+       Pleroma.Config.put(
+         [:welcome, :email, :subject],
+         "Hello, welcome to cool site: <%= instance_name %>"
+       )
+       instance_name = Pleroma.Config.get([:instance, :name])
+       cng = User.register_changeset(%User{}, @full_user_data)
+       {:ok, registered_user} = User.register(cng)
+       ObanHelpers.perform_all()
+       assert_email_sent(
+         from: {instance_name, welcome_user.email},
+         to: {registered_user.name, registered_user.email},
+         subject: "Hello, welcome to cool site: #{instance_name}",
+         html_body: "Welcome to #{instance_name}"
+       )
+     end
+     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)
        refute changeset.valid?
      end
  
+     test "it blocks blacklisted email domains" do
+       clear_config([User, :email_blacklist], ["trolling.world"])
+       # Block with match
+       params = Map.put(@full_user_data, :email, "troll@trolling.world")
+       changeset = User.register_changeset(%User{}, params)
+       refute changeset.valid?
+       # Block with subdomain match
+       params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world")
+       changeset = User.register_changeset(%User{}, params)
+       refute changeset.valid?
+       # Pass with different domains that are similar
+       params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world")
+       changeset = User.register_changeset(%User{}, params)
+       assert changeset.valid?
+       params = Map.put(@full_user_data, :email, "troll@trolling.world.us")
+       changeset = User.register_changeset(%User{}, params)
+       assert changeset.valid?
+     end
      test "it sets the password_hash and ap_id" do
        changeset = User.register_changeset(%User{}, @full_user_data)
  
  
        assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
      end
+     test "it sets the 'accepts_chat_messages' set to true" do
+       changeset = User.register_changeset(%User{}, @full_user_data)
+       assert changeset.valid?
+       {:ok, user} = Repo.insert(changeset)
+       assert user.accepts_chat_messages
+     end
+     test "it creates a confirmed user" do
+       changeset = User.register_changeset(%User{}, @full_user_data)
+       assert changeset.valid?
+       {:ok, user} = Repo.insert(changeset)
+       refute user.confirmation_pending
+     end
    end
  
    describe "user registration, with :account_activation_required" do
      }
      setup do: clear_config([:instance, :account_activation_required], true)
  
-     test "it sets the 'accepts_chat_messages' set to true" do
-       changeset = User.register_changeset(%User{}, @full_user_data)
-       assert changeset.valid?
-       {:ok, user} = Repo.insert(changeset)
-       assert user.accepts_chat_messages
-     end
      test "it creates unconfirmed user" do
        changeset = User.register_changeset(%User{}, @full_user_data)
        assert changeset.valid?
      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",
+       registration_reason: "I'm a cool guy :)"
+     }
+     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
+       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
    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
  
      assert User.avatar_url(user, no_default: true) == nil
    end
 +
 +  test "add_aliases/2" do
 +    user = insert(:user)
 +
 +    aliases = [
 +      "https://gleasonator.com/users/alex",
 +      "https://gleasonator.com/users/alex",
 +      "https://animalliberation.social/users/alex"
 +    ]
 +
 +    {:ok, user} = User.add_aliases(user, aliases)
 +
 +    assert user.ap_aliases == [
 +             "https://animalliberation.social/users/alex",
 +             "https://gleasonator.com/users/alex"
 +           ]
 +  end
 +
 +  test "add_aliases/2 with invalid alias" do
 +    user = insert(:user)
 +    {:error, _} = User.add_aliases(user, ["invalid_alias"])
 +    {:error, _} = User.add_aliases(user, ["http://still_invalid"])
 +    {:error, _} = User.add_aliases(user, ["http://validalias.com/users/dude", "invalid_alias"])
 +  end
 +
 +  test "delete_aliases/2" do
 +    user =
 +      insert(:user,
 +        ap_aliases: [
 +          "https://animalliberation.social/users/alex",
 +          "https://benis.social/users/benis",
 +          "https://gleasonator.com/users/alex"
 +        ]
 +      )
 +
 +    aliases = ["https://benis.social/users/benis"]
 +
 +    {:ok, user} = User.delete_aliases(user, aliases)
 +
 +    assert user.ap_aliases == [
 +             "https://animalliberation.social/users/alex",
 +             "https://gleasonator.com/users/alex"
 +           ]
 +  end
  end
index 4a0512e6855bdacc115ff2fc3044aa9eb77f0352,8f37efa3c3e2b8df5fa13bfa7955ac5f898fbb3c..a55b5a06e429686645513cf9e94c0c5a9a012513
@@@ -37,8 -37,7 +37,8 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
            "<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f '&<>\"",
          inserted_at: ~N[2017-08-15 15:47:06.597036],
          emoji: %{"karjalanpiirakka" => "/file.png"},
 -        raw_bio: "valid html. a\nb\nc\nd\nf '&<>\""
 +        raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"",
 +        ap_aliases: ["https://shitposter.zone/users/shp"]
        })
  
      expected = %{
@@@ -78,7 -77,6 +78,7 @@@
        },
        pleroma: %{
          ap_id: user.ap_id,
 +        ap_aliases: ["https://shitposter.zone/users/shp"],
          background_image: "https://example.com/images/asuka_hospital.png",
          favicon:
            "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
@@@ -97,7 -95,7 +97,7 @@@
        }
      }
  
-     assert expected == AccountView.render("show.json", %{user: user})
+     assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
    end
  
    test "Favicon is nil when :instances_favicons is disabled" do
                 favicon:
                   "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png"
               }
-            } = AccountView.render("show.json", %{user: user})
+            } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
  
      Config.put([:instances_favicons, :enabled], false)
  
-     assert %{pleroma: %{favicon: nil}} = AccountView.render("show.json", %{user: user})
+     assert %{pleroma: %{favicon: nil}} =
+              AccountView.render("show.json", %{user: user, skip_visibility_check: true})
    end
  
    test "Represent the user account for the account owner" do
        },
        pleroma: %{
          ap_id: user.ap_id,
 +        ap_aliases: [],
          background_image: nil,
          favicon:
            "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
        }
      }
  
-     assert expected == AccountView.render("show.json", %{user: user})
+     assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
    end
  
    test "Represent a Funkwhale channel" do
          "https://channels.tests.funkwhale.audio/federation/actors/compositions"
        )
  
-     assert represented = AccountView.render("show.json", %{user: user})
+     assert represented =
+              AccountView.render("show.json", %{user: user, skip_visibility_check: true})
      assert represented.acct == "compositions@channels.tests.funkwhale.audio"
      assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions"
    end
      assert expected == AccountView.render("mention.json", %{user: user})
    end
  
+   test "demands :for or :skip_visibility_check option for account rendering" do
+     clear_config([:restrict_unauthenticated, :profiles, :local], false)
+     user = insert(:user)
+     user_id = user.id
+     assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil})
+     assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user})
+     assert %{id: ^user_id} =
+              AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+     assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn ->
+       AccountView.render("show.json", %{user: user})
+     end
+   end
    describe "relationship" do
      defp test_relationship_rendering(user, other_user, expected_result) do
        opts = %{user: user, target: other_user, relationships: nil}
  
      assert result.pleroma.settings_store == %{:fe => "test"}
  
-     result = AccountView.render("show.json", %{user: user, with_pleroma_settings: true})
+     result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true})
      assert result.pleroma[:settings_store] == nil
  
      result = AccountView.render("show.json", %{user: user, for: user})
  
    test "doesn't sanitize display names" do
      user = insert(:user, name: "<marquee> username </marquee>")
-     result = AccountView.render("show.json", %{user: user})
+     result = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
      assert result.display_name == "<marquee> username </marquee>"
    end
  
    test "never display nil user follow counts" do
      user = insert(:user, following_count: 0, follower_count: 0)
-     result = AccountView.render("show.json", %{user: user})
+     result = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
  
      assert result.following_count == 0
      assert result.followers_count == 0
                 followers_count: 0,
                 following_count: 0,
                 pleroma: %{hide_follows_count: true, hide_followers_count: true}
-              } = AccountView.render("show.json", %{user: user})
+              } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
      end
  
      test "shows when follows/followers are hidden" do
                 followers_count: 1,
                 following_count: 1,
                 pleroma: %{hide_follows: true, hide_followers: true}
-              } = AccountView.render("show.json", %{user: user})
+              } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
      end
  
      test "shows actual follower/following count to the account owner" do
          emoji: %{"joker_smile" => "https://evil.website/society.png"}
        )
  
-     AccountView.render("show.json", %{user: user})
+     AccountView.render("show.json", %{user: user, skip_visibility_check: true})
      |> Enum.all?(fn
        {key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
          String.starts_with?(url, Pleroma.Web.base_url())