Merge branch 'admin-create-users' into 'develop'
authorlain <lain@soykaf.club>
Sat, 24 Aug 2019 16:04:19 +0000 (16:04 +0000)
committerlain <lain@soykaf.club>
Sat, 24 Aug 2019 16:04:19 +0000 (16:04 +0000)
user creation admin api will create multiple users

See merge request pleroma/pleroma!1170

1  2 
CHANGELOG.md
lib/pleroma/user.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/router.ex
test/web/admin_api/admin_api_controller_test.exs

diff --combined CHANGELOG.md
index b1ec218183317cda6eef5eb6ff894b799dc07931,8ba48b72cf0d06bb912e9c04f158f9c8bef483b2..9c3051c944a35d185592fefc93b94672d3b01df9
@@@ -3,127 -3,12 +3,127 @@@ All notable changes to this project wil
  
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  
 -## [unreleased]
 +## [Unreleased]
 +### Security
 +- OStatus: eliminate the possibility of a protocol downgrade attack.
 +- OStatus: prevent following locked accounts, bypassing the approval process.
 +
 +### Changed
 +- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
 +- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
 +- Configuration: OpenGraph and TwitterCard providers enabled by default
 +- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
 +- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
 +- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
 +- NodeInfo: Return `mailerEnabled` in `metadata`
 +- Mastodon API: Unsubscribe followers when they unfollow a user
 +- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
 +- Improve digest email template
 +
 +### Fixed
 +- Not being able to pin unlisted posts
 +- Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
 +- Favorites timeline doing database-intensive queries
 +- Metadata rendering errors resulting in the entire page being inaccessible
 +- `federation_incoming_replies_max_depth` option being ignored in certain cases
 +- Federation/MediaProxy not working with instances that have wrong certificate order
 +- Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
 +- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
 +- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
 +- Mastodon API: `muted` in the Status entity, using author's account to determine if the tread was muted
 +- Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
 +- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
 +- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
 +- Existing user id not being preserved on insert conflict
 +- Rich Media: Parser failing when no TTL can be found by image TTL setters
 +- Rich Media: The crawled URL is now spliced into the rich media data.
 +- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
 +- ActivityPub S2S: remote user deletions now work the same as local user deletions.
 +- ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
 +- Not being able to access the Mastodon FE login page on private instances
 +- Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
 +- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
 +- Report email not being sent to admins when the reporter is a remote user
 +- MRF: ensure that subdomain_match calls are case-insensitive
 +- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
 +- MRF: fix use of unserializable keyword lists in describe() implementations
 +- ActivityPub: Deactivated user deletion
 +
 +### Added
 +- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
 +- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour.
 +- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty.
 +- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default.
 +- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
 +- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
 +  Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
 +- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
 +- MRF: Support for excluding specific domains from Transparency.
 +- MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)
 +- MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
 +- MRF (Simple Policy): Support for wildcard domains.
 +- Support for wildcard domains in user domain blocks setting.
 +- Configuration: `quarantined_instances` support wildcard domains.
 +- Configuration: `federation_incoming_replies_max_depth` option
 +- Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
 +- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
 +- Mastodon API, extension: Ability to reset avatar, profile banner, and background
 +- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
 +- Mastodon API: Add support for muting/unmuting notifications
 +- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
 +- Mastodon API: Add support for the `domain_blocking` attribute in the relationship API (`GET /api/v1/accounts/relationships`).
 +- Mastodon API: Add `pleroma.deactivated` to the Account entity
 +- Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
 +- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
 +- Mastodon API: Improve support for the user profile custom fields
 +- Admin API: Return users' tags when querying reports
 +- Admin API: Return avatar and display name when querying users
 +- Admin API: Allow querying user by ID
 +- Admin API: Added support for `tuples`.
 +- Admin API: Added endpoints to run mix tasks pleroma.config migrate_to_db & pleroma.config migrate_from_db
 +- Added synchronization of following/followers counters for external users
 +- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
 +- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
 +- Configuration: `user_bio_length` and `user_name_length` options.
 +- Addressable lists
 +- Twitter API: added rate limit for `/api/account/password_reset` endpoint.
 +- ActivityPub: Add an internal service actor for fetching ActivityPub objects.
 +- ActivityPub: Optional signing of ActivityPub object fetches.
 +- Admin API: Endpoint for fetching latest user's statuses
 +- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
 +- Relays: Added a task to list relay subscriptions.
 +- Mix Tasks: `mix pleroma.database fix_likes_collections`
 +- Federation: Remove `likes` from objects.
 +
 +### Changed
 +- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
 +- Admin API: changed json structure for saving config settings.
 +- RichMedia: parsers and their order are configured in `rich_media` config.
 +- RichMedia: add the rich media ttl based on image expiration time.
 +
 +### Removed
 +- Emoji: Remove longfox emojis.
 +- Remove `Reply-To` header from report emails for admins.
 +- ActivityPub: The `accept_blocks` configuration setting.
 +
 +## [1.0.1] - 2019-07-14
 +### Security
 +- OStatus: fix an object spoofing vulnerability.
 +
 +## [1.0.0] - 2019-06-29
 +### Security
 +- Mastodon API: Fix display names not being sanitized
 +- Rich media: Do not crawl private IP ranges
 +
  ### Added
 +- Digest email for inactive users
 +- Add a generic settings store for frontends / clients to use.
 +- Explicit addressing option for posting.
  - Optional SSH access mode. (Needs `erlang-ssh` package on some distributions).
  - [MongooseIM](https://github.com/esl/MongooseIM) http authentication support.
  - LDAP authentication
  - External OAuth provider authentication
 +- Support for building a release using [`mix release`](https://hexdocs.pm/mix/master/Mix.Tasks.Release.html)
  - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
  - [Prometheus](https://prometheus.io/) metrics
  - Support for Mastodon's remote interaction
  - Mix Tasks: `mix pleroma.database remove_embedded_objects`
  - Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
  - Mix Tasks: `mix pleroma.user toggle_confirmed`
 +- Mix Tasks: `mix pleroma.config migrate_to_db`
 +- Mix Tasks: `mix pleroma.config migrate_from_db`
 +- Federation: Support for `Question` and `Answer` objects
  - Federation: Support for reports
 +- Configuration: `poll_limits` option
 +- Configuration: `pack_extensions` option
  - Configuration: `safe_dm_mentions` option
  - Configuration: `link_name` option
  - Configuration: `fetch_initial_posts` option
  - Configuration: `notify_email` option
  - Configuration: Media proxy `whitelist` option
  - Configuration: `report_uri` option
 +- Configuration: `email_notifications` option
 +- Configuration: `limit_to_local_content` option
  - Pleroma API: User subscriptions
  - Pleroma API: Healthcheck endpoint
  - Pleroma API: `/api/v1/pleroma/mascot` per-user frontend mascot configuration endpoints
  - Admin API: added filters (role, tags, email, name) for users endpoint
  - Admin API: Endpoints for managing reports
  - Admin API: Endpoints for deleting and changing the scope of individual reported statuses
 +- Admin API: Endpoints to view and change config settings.
  - AdminFE: initial release with basic user management accessible at /pleroma/admin/
 +- Mastodon API: Add chat token to `verify_credentials` response
 +- Mastodon API: Add background image setting to `update_credentials`
  - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
  - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
  - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
  - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
  - Mastodon API: `POST /api/v1/accounts` (account creation API)
 +- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/)
  - ActivityPub C2S: OAuth endpoints
  - Metadata: RelMe provider
  - OAuth: added support for refresh tokens
  - OAuth: added job to clean expired access tokens
  - MRF: Support for rejecting reports from specific instances (`mrf_simple`)
  - MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
 +- MRF: Support for running subchains.
 +- Configuration: `skip_thread_containment` option
 +- Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
 +- MRF: Support for filtering out likely spam messages by rejecting posts from new users that contain links.
 +- Configuration: `ignore_hosts` option
 +- Configuration: `ignore_tld` option
 +- Configuration: default syslog tag "Pleroma" is now lowercased to "pleroma"
  
  ### Changed
 +- **Breaking:** bind to 127.0.0.1 instead of 0.0.0.0 by default
  - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
 +- Thread containment / test for complete visibility will be skipped by default.
  - Enforcement of OAuth scopes
  - Add multiple use/time expiring invite token
  - Restyled OAuth pages to fit with Pleroma's default theme
  - Federation: Expand the audience of delete activities to all recipients of the deleted object
  - Federation: Removed `inReplyToStatusId` from objects
  - Configuration: Dedupe enabled by default
 +- Configuration: Default log level in `prod` environment is now set to `warn`
  - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
  - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
  - Admin API: Move the user related API to `api/pleroma/admin/users`
+ - Admin API: `POST /api/pleroma/admin/users` will take list of users
  - Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
  - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
  - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
  - Posts which are marked sensitive or tagged nsfw no longer have link previews.
  - HTTP connection timeout is now set to 10 seconds.
  - Respond with a 404 Not implemented JSON error message when requested API is not implemented
 +- Rich Media: crawl only https URLs.
  
  ### Fixed
 +- Follow requests don't get 'stuck' anymore.
  - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.
  - Followers counter not being updated when a follower is blocked
  - Deactivated users being able to request an access token
diff --combined lib/pleroma/user.ex
index 134b8bb6c64f971ede1fe74af7fbaae2434e68cc,6abcb7288f5f3237a80cb212409656430cc153ef..29fd6d2ea1cba4d4343e88548d648a29e36f45f6
@@@ -9,19 -9,16 +9,19 @@@ defmodule Pleroma.User d
    import Ecto.Query
  
    alias Comeonin.Pbkdf2
 +  alias Ecto.Multi
    alias Pleroma.Activity
    alias Pleroma.Keys
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Registration
    alias Pleroma.Repo
 +  alias Pleroma.RepoStreamer
    alias Pleroma.User
    alias Pleroma.Web
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
 +  alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
    alias Pleroma.Web.OAuth
    alias Pleroma.Web.OStatus
      field(:avatar, :map)
      field(:local, :boolean, default: true)
      field(:follower_address, :string)
 +    field(:following_address, :string)
      field(:search_rank, :float, virtual: true)
      field(:search_type, :integer, virtual: true)
      field(:tags, {:array, :string}, default: [])
      field(:last_refreshed_at, :naive_datetime_usec)
 +    field(:last_digest_emailed_at, :naive_datetime)
      has_many(:notifications, Notification)
      has_many(:registrations, Registration)
      embeds_one(:info, User.Info)
    def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
    def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
  
 -  def user_info(%User{} = user) do
 +  @spec ap_following(User.t()) :: Sring.t()
 +  def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
 +  def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
 +
 +  def user_info(%User{} = user, args \\ %{}) do
 +    following_count =
 +      if args[:following_count],
 +        do: args[:following_count],
 +        else: user.info.following_count || following_count(user)
 +
 +    follower_count =
 +      if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
 +
      %{
 -      following_count: following_count(user),
        note_count: user.info.note_count,
 -      follower_count: user.info.follower_count,
        locked: user.info.locked,
        confirmation_pending: user.info.confirmation_pending,
        default_scope: user.info.default_scope
      }
 +    |> Map.put(:following_count, following_count)
 +    |> Map.put(:follower_count, follower_count)
 +  end
 +
 +  def follow_state(%User{} = user, %User{} = target) do
 +    follow_activity = Utils.fetch_latest_follow(user, target)
 +
 +    if follow_activity,
 +      do: follow_activity.data["state"],
 +      # Ideally this would be nil, but then Cachex does not commit the value
 +      else: false
    end
  
 +  def get_cached_follow_state(user, target) do
 +    key = "follow_state:#{user.ap_id}|#{target.ap_id}"
 +    Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
 +  end
 +
 +  def set_follow_state_cache(user_ap_id, target_ap_id, state) do
 +    Cachex.put(
 +      :user_cache,
 +      "follow_state:#{user_ap_id}|#{target_ap_id}",
 +      state
 +    )
 +  end
 +
 +  def set_info_cache(user, args) do
 +    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
 +  end
 +
 +  @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
    def restrict_deactivated(query) do
      from(u in query,
        where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
    end
  
    def remote_user_creation(params) do
 -    params =
 -      params
 -      |> Map.put(:info, params[:info] || %{})
 +    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
 +    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
  
 +    params = Map.put(params, :info, params[:info] || %{})
      info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
  
      changes =
        |> validate_required([:name, :ap_id])
        |> unique_constraint(:nickname)
        |> validate_format(:nickname, @email_regex)
 -      |> validate_length(:bio, max: 5000)
 -      |> validate_length(:name, max: 100)
 +      |> validate_length(:bio, max: bio_limit)
 +      |> validate_length(:name, max: name_limit)
        |> put_change(:local, false)
        |> put_embed(:info, info_cng)
  
      if changes.valid? do
        case info_cng.changes[:source_data] do
 -        %{"followers" => followers} ->
 +        %{"followers" => followers, "following" => following} ->
            changes
            |> put_change(:follower_address, followers)
 +          |> put_change(:following_address, following)
  
          _ ->
            followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
    end
  
    def update_changeset(struct, params \\ %{}) do
 +    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
 +    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
 +
      struct
      |> cast(params, [:bio, :name, :avatar, :following])
      |> unique_constraint(:nickname)
      |> validate_format(:nickname, local_nickname_regex())
 -    |> validate_length(:bio, max: 5000)
 -    |> validate_length(:name, min: 1, max: 100)
 +    |> validate_length(:bio, max: bio_limit)
 +    |> validate_length(:name, min: 1, max: name_limit)
    end
  
 -  def upgrade_changeset(struct, params \\ %{}) do
 -    params =
 -      params
 -      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
 +  def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
 +    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
 +    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
  
 -    info_cng =
 -      struct.info
 -      |> User.Info.user_upgrade(params[:info])
 +    params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
 +    info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
  
      struct
 -    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
 +    |> cast(params, [
 +      :bio,
 +      :name,
 +      :follower_address,
 +      :following_address,
 +      :avatar,
 +      :last_refreshed_at
 +    ])
      |> unique_constraint(:nickname)
      |> validate_format(:nickname, local_nickname_regex())
 -    |> validate_length(:bio, max: 5000)
 -    |> validate_length(:name, max: 100)
 +    |> validate_length(:bio, max: bio_limit)
 +    |> validate_length(:name, max: name_limit)
      |> put_embed(:info, info_cng)
    end
  
    def password_update_changeset(struct, params) do
 -    changeset =
 -      struct
 -      |> cast(params, [:password, :password_confirmation])
 -      |> validate_required([:password, :password_confirmation])
 -      |> validate_confirmation(:password)
 -
 -    OAuth.Token.delete_user_tokens(struct)
 -    OAuth.Authorization.delete_user_authorizations(struct)
 -
 -    if changeset.valid? do
 -      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
 -
 -      changeset
 -      |> put_change(:password_hash, hashed)
 -    else
 -      changeset
 +    struct
 +    |> cast(params, [:password, :password_confirmation])
 +    |> validate_required([:password, :password_confirmation])
 +    |> validate_confirmation(:password)
 +    |> put_password_hash
 +  end
 +
 +  @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
 +  def reset_password(%User{id: user_id} = user, data) do
 +    multi =
 +      Multi.new()
 +      |> Multi.update(:user, password_update_changeset(user, data))
 +      |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
 +      |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
 +
 +    case Repo.transaction(multi) do
 +      {:ok, %{user: user} = _} -> set_cache(user)
 +      {:error, _, changeset, _} -> {:error, changeset}
      end
    end
  
 -  def reset_password(user, data) do
 -    update_and_set_cache(password_update_changeset(user, data))
 -  end
 -
    def register_changeset(struct, params \\ %{}, opts \\ []) do
 +    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
 +    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
 +
      need_confirmation? =
        if is_nil(opts[:need_confirmation]) do
          Pleroma.Config.get([:instance, :account_activation_required])
        |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
        |> validate_format(:nickname, local_nickname_regex())
        |> validate_format(:email, @email_regex)
 -      |> validate_length(:bio, max: 1000)
 -      |> validate_length(:name, min: 1, max: 100)
 +      |> validate_length(:bio, max: bio_limit)
 +      |> validate_length(:name, min: 1, max: name_limit)
        |> put_change(:info, info_change)
  
      changeset =
        end
  
      if changeset.valid? do
 -      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
        followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
  
        changeset
 -      |> put_change(:password_hash, hashed)
 +      |> put_password_hash
        |> put_change(:ap_id, ap_id)
        |> unique_constraint(:ap_id)
        |> put_change(:following, [followers])
    @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
    def register(%Ecto.Changeset{} = changeset) do
      with {:ok, user} <- Repo.insert(changeset),
-          {:ok, user} <- autofollow_users(user),
+          {:ok, user} <- post_register_action(user) do
+       {:ok, user}
+     end
+   end
+   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, _} <- try_send_confirmation_email(user) do
  
    def needs_update?(_), do: true
  
 +  @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
    def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
      {:ok, follower}
    end
      end
    end
  
 -  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
 -    if not following?(follower, followed) do
 -      follow(follower, followed)
 -    else
 -      {:ok, follower}
 -    end
 -  end
 -
    @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
    @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
    def follow_all(follower, followeds) do
      ap_followers = followed.follower_address
  
      cond do
 -      following?(follower, followed) or info.deactivated ->
 -        {:error, "Could not follow user: #{followed.nickname} is already on your list."}
 +      info.deactivated ->
 +        {:error, "Could not follow user: You are deactivated."}
  
        deny_follow_blocked and blocks?(followed, follower) ->
          {:error, "Could not follow user: #{followed.nickname} blocked you."}
  
          {1, [follower]} = Repo.update_all(q, [])
  
 +        follower = maybe_update_following_count(follower)
 +
          {:ok, _} = update_follower_count(followed)
  
          set_cache(follower)
  
        {1, [follower]} = Repo.update_all(q, [])
  
 +      follower = maybe_update_following_count(follower)
 +
        {:ok, followed} = update_follower_count(followed)
  
        set_cache(follower)
      Repo.get_by(User, ap_id: ap_id)
    end
  
 +  def get_all_by_ap_id(ap_ids) do
 +    from(u in __MODULE__,
 +      where: u.ap_id in ^ap_ids
 +    )
 +    |> Repo.all()
 +  end
 +
    # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
    # of the ap_id and the domain and tries to get that user
    def get_by_guessed_nickname(ap_id) do
    end
  
    def update_and_set_cache(changeset) do
 -    with {:ok, user} <- Repo.update(changeset) do
 +    with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
        set_cache(user)
      else
        e -> e
    @spec get_followers_query(User.t()) :: Ecto.Query.t()
    def get_followers_query(user), do: get_followers_query(user, nil)
  
 +  @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
    def get_followers(user, page \\ nil) do
      q = get_followers_query(user, page)
  
      {:ok, Repo.all(q)}
    end
  
 +  @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
 +  def get_external_followers(user, page \\ nil) do
 +    q =
 +      user
 +      |> get_followers_query(page)
 +      |> User.Query.build(%{external: true})
 +
 +    {:ok, Repo.all(q)}
 +  end
 +
    def get_followers_ids(user, page \\ nil) do
      q = get_followers_query(user, page)
  
      |> update_and_set_cache()
    end
  
 +  @spec maybe_fetch_follow_information(User.t()) :: User.t()
 +  def maybe_fetch_follow_information(user) do
 +    with {:ok, user} <- fetch_follow_information(user) do
 +      user
 +    else
 +      e ->
 +        Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
 +
 +        user
 +    end
 +  end
 +
 +  def fetch_follow_information(user) do
 +    with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
 +      info_cng = User.Info.follow_information_update(user.info, info)
 +
 +      changeset =
 +        user
 +        |> change()
 +        |> put_embed(:info, info_cng)
 +
 +      update_and_set_cache(changeset)
 +    else
 +      {:error, _} = e -> e
 +      e -> {:error, e}
 +    end
 +  end
 +
    def update_follower_count(%User{} = user) do
 -    follower_count_query =
 -      User.Query.build(%{followers: user, deactivated: false})
 -      |> select([u], %{count: count(u.id)})
 +    if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
 +      follower_count_query =
 +        User.Query.build(%{followers: user, deactivated: false})
 +        |> select([u], %{count: count(u.id)})
 +
 +      User
 +      |> where(id: ^user.id)
 +      |> join(:inner, [u], s in subquery(follower_count_query))
 +      |> update([u, s],
 +        set: [
 +          info:
 +            fragment(
 +              "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
 +              u.info,
 +              s.count
 +            )
 +        ]
 +      )
 +      |> select([u], u)
 +      |> Repo.update_all([])
 +      |> case do
 +        {1, [user]} -> set_cache(user)
 +        _ -> {:error, user}
 +      end
 +    else
 +      {:ok, maybe_fetch_follow_information(user)}
 +    end
 +  end
  
 -    User
 -    |> where(id: ^user.id)
 -    |> join(:inner, [u], s in subquery(follower_count_query))
 -    |> update([u, s],
 -      set: [
 -        info:
 -          fragment(
 -            "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
 -            u.info,
 -            s.count
 -          )
 -      ]
 -    )
 -    |> select([u], u)
 -    |> Repo.update_all([])
 -    |> case do
 -      {1, [user]} -> set_cache(user)
 -      _ -> {:error, user}
 +  @spec maybe_update_following_count(User.t()) :: User.t()
 +  def maybe_update_following_count(%User{local: false} = user) do
 +    if Pleroma.Config.get([:instance, :external_user_synchronization]) do
 +      maybe_fetch_follow_information(user)
 +    else
 +      user
      end
    end
  
 +  def maybe_update_following_count(user), do: user
 +
    def remove_duplicated_following(%User{following: following} = user) do
      uniq_following = Enum.uniq(following)
  
      |> Repo.all()
    end
  
 -  def search(query, resolve \\ false, for_user \\ nil) do
 -    # Strip the beginning @ off if there is a query
 -    query = String.trim_leading(query, "@")
 -
 -    if resolve, do: get_or_fetch(query)
 -
 -    {:ok, results} =
 -      Repo.transaction(fn ->
 -        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
 -        Repo.all(search_query(query, for_user))
 -      end)
 -
 -    results
 -  end
 -
 -  def search_query(query, for_user) do
 -    fts_subquery = fts_search_subquery(query)
 -    trigram_subquery = trigram_search_subquery(query)
 -    union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
 -    distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
 +  @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
 +  def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
 +    info = muter.info
  
 -    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
 -      order_by: [desc: s.search_rank],
 -      limit: 40
 -    )
 -  end
 -
 -  defp boost_search_rank_query(query, nil), do: query
 -
 -  defp boost_search_rank_query(query, for_user) do
 -    friends_ids = get_friends_ids(for_user)
 -    followers_ids = get_followers_ids(for_user)
 -
 -    from(u in subquery(query),
 -      select_merge: %{
 -        search_rank:
 -          fragment(
 -            """
 -             CASE WHEN (?) THEN (?) * 1.3
 -             WHEN (?) THEN (?) * 1.2
 -             WHEN (?) THEN (?) * 1.1
 -             ELSE (?) END
 -            """,
 -            u.id in ^friends_ids and u.id in ^followers_ids,
 -            u.search_rank,
 -            u.id in ^friends_ids,
 -            u.search_rank,
 -            u.id in ^followers_ids,
 -            u.search_rank,
 -            u.search_rank
 -          )
 -      }
 -    )
 -  end
 -
 -  defp fts_search_subquery(term, query \\ User) do
 -    processed_query =
 -      term
 -      |> String.replace(~r/\W+/, " ")
 -      |> String.trim()
 -      |> String.split()
 -      |> Enum.map(&(&1 <> ":*"))
 -      |> Enum.join(" | ")
 -
 -    from(
 -      u in query,
 -      select_merge: %{
 -        search_type: ^0,
 -        search_rank:
 -          fragment(
 -            """
 -            ts_rank_cd(
 -              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
 -              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
 -              to_tsquery('simple', ?),
 -              32
 -            )
 -            """,
 -            u.nickname,
 -            u.name,
 -            ^processed_query
 -          )
 -      },
 -      where:
 -        fragment(
 -          """
 -            (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
 -            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
 -          """,
 -          u.nickname,
 -          u.name,
 -          ^processed_query
 -        )
 -    )
 -    |> restrict_deactivated()
 -  end
 -
 -  defp trigram_search_subquery(term) do
 -    from(
 -      u in User,
 -      select_merge: %{
 -        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
 -        search_type: fragment("?", 1),
 -        search_rank:
 -          fragment(
 -            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
 -            ^term,
 -            u.nickname,
 -            u.name
 -          )
 -      },
 -      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
 -    )
 -    |> restrict_deactivated()
 -  end
 -
 -  def mute(muter, %User{ap_id: ap_id}) do
      info_cng =
 -      muter.info
 -      |> User.Info.add_to_mutes(ap_id)
 +      User.Info.add_to_mutes(info, ap_id)
 +      |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
  
      cng =
        change(muter)
    end
  
    def unmute(muter, %{ap_id: ap_id}) do
 +    info = muter.info
 +
      info_cng =
 -      muter.info
 -      |> User.Info.remove_from_mutes(ap_id)
 +      User.Info.remove_from_mutes(info, ap_id)
 +      |> User.Info.remove_from_muted_notifications(info, ap_id)
  
      cng =
        change(muter)
          blocker
        end
  
 +    # clear any requested follows as well
 +    blocked =
 +      case CommonAPI.reject_follow_request(blocked, blocker) do
 +        {:ok, %User{} = updated_blocked} -> updated_blocked
 +        nil -> blocked
 +      end
 +
      blocker =
        if subscribed_to?(blocked, blocker) do
          {:ok, blocker} = unsubscribe(blocked, blocker)
    def mutes?(nil, _), do: false
    def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
  
 -  def blocks?(user, %{ap_id: ap_id}) do
 -    blocks = user.info.blocks
 -    domain_blocks = user.info.domain_blocks
 -    %{host: host} = URI.parse(ap_id)
 +  @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
 +  def muted_notifications?(nil, _), do: false
  
 -    Enum.member?(blocks, ap_id) ||
 -      Enum.any?(domain_blocks, fn domain ->
 -        host == domain
 -      end)
 +  def muted_notifications?(user, %{ap_id: ap_id}),
 +    do: Enum.member?(user.info.muted_notifications, ap_id)
 +
 +  def blocks?(%User{} = user, %User{} = target) do
 +    blocks_ap_id?(user, target) || blocks_domain?(user, target)
 +  end
 +
 +  def blocks?(nil, _), do: false
 +
 +  def blocks_ap_id?(%User{} = user, %User{} = target) do
 +    Enum.member?(user.info.blocks, target.ap_id)
 +  end
 +
 +  def blocks_ap_id?(_, _), do: false
 +
 +  def blocks_domain?(%User{} = user, %User{} = target) do
 +    domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
 +    %{host: host} = URI.parse(target.ap_id)
 +    Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
    end
  
 +  def blocks_domain?(_, _), do: false
 +
    def subscribed_to?(user, %{ap_id: ap_id}) do
      with %User{} = target <- get_cached_by_ap_id(ap_id) do
        Enum.member?(target.info.subscribers, user.ap_id)
  
    @spec perform(atom(), User.t()) :: {:ok, User.t()}
    def perform(:delete, %User{} = user) do
 -    {:ok, user} = User.deactivate(user)
 +    {:ok, _user} = ActivityPub.delete(user)
  
      # Remove all relationships
      {:ok, followers} = User.get_followers(user)
  
 -    Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
 +    Enum.each(followers, fn follower ->
 +      ActivityPub.unfollow(follower, user)
 +      User.unfollow(follower, user)
 +    end)
  
      {:ok, friends} = User.get_friends(user)
  
 -    Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
 +    Enum.each(friends, fn followed ->
 +      ActivityPub.unfollow(user, followed)
 +      User.unfollow(user, followed)
 +    end)
  
      delete_user_activities(user)
 +    invalidate_cache(user)
 +    Repo.delete(user)
    end
  
    @spec perform(atom(), User.t()) :: {:ok, User.t()}
      )
    end
  
 +  @spec external_users_query() :: Ecto.Query.t()
 +  def external_users_query do
 +    User.Query.build(%{
 +      external: true,
 +      active: true,
 +      order_by: :id
 +    })
 +  end
 +
 +  @spec external_users(keyword()) :: [User.t()]
 +  def external_users(opts \\ []) do
 +    query =
 +      external_users_query()
 +      |> select([u], struct(u, [:id, :ap_id, :info]))
 +
 +    query =
 +      if opts[:max_id],
 +        do: where(query, [u], u.id > ^opts[:max_id]),
 +        else: query
 +
 +    query =
 +      if opts[:limit],
 +        do: limit(query, ^opts[:limit]),
 +        else: query
 +
 +    Repo.all(query)
 +  end
 +
    def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
      do:
        PleromaJobQueue.enqueue(:background, __MODULE__, [
        ])
  
    def delete_user_activities(%User{ap_id: ap_id} = user) do
 -    stream =
 -      ap_id
 -      |> Activity.query_by_actor()
 -      |> Repo.stream()
 -
 -    Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
 +    ap_id
 +    |> Activity.query_by_actor()
 +    |> RepoStreamer.chunk_stream(50)
 +    |> Stream.each(fn activities ->
 +      Enum.each(activities, &delete_activity(&1))
 +    end)
 +    |> Stream.run()
  
      {:ok, user}
    end
  
    defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
 -    Object.normalize(activity) |> ActivityPub.delete()
 +    activity
 +    |> Object.normalize()
 +    |> ActivityPub.delete()
 +  end
 +
 +  defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
 +    user = get_cached_by_ap_id(activity.actor)
 +    object = Object.normalize(activity)
 +
 +    ActivityPub.unlike(user, object)
 +  end
 +
 +  defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
 +    user = get_cached_by_ap_id(activity.actor)
 +    object = Object.normalize(activity)
 +
 +    ActivityPub.unannounce(user, object)
    end
  
    defp delete_activity(_activity), do: "Doing nothing"
      Pleroma.HTML.Scrubber.TwitterText
    end
  
 -  @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
 -
 -  def html_filter_policy(_), do: @default_scrubbers
 +  def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
  
    def fetch_by_ap_id(ap_id) do
      ap_try = ActivityPub.make_user_from_ap_id(ap_id)
      end
    end
  
 -  def get_or_create_instance_user do
 -    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
 -
 -    if user = get_cached_by_ap_id(relay_uri) do
 +  @doc "Creates an internal service actor by URI if missing.  Optionally takes nickname for addressing."
 +  def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
 +    if user = get_cached_by_ap_id(uri) do
        user
      else
        changes =
          %User{info: %User.Info{}}
          |> cast(%{}, [:ap_id, :nickname, :local])
 -        |> put_change(:ap_id, relay_uri)
 -        |> put_change(:nickname, nil)
 +        |> put_change(:ap_id, uri)
 +        |> put_change(:nickname, nickname)
          |> put_change(:local, true)
 -        |> put_change(:follower_address, relay_uri <> "/followers")
 +        |> put_change(:follower_address, uri <> "/followers")
  
        {:ok, user} = Repo.insert(changes)
        user
    end
  
    # OStatus Magic Key
 -  def public_key_from_info(%{magic_key: magic_key}) do
 +  def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
      {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
    end
  
 +  def public_key_from_info(_), do: {:error, "not found key"}
 +
    def get_public_key_for_ap_id(ap_id) do
      with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
           {:ok, public_key} <- public_key_from_info(user.info) do
      data
      |> Map.put(:name, blank?(data[:name]) || data[:nickname])
      |> remote_user_creation()
 -    |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
 +    |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
      |> set_cache()
    end
  
      target.ap_id not in user.info.muted_reblogs
    end
  
 +  @doc """
 +  The function returns a query to get users with no activity for given interval of days.
 +  Inactive users are those who didn't read any notification, or had any activity where
 +  the user is the activity's actor, during `inactivity_threshold` days.
 +  Deactivated users will not appear in this list.
 +
 +  ## Examples
 +
 +      iex> Pleroma.User.list_inactive_users()
 +      %Ecto.Query{}
 +  """
 +  @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
 +  def list_inactive_users_query(inactivity_threshold \\ 7) do
 +    negative_inactivity_threshold = -inactivity_threshold
 +    now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
 +    # Subqueries are not supported in `where` clauses, join gets too complicated.
 +    has_read_notifications =
 +      from(n in Pleroma.Notification,
 +        where: n.seen == true,
 +        group_by: n.id,
 +        having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
 +        select: n.user_id
 +      )
 +      |> Pleroma.Repo.all()
 +
 +    from(u in Pleroma.User,
 +      left_join: a in Pleroma.Activity,
 +      on: u.ap_id == a.actor,
 +      where: not is_nil(u.nickname),
 +      where: fragment("not (?->'deactivated' @> 'true')", u.info),
 +      where: u.id not in ^has_read_notifications,
 +      group_by: u.id,
 +      having:
 +        max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
 +          is_nil(max(a.inserted_at))
 +    )
 +  end
 +
 +  @doc """
 +  Enable or disable email notifications for user
 +
 +  ## Examples
 +
 +      iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
 +      Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
 +
 +      iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
 +      Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
 +  """
 +  @spec switch_email_notifications(t(), String.t(), boolean()) ::
 +          {:ok, t()} | {:error, Ecto.Changeset.t()}
 +  def switch_email_notifications(user, type, status) do
 +    info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
 +
 +    change(user)
 +    |> put_embed(:info, info)
 +    |> update_and_set_cache()
 +  end
 +
 +  @doc """
 +  Set `last_digest_emailed_at` value for the user to current time
 +  """
 +  @spec touch_last_digest_emailed_at(t()) :: t()
 +  def touch_last_digest_emailed_at(user) do
 +    now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
 +
 +    {:ok, updated_user} =
 +      user
 +      |> change(%{last_digest_emailed_at: now})
 +      |> update_and_set_cache()
 +
 +    updated_user
 +  end
 +
    @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
    def toggle_confirmation(%User{} = user) do
      need_confirmation? = !user.info.confirmation_pending
      }
    end
  
 -  def ensure_keys_present(user) do
 -    info = user.info
 -
 +  def ensure_keys_present(%User{info: info} = user) do
      if info.keys do
        {:ok, user}
      else
        {:ok, pem} = Keys.generate_rsa_pem()
  
 -      info_cng =
 -        info
 -        |> User.Info.set_keys(pem)
 +      user
 +      |> Ecto.Changeset.change()
 +      |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
 +      |> update_and_set_cache()
 +    end
 +  end
  
 -      cng =
 -        Ecto.Changeset.change(user)
 -        |> Ecto.Changeset.put_embed(:info, info_cng)
 +  def get_ap_ids_by_nicknames(nicknames) do
 +    from(u in User,
 +      where: u.nickname in ^nicknames,
 +      select: u.ap_id
 +    )
 +    |> Repo.all()
 +  end
  
 -      update_and_set_cache(cng)
 -    end
 +  defdelegate search(query, opts \\ []), to: User.Search
 +
 +  defp put_password_hash(
 +         %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
 +       ) do
 +    change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
    end
 +
 +  defp put_password_hash(changeset), do: changeset
 +
 +  def is_internal_user?(%User{nickname: nil}), do: true
 +  def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
 +  def is_internal_user?(_), do: false
  end
index 2d3d0adc44f63d8fd9a7c3dcafb71778f4f44207,479fd5829087aee42db457af8195e066135d93ac..048ac80198320cee630adf7b1c73f4fa4f7e322b
@@@ -10,8 -10,6 +10,8 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Relay
    alias Pleroma.Web.AdminAPI.AccountView
 +  alias Pleroma.Web.AdminAPI.Config
 +  alias Pleroma.Web.AdminAPI.ConfigView
    alias Pleroma.Web.AdminAPI.ReportView
    alias Pleroma.Web.AdminAPI.Search
    alias Pleroma.Web.CommonAPI
      |> json("ok")
    end
  
-   def user_create(
-         conn,
-         %{"nickname" => nickname, "email" => email, "password" => password}
-       ) do
-     user_data = %{
-       nickname: nickname,
-       name: nickname,
-       email: email,
-       password: password,
-       password_confirmation: password,
-       bio: "."
-     }
-     changeset = User.register_changeset(%User{}, user_data, need_confirmation: false)
-     {:ok, user} = User.register(changeset)
-     conn
-     |> json(user.nickname)
+   def users_create(conn, %{"users" => users}) do
+     changesets =
+       Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
+         user_data = %{
+           nickname: nickname,
+           name: nickname,
+           email: email,
+           password: password,
+           password_confirmation: password,
+           bio: "."
+         }
+         User.register_changeset(%User{}, user_data, need_confirmation: false)
+       end)
+       |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
+         Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
+       end)
+     case Pleroma.Repo.transaction(changesets) do
+       {:ok, users} ->
+         res =
+           users
+           |> Map.values()
+           |> Enum.map(fn user ->
+             {:ok, user} = User.post_register_action(user)
+             user
+           end)
+           |> Enum.map(&AccountView.render("created.json", %{user: &1}))
+         conn
+         |> json(res)
+       {:error, id, changeset, _} ->
+         res =
+           Enum.map(changesets.operations, fn
+             {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
+               AccountView.render("create-error.json", %{changeset: changeset})
+             {_, {:changeset, current_changeset, _}} ->
+               AccountView.render("create-error.json", %{changeset: current_changeset})
+           end)
+         conn
+         |> put_status(:conflict)
+         |> json(res)
+     end
    end
  
    def user_show(conn, %{"nickname" => nickname}) do
 -    with %User{} = user <- User.get_cached_by_nickname(nickname) do
 +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
        conn
        |> json(AccountView.render("show.json", %{user: user}))
      else
      end
    end
  
 +  def list_user_statuses(conn, %{"nickname" => nickname} = params) do
 +    godmode = params["godmode"] == "true" || params["godmode"] == true
 +
 +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
 +      {_, page_size} = page_params(params)
 +
 +      activities =
 +        ActivityPub.fetch_user_activities(user, nil, %{
 +          "limit" => page_size,
 +          "godmode" => godmode
 +        })
 +
 +      conn
 +      |> json(StatusView.render("index.json", %{activities: activities, as: :activity}))
 +    else
 +      _ -> {:error, :not_found}
 +    end
 +  end
 +
    def user_toggle_activation(conn, %{"nickname" => nickname}) do
      user = User.get_cached_by_nickname(nickname)
  
    end
  
    def right_add(conn, _) do
 -    conn
 -    |> put_status(404)
 -    |> json(%{error: "No such permission_group"})
 +    render_error(conn, :not_found, "No such permission_group")
    end
  
    def right_get(conn, %{"nickname" => nickname}) do
        )
        when permission_group in ["moderator", "admin"] do
      if admin_nickname == nickname do
 -      conn
 -      |> put_status(403)
 -      |> json(%{error: "You can't revoke your own admin status."})
 +      render_error(conn, :forbidden, "You can't revoke your own admin status.")
      else
        user = User.get_cached_by_nickname(nickname)
  
    end
  
    def right_delete(conn, _) do
 -    conn
 -    |> put_status(404)
 -    |> json(%{error: "No such permission_group"})
 +    render_error(conn, :not_found, "No such permission_group")
    end
  
    def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
  
    @doc "Revokes invite by token"
    def revoke_invite(conn, %{"token" => token}) do
 -    invite = UserInviteToken.find_by_token!(token)
 -    {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true})
 -
 -    conn
 -    |> json(AccountView.render("invite.json", %{invite: updated_invite}))
 +    with {:ok, invite} <- UserInviteToken.find_by_token(token),
 +         {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
 +      conn
 +      |> json(AccountView.render("invite.json", %{invite: updated_invite}))
 +    else
 +      nil -> {:error, :not_found}
 +    end
    end
  
    @doc "Get a password reset token (base64 string) for given nickname"
      end
    end
  
 +  def migrate_to_db(conn, _params) do
 +    Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
 +    json(conn, %{})
 +  end
 +
 +  def migrate_from_db(conn, _params) do
 +    Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
 +    json(conn, %{})
 +  end
 +
 +  def config_show(conn, _params) do
 +    configs = Pleroma.Repo.all(Config)
 +
 +    conn
 +    |> put_view(ConfigView)
 +    |> render("index.json", %{configs: configs})
 +  end
 +
 +  def config_update(conn, %{"configs" => configs}) do
 +    updated =
 +      if Pleroma.Config.get([:instance, :dynamic_configuration]) do
 +        updated =
 +          Enum.map(configs, fn
 +            %{"group" => group, "key" => key, "delete" => "true"} = params ->
 +              {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
 +              config
 +
 +            %{"group" => group, "key" => key, "value" => value} ->
 +              {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
 +              config
 +          end)
 +          |> Enum.reject(&is_nil(&1))
 +
 +        Pleroma.Config.TransferTask.load_and_update_env()
 +        Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
 +        updated
 +      else
 +        []
 +      end
 +
 +    conn
 +    |> put_view(ConfigView)
 +    |> render("index.json", %{configs: updated})
 +  end
 +
    def errors(conn, {:error, :not_found}) do
      conn
 -    |> put_status(404)
 -    |> json("Not found")
 +    |> put_status(:not_found)
 +    |> json(dgettext("errors", "Not found"))
    end
  
    def errors(conn, {:error, reason}) do
      conn
 -    |> put_status(400)
 +    |> put_status(:bad_request)
      |> json(reason)
    end
  
    def errors(conn, {:param_cast, _}) do
      conn
 -    |> put_status(400)
 -    |> json("Invalid parameters")
 +    |> put_status(:bad_request)
 +    |> json(dgettext("errors", "Invalid parameters"))
    end
  
    def errors(conn, _) do
      conn
 -    |> put_status(500)
 -    |> json("Something went wrong")
 +    |> put_status(:internal_server_error)
 +    |> json(dgettext("errors", "Something went wrong"))
    end
  
    defp page_params(params) do
index 7e1b9c431bfbdd36c2ea6b631e8822f84e47b812,cccdeff7e93f1abe4c66043fd5d487b4b612c348..a96affd40cbe4800b0b213c287144688f505ac62
@@@ -5,11 -5,8 +5,11 @@@
  defmodule Pleroma.Web.AdminAPI.AccountView do
    use Pleroma.Web, :view
  
 +  alias Pleroma.HTML
 +  alias Pleroma.User
    alias Pleroma.User.Info
    alias Pleroma.Web.AdminAPI.AccountView
 +  alias Pleroma.Web.MediaProxy
  
    def render("index.json", %{users: users, count: count, page_size: page_size}) do
      %{
    end
  
    def render("show.json", %{user: user}) do
 +    avatar = User.avatar_url(user) |> MediaProxy.url()
 +    display_name = HTML.strip_tags(user.name || user.nickname)
 +
      %{
        "id" => user.id,
 +      "avatar" => avatar,
        "nickname" => user.nickname,
 +      "display_name" => display_name,
        "deactivated" => user.info.deactivated,
        "local" => user.local,
        "roles" => Info.roles(user.info),
        invites: render_many(invites, AccountView, "invite.json", as: :invite)
      }
    end
+   def render("created.json", %{user: user}) do
+     %{
+       type: "success",
+       code: 200,
+       data: %{
+         nickname: user.nickname,
+         email: user.email
+       }
+     }
+   end
+   def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do
+     %{
+       type: "error",
+       code: 409,
+       error: parse_error(errors),
+       data: %{
+         nickname: Map.get(changes, :nickname),
+         email: Map.get(changes, :email)
+       }
+     }
+   end
+   defp parse_error([]), do: ""
+   defp parse_error(errors) do
+     ## when nickname is duplicate ap_id constraint error is raised
+     nickname_error = Keyword.get(errors, :nickname) || Keyword.get(errors, :ap_id)
+     email_error = Keyword.get(errors, :email)
+     password_error = Keyword.get(errors, :password)
+     cond do
+       nickname_error ->
+         "nickname #{elem(nickname_error, 0)}"
+       email_error ->
+         "email #{elem(email_error, 0)}"
+       password_error ->
+         "password #{elem(password_error, 0)}"
+       true ->
+         ""
+     end
+   end
  end
index 1eb6f7b9dedc452a298df3802ee7a24163436c28,445cf62e2d5f3f59fd8cebbeea1c999bd807b42b..97c5016d5cd3fda01db750b609e0a3eaf7f84013
@@@ -27,7 -27,6 +27,7 @@@ defmodule Pleroma.Web.Router d
      plug(Pleroma.Plugs.UserEnabledPlug)
      plug(Pleroma.Plugs.SetUserSessionIdPlug)
      plug(Pleroma.Plugs.EnsureUserKeyPlug)
 +    plug(Pleroma.Plugs.IdempotencyPlug)
    end
  
    pipeline :authenticated_api do
@@@ -42,7 -41,6 +42,7 @@@
      plug(Pleroma.Plugs.UserEnabledPlug)
      plug(Pleroma.Plugs.SetUserSessionIdPlug)
      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
 +    plug(Pleroma.Plugs.IdempotencyPlug)
    end
  
    pipeline :admin_api do
@@@ -59,7 -57,6 +59,7 @@@
      plug(Pleroma.Plugs.SetUserSessionIdPlug)
      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
      plug(Pleroma.Plugs.UserIsAdminPlug)
 +    plug(Pleroma.Plugs.IdempotencyPlug)
    end
  
    pipeline :mastodon_html do
    scope "/api/pleroma", Pleroma.Web.TwitterAPI do
      pipe_through(:pleroma_api)
  
 -    get("/password_reset/:token", UtilController, :show_password_reset)
 -    post("/password_reset", UtilController, :password_reset)
 +    get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
 +    post("/password_reset", PasswordController, :do_reset, as: :reset_password)
      get("/emoji", UtilController, :emoji)
      get("/captcha", UtilController, :captcha)
      get("/healthcheck", UtilController, :healthcheck)
      post("/users/unfollow", AdminAPIController, :user_unfollow)
  
      delete("/users", AdminAPIController, :user_delete)
-     post("/users", AdminAPIController, :user_create)
+     post("/users", AdminAPIController, :users_create)
      patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
      put("/users/tag", AdminAPIController, :tag_users)
      delete("/users/tag", AdminAPIController, :untag_users)
  
 -    # TODO: to be removed at version 1.0
 -    get("/permission_group/:nickname", AdminAPIController, :right_get)
 -    get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
 -    post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
 -    delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
 -
      get("/users/:nickname/permission_group", AdminAPIController, :right_get)
      get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
      post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
      post("/users/revoke_invite", AdminAPIController, :revoke_invite)
      post("/users/email_invite", AdminAPIController, :email_invite)
  
 -    # TODO: to be removed at version 1.0
 -    get("/password_reset", AdminAPIController, :get_password_reset)
 -
      get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
  
      get("/users", AdminAPIController, :list_users)
      get("/users/:nickname", AdminAPIController, :user_show)
 +    get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
  
      get("/reports", AdminAPIController, :list_reports)
      get("/reports/:id", AdminAPIController, :report_show)
  
      put("/statuses/:id", AdminAPIController, :status_update)
      delete("/statuses/:id", AdminAPIController, :status_delete)
 +
 +    get("/config", AdminAPIController, :config_show)
 +    post("/config", AdminAPIController, :config_update)
 +    get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
 +    get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
    end
  
    scope "/", Pleroma.Web.TwitterAPI do
      end
    end
  
 +  scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
 +    pipe_through(:authenticated_api)
 +
 +    scope [] do
 +      pipe_through(:oauth_read)
 +      get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
 +      get("/conversations/:id", PleromaAPIController, :conversation)
 +    end
 +
 +    scope [] do
 +      pipe_through(:oauth_write)
 +      patch("/conversations/:id", PleromaAPIController, :update_conversation)
 +    end
 +  end
 +
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:authenticated_api)
  
        post("/conversations/:id/read", MastodonAPIController, :conversation_read)
  
        get("/endorsements", MastodonAPIController, :empty_array)
 -
 -      get("/pleroma/flavour", MastodonAPIController, :get_flavour)
      end
  
      scope [] do
        put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
        delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
  
 +      post("/polls/:id/votes", MastodonAPIController, :poll_vote)
 +
        post("/media", MastodonAPIController, :upload)
        put("/media/:id", MastodonAPIController, :update_media)
  
        put("/filters/:id", MastodonAPIController, :update_filter)
        delete("/filters/:id", MastodonAPIController, :delete_filter)
  
 -      post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
 +      patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
 +      patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
 +      patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)
  
        get("/pleroma/mascot", MastodonAPIController, :get_mascot)
        put("/pleroma/mascot", MastodonAPIController, :set_mascot)
  
      get("/trends", MastodonAPIController, :empty_array)
  
 -    get("/accounts/search", MastodonAPIController, :account_search)
 +    get("/accounts/search", SearchController, :account_search)
 +
 +    post(
 +      "/pleroma/accounts/confirmation_resend",
 +      MastodonAPIController,
 +      :account_confirmation_resend
 +    )
  
      scope [] do
        pipe_through(:oauth_read_or_public)
        get("/statuses/:id", MastodonAPIController, :get_status)
        get("/statuses/:id/context", MastodonAPIController, :get_context)
  
 +      get("/polls/:id", MastodonAPIController, :get_poll)
 +
        get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
        get("/accounts/:id/followers", MastodonAPIController, :followers)
        get("/accounts/:id/following", MastodonAPIController, :following)
        get("/accounts/:id", MastodonAPIController, :user)
  
 -      get("/search", MastodonAPIController, :search)
 +      get("/search", SearchController, :search)
  
        get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
      end
  
    scope "/api/v2", Pleroma.Web.MastodonAPI do
      pipe_through([:api, :oauth_read_or_public])
 -    get("/search", MastodonAPIController, :search2)
 +    get("/search", SearchController, :search2)
    end
  
    scope "/api", Pleroma.Web do
      end
    end
  
 -  pipeline :ap_relay do
 +  pipeline :ap_service_actor do
      plug(:accepts, ["activity+json", "json"])
    end
  
      post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
      get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
      post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
 -  end
 -
 -  scope "/", Pleroma.Web do
 -    pipe_through(:oembed)
  
 -    get("/oembed", OEmbed.OEmbedController, :url)
 +    get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
    end
  
    pipeline :activitypub do
      plug(:accepts, ["activity+json", "json"])
      plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
 +    plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
    end
  
    scope "/", Pleroma.Web.ActivityPub do
      # XXX: not really ostatus
      pipe_through(:ostatus)
  
 -    get("/users/:nickname/followers", ActivityPubController, :followers)
 -    get("/users/:nickname/following", ActivityPubController, :following)
      get("/users/:nickname/outbox", ActivityPubController, :outbox)
      get("/objects/:uuid/likes", ActivityPubController, :object_likes)
    end
        pipe_through(:oauth_write)
        post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
      end
 -  end
  
 -  scope "/relay", Pleroma.Web.ActivityPub do
 -    pipe_through(:ap_relay)
 -    get("/", ActivityPubController, :relay)
 +    scope [] do
 +      pipe_through(:oauth_read_or_public)
 +      get("/users/:nickname/followers", ActivityPubController, :followers)
 +      get("/users/:nickname/following", ActivityPubController, :following)
 +    end
    end
  
    scope "/", Pleroma.Web.ActivityPub do
      post("/users/:nickname/inbox", ActivityPubController, :inbox)
    end
  
 +  scope "/relay", Pleroma.Web.ActivityPub do
 +    pipe_through(:ap_service_actor)
 +
 +    get("/", ActivityPubController, :relay)
 +    post("/inbox", ActivityPubController, :inbox)
 +  end
 +
 +  scope "/internal/fetch", Pleroma.Web.ActivityPub do
 +    pipe_through(:ap_service_actor)
 +
 +    get("/", ActivityPubController, :internal_fetch)
 +    post("/inbox", ActivityPubController, :inbox)
 +  end
 +
    scope "/.well-known", Pleroma.Web do
      pipe_through(:well_known)
  
      get("/web/login", MastodonAPIController, :login)
      delete("/auth/sign_out", MastodonAPIController, :logout)
  
 +    post("/auth/password", MastodonAPIController, :password_reset)
 +
      scope [] do
 -      pipe_through(:oauth_read_or_public)
 +      pipe_through(:oauth_read)
        get("/web/*path", MastodonAPIController, :index)
      end
    end
      get("/:sig/:url/:filename", MediaProxyController, :remote)
    end
  
 -  if Mix.env() == :dev do
 +  if Pleroma.Config.get(:env) == :dev do
      scope "/dev" do
        pipe_through([:mailbox_preview])
  
      options("/*path", RedirectController, :empty)
    end
  end
 -
 -defmodule Fallback.RedirectController do
 -  use Pleroma.Web, :controller
 -  alias Pleroma.User
 -  alias Pleroma.Web.Metadata
 -
 -  def api_not_implemented(conn, _params) do
 -    conn
 -    |> put_status(404)
 -    |> json(%{error: "Not implemented"})
 -  end
 -
 -  def redirector(conn, _params, code \\ 200) do
 -    conn
 -    |> put_resp_content_type("text/html")
 -    |> send_file(code, index_file_path())
 -  end
 -
 -  def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
 -    with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
 -      redirector_with_meta(conn, %{user: user})
 -    else
 -      nil ->
 -        redirector(conn, params)
 -    end
 -  end
 -
 -  def redirector_with_meta(conn, params) do
 -    {:ok, index_content} = File.read(index_file_path())
 -    tags = Metadata.build_tags(params)
 -    response = String.replace(index_content, "<!--server-generated-meta-->", tags)
 -
 -    conn
 -    |> put_resp_content_type("text/html")
 -    |> send_resp(200, response)
 -  end
 -
 -  def index_file_path do
 -    Pleroma.Plugs.InstanceStatic.file_path("index.html")
 -  end
 -
 -  def registration_page(conn, params) do
 -    redirector(conn, params)
 -  end
 -
 -  def empty(conn, _params) do
 -    conn
 -    |> put_status(204)
 -    |> text("")
 -  end
 -end
index 844cd07324605abbe7e89b5224bdea0b1873d55c,86b16024609bc7ede6051bfe1302b39c6e57f2ae..ab829d6bdb78beff3d24615d21903eb056dab9a4
@@@ -6,11 -6,9 +6,11 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    use Pleroma.Web.ConnCase
  
    alias Pleroma.Activity
 +  alias Pleroma.HTML
    alias Pleroma.User
    alias Pleroma.UserInviteToken
    alias Pleroma.Web.CommonAPI
 +  alias Pleroma.Web.MediaProxy
    import Pleroma.Factory
  
    describe "/api/pleroma/admin/users" do
          |> assign(:user, admin)
          |> put_req_header("accept", "application/json")
          |> post("/api/pleroma/admin/users", %{
-           "nickname" => "lain",
-           "email" => "lain@example.org",
-           "password" => "test"
+           "users" => [
+             %{
+               "nickname" => "lain",
+               "email" => "lain@example.org",
+               "password" => "test"
+             },
+             %{
+               "nickname" => "lain2",
+               "email" => "lain2@example.org",
+               "password" => "test"
+             }
+           ]
          })
  
-       assert json_response(conn, 200) == "lain"
+       response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
+       assert response == ["success", "success"]
+     end
+     test "Cannot create user with exisiting email" do
+       admin = insert(:user, info: %{is_admin: true})
+       user = insert(:user)
+       conn =
+         build_conn()
+         |> assign(:user, admin)
+         |> put_req_header("accept", "application/json")
+         |> post("/api/pleroma/admin/users", %{
+           "users" => [
+             %{
+               "nickname" => "lain",
+               "email" => user.email,
+               "password" => "test"
+             }
+           ]
+         })
+       assert json_response(conn, 409) == [
+                %{
+                  "code" => 409,
+                  "data" => %{
+                    "email" => user.email,
+                    "nickname" => "lain"
+                  },
+                  "error" => "email has already been taken",
+                  "type" => "error"
+                }
+              ]
+     end
+     test "Cannot create user with exisiting nickname" do
+       admin = insert(:user, info: %{is_admin: true})
+       user = insert(:user)
+       conn =
+         build_conn()
+         |> assign(:user, admin)
+         |> put_req_header("accept", "application/json")
+         |> post("/api/pleroma/admin/users", %{
+           "users" => [
+             %{
+               "nickname" => user.nickname,
+               "email" => "someuser@plerama.social",
+               "password" => "test"
+             }
+           ]
+         })
+       assert json_response(conn, 409) == [
+                %{
+                  "code" => 409,
+                  "data" => %{
+                    "email" => "someuser@plerama.social",
+                    "nickname" => user.nickname
+                  },
+                  "error" => "nickname has already been taken",
+                  "type" => "error"
+                }
+              ]
+     end
+     test "Multiple user creation works in transaction" do
+       admin = insert(:user, info: %{is_admin: true})
+       user = insert(:user)
+       conn =
+         build_conn()
+         |> assign(:user, admin)
+         |> put_req_header("accept", "application/json")
+         |> post("/api/pleroma/admin/users", %{
+           "users" => [
+             %{
+               "nickname" => "newuser",
+               "email" => "newuser@pleroma.social",
+               "password" => "test"
+             },
+             %{
+               "nickname" => "lain",
+               "email" => user.email,
+               "password" => "test"
+             }
+           ]
+         })
+       assert json_response(conn, 409) == [
+                %{
+                  "code" => 409,
+                  "data" => %{
+                    "email" => user.email,
+                    "nickname" => "lain"
+                  },
+                  "error" => "email has already been taken",
+                  "type" => "error"
+                },
+                %{
+                  "code" => 409,
+                  "data" => %{
+                    "email" => "newuser@pleroma.social",
+                    "nickname" => "newuser"
+                  },
+                  "error" => "",
+                  "type" => "error"
+                }
+              ]
+       assert User.get_by_nickname("newuser") === nil
      end
    end
  
          "local" => true,
          "nickname" => user.nickname,
          "roles" => %{"admin" => false, "moderator" => false},
 -        "tags" => []
 +        "tags" => [],
 +        "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +        "display_name" => HTML.strip_tags(user.name || user.nickname)
        }
  
        assert expected == json_response(conn, 200)
  
    describe "POST /api/pleroma/admin/email_invite, with valid config" do
      setup do
 -      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
 -      invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
 -      Pleroma.Config.put([:instance, :registrations_open], false)
 -      Pleroma.Config.put([:instance, :invites_enabled], true)
 +      [user: insert(:user, info: %{is_admin: true})]
 +    end
  
 -      on_exit(fn ->
 -        Pleroma.Config.put([:instance, :registrations_open], registrations_open)
 -        Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
 -        :ok
 -      end)
 +    clear_config([:instance, :registrations_open]) do
 +      Pleroma.Config.put([:instance, :registrations_open], false)
 +    end
  
 -      [user: insert(:user, info: %{is_admin: true})]
 +    clear_config([:instance, :invites_enabled]) do
 +      Pleroma.Config.put([:instance, :invites_enabled], true)
      end
  
      test "sends invitation and returns 204", %{conn: conn, user: user} do
        [user: insert(:user, info: %{is_admin: true})]
      end
  
 +    clear_config([:instance, :registrations_open])
 +    clear_config([:instance, :invites_enabled])
 +
      test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
 -      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
 -      invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
        Pleroma.Config.put([:instance, :registrations_open], false)
        Pleroma.Config.put([:instance, :invites_enabled], false)
  
 -      on_exit(fn ->
 -        Pleroma.Config.put([:instance, :registrations_open], registrations_open)
 -        Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
 -        :ok
 -      end)
 -
        conn =
          conn
          |> assign(:user, user)
      end
  
      test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
 -      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
 -      invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
        Pleroma.Config.put([:instance, :registrations_open], true)
        Pleroma.Config.put([:instance, :invites_enabled], true)
  
 -      on_exit(fn ->
 -        Pleroma.Config.put([:instance, :registrations_open], registrations_open)
 -        Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
 -        :ok
 -      end)
 -
        conn =
          conn
          |> assign(:user, user)
              "nickname" => admin.nickname,
              "roles" => %{"admin" => true, "moderator" => false},
              "local" => true,
 -            "tags" => []
 +            "tags" => [],
 +            "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
            },
            %{
              "deactivated" => user.info.deactivated,
              "nickname" => user.nickname,
              "roles" => %{"admin" => false, "moderator" => false},
              "local" => false,
 -            "tags" => ["foo", "bar"]
 +            "tags" => ["foo", "bar"],
 +            "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(user.name || user.nickname)
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                     "nickname" => user2.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user2.name || user2.nickname)
                   }
                 ]
               }
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => true,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
              "nickname" => user.nickname,
              "roles" => %{"admin" => false, "moderator" => false},
              "local" => true,
 -            "tags" => []
 +            "tags" => [],
 +            "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(user.name || user.nickname)
            },
            %{
              "deactivated" => admin.info.deactivated,
              "nickname" => admin.nickname,
              "roles" => %{"admin" => true, "moderator" => false},
              "local" => true,
 -            "tags" => []
 +            "tags" => [],
 +            "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
            },
            %{
              "deactivated" => false,
              "local" => true,
              "nickname" => old_admin.nickname,
              "roles" => %{"admin" => true, "moderator" => false},
 -            "tags" => []
 +            "tags" => [],
 +            "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname)
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
              "nickname" => admin.nickname,
              "roles" => %{"admin" => true, "moderator" => false},
              "local" => admin.local,
 -            "tags" => []
 +            "tags" => [],
 +            "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
            },
            %{
              "deactivated" => false,
              "nickname" => second_admin.nickname,
              "roles" => %{"admin" => true, "moderator" => false},
              "local" => second_admin.local,
 -            "tags" => []
 +            "tags" => [],
 +            "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname)
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
                     "nickname" => moderator.nickname,
                     "roles" => %{"admin" => false, "moderator" => true},
                     "local" => moderator.local,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(moderator.name || moderator.nickname)
                   }
                 ]
               }
              "nickname" => user1.nickname,
              "roles" => %{"admin" => false, "moderator" => false},
              "local" => user1.local,
 -            "tags" => ["first"]
 +            "tags" => ["first"],
 +            "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(user1.name || user1.nickname)
            },
            %{
              "deactivated" => false,
              "nickname" => user2.nickname,
              "roles" => %{"admin" => false, "moderator" => false},
              "local" => user2.local,
 -            "tags" => ["second"]
 +            "tags" => ["second"],
 +            "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
 +            "display_name" => HTML.strip_tags(user2.name || user2.nickname)
            }
          ]
          |> Enum.sort_by(& &1["nickname"])
                     "nickname" => user.nickname,
                     "roles" => %{"admin" => false, "moderator" => false},
                     "local" => user.local,
 -                   "tags" => []
 +                   "tags" => [],
 +                   "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +                   "display_name" => HTML.strip_tags(user.name || user.nickname)
                   }
                 ]
               }
                 "nickname" => user.nickname,
                 "roles" => %{"admin" => false, "moderator" => false},
                 "local" => true,
 -               "tags" => []
 +               "tags" => [],
 +               "avatar" => User.avatar_url(user) |> MediaProxy.url(),
 +               "display_name" => HTML.strip_tags(user.name || user.nickname)
               }
    end
  
                 "uses" => 0
               }
      end
 +
 +    test "with invalid token" do
 +      admin = insert(:user, info: %{is_admin: true})
 +
 +      conn =
 +        build_conn()
 +        |> assign(:user, admin)
 +        |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
 +
 +      assert json_response(conn, :not_found) == "Not found"
 +    end
    end
  
    describe "GET /api/pleroma/admin/reports/:id" do
  
        recipients = Enum.map(response["mentions"], & &1["username"])
  
 -      assert conn.assigns[:user].nickname in recipients
        assert reporter.nickname in recipients
        assert response["content"] == "I will check it out"
        assert response["visibility"] == "direct"
        assert json_response(conn, :bad_request) == "Could not delete"
      end
    end
 +
 +  describe "GET /api/pleroma/admin/config" do
 +    setup %{conn: conn} do
 +      admin = insert(:user, info: %{is_admin: true})
 +
 +      %{conn: assign(conn, :user, admin)}
 +    end
 +
 +    test "without any settings in db", %{conn: conn} do
 +      conn = get(conn, "/api/pleroma/admin/config")
 +
 +      assert json_response(conn, 200) == %{"configs" => []}
 +    end
 +
 +    test "with settings in db", %{conn: conn} do
 +      config1 = insert(:config)
 +      config2 = insert(:config)
 +
 +      conn = get(conn, "/api/pleroma/admin/config")
 +
 +      %{
 +        "configs" => [
 +          %{
 +            "key" => key1,
 +            "value" => _
 +          },
 +          %{
 +            "key" => key2,
 +            "value" => _
 +          }
 +        ]
 +      } = json_response(conn, 200)
 +
 +      assert key1 == config1.key
 +      assert key2 == config2.key
 +    end
 +  end
 +
 +  describe "POST /api/pleroma/admin/config" do
 +    setup %{conn: conn} do
 +      admin = insert(:user, info: %{is_admin: true})
 +
 +      temp_file = "config/test.exported_from_db.secret.exs"
 +
 +      on_exit(fn ->
 +        Application.delete_env(:pleroma, :key1)
 +        Application.delete_env(:pleroma, :key2)
 +        Application.delete_env(:pleroma, :key3)
 +        Application.delete_env(:pleroma, :key4)
 +        Application.delete_env(:pleroma, :keyaa1)
 +        Application.delete_env(:pleroma, :keyaa2)
 +        Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
 +        Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
 +        :ok = File.rm(temp_file)
 +      end)
 +
 +      %{conn: assign(conn, :user, admin)}
 +    end
 +
 +    clear_config([:instance, :dynamic_configuration]) do
 +      Pleroma.Config.put([:instance, :dynamic_configuration], true)
 +    end
 +
 +    test "create new config setting in db", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{group: "pleroma", key: "key1", value: "value1"},
 +            %{
 +              group: "ueberauth",
 +              key: "Ueberauth.Strategy.Twitter.OAuth",
 +              value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
 +            },
 +            %{
 +              group: "pleroma",
 +              key: "key2",
 +              value: %{
 +                ":nested_1" => "nested_value1",
 +                ":nested_2" => [
 +                  %{":nested_22" => "nested_value222"},
 +                  %{":nested_33" => %{":nested_44" => "nested_444"}}
 +                ]
 +              }
 +            },
 +            %{
 +              group: "pleroma",
 +              key: "key3",
 +              value: [
 +                %{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
 +                %{"nested_4" => true}
 +              ]
 +            },
 +            %{
 +              group: "pleroma",
 +              key: "key4",
 +              value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
 +            },
 +            %{
 +              group: "idna",
 +              key: "key5",
 +              value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) == %{
 +               "configs" => [
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "key1",
 +                   "value" => "value1"
 +                 },
 +                 %{
 +                   "group" => "ueberauth",
 +                   "key" => "Ueberauth.Strategy.Twitter.OAuth",
 +                   "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}]
 +                 },
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "key2",
 +                   "value" => %{
 +                     ":nested_1" => "nested_value1",
 +                     ":nested_2" => [
 +                       %{":nested_22" => "nested_value222"},
 +                       %{":nested_33" => %{":nested_44" => "nested_444"}}
 +                     ]
 +                   }
 +                 },
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "key3",
 +                   "value" => [
 +                     %{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
 +                     %{"nested_4" => true}
 +                   ]
 +                 },
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "key4",
 +                   "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}
 +                 },
 +                 %{
 +                   "group" => "idna",
 +                   "key" => "key5",
 +                   "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
 +                 }
 +               ]
 +             }
 +
 +      assert Application.get_env(:pleroma, :key1) == "value1"
 +
 +      assert Application.get_env(:pleroma, :key2) == %{
 +               nested_1: "nested_value1",
 +               nested_2: [
 +                 %{nested_22: "nested_value222"},
 +                 %{nested_33: %{nested_44: "nested_444"}}
 +               ]
 +             }
 +
 +      assert Application.get_env(:pleroma, :key3) == [
 +               %{"nested_3" => :nested_3, "nested_33" => "nested_33"},
 +               %{"nested_4" => true}
 +             ]
 +
 +      assert Application.get_env(:pleroma, :key4) == %{
 +               "endpoint" => "https://example.com",
 +               nested_5: :upload
 +             }
 +
 +      assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
 +    end
 +
 +    test "update config setting & delete", %{conn: conn} do
 +      config1 = insert(:config, key: "keyaa1")
 +      config2 = insert(:config, key: "keyaa2")
 +
 +      insert(:config,
 +        group: "ueberauth",
 +        key: "Ueberauth.Strategy.Microsoft.OAuth",
 +        value: :erlang.term_to_binary([])
 +      )
 +
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{group: config1.group, key: config1.key, value: "another_value"},
 +            %{group: config2.group, key: config2.key, delete: "true"},
 +            %{
 +              group: "ueberauth",
 +              key: "Ueberauth.Strategy.Microsoft.OAuth",
 +              delete: "true"
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) == %{
 +               "configs" => [
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => config1.key,
 +                   "value" => "another_value"
 +                 }
 +               ]
 +             }
 +
 +      assert Application.get_env(:pleroma, :keyaa1) == "another_value"
 +      refute Application.get_env(:pleroma, :keyaa2)
 +    end
 +
 +    test "common config example", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              "group" => "pleroma",
 +              "key" => "Pleroma.Captcha.NotReal",
 +              "value" => [
 +                %{"tuple" => [":enabled", false]},
 +                %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
 +                %{"tuple" => [":seconds_valid", 60]},
 +                %{"tuple" => [":path", ""]},
 +                %{"tuple" => [":key1", nil]},
 +                %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}
 +              ]
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) == %{
 +               "configs" => [
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "Pleroma.Captcha.NotReal",
 +                   "value" => [
 +                     %{"tuple" => [":enabled", false]},
 +                     %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
 +                     %{"tuple" => [":seconds_valid", 60]},
 +                     %{"tuple" => [":path", ""]},
 +                     %{"tuple" => [":key1", nil]},
 +                     %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}
 +                   ]
 +                 }
 +               ]
 +             }
 +    end
 +
 +    test "tuples with more than two values", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              "group" => "pleroma",
 +              "key" => "Pleroma.Web.Endpoint.NotReal",
 +              "value" => [
 +                %{
 +                  "tuple" => [
 +                    ":http",
 +                    [
 +                      %{
 +                        "tuple" => [
 +                          ":key2",
 +                          [
 +                            %{
 +                              "tuple" => [
 +                                ":_",
 +                                [
 +                                  %{
 +                                    "tuple" => [
 +                                      "/api/v1/streaming",
 +                                      "Pleroma.Web.MastodonAPI.WebsocketHandler",
 +                                      []
 +                                    ]
 +                                  },
 +                                  %{
 +                                    "tuple" => [
 +                                      "/websocket",
 +                                      "Phoenix.Endpoint.CowboyWebSocket",
 +                                      %{
 +                                        "tuple" => [
 +                                          "Phoenix.Transports.WebSocket",
 +                                          %{
 +                                            "tuple" => [
 +                                              "Pleroma.Web.Endpoint",
 +                                              "Pleroma.Web.UserSocket",
 +                                              []
 +                                            ]
 +                                          }
 +                                        ]
 +                                      }
 +                                    ]
 +                                  },
 +                                  %{
 +                                    "tuple" => [
 +                                      ":_",
 +                                      "Phoenix.Endpoint.Cowboy2Handler",
 +                                      %{"tuple" => ["Pleroma.Web.Endpoint", []]}
 +                                    ]
 +                                  }
 +                                ]
 +                              ]
 +                            }
 +                          ]
 +                        ]
 +                      }
 +                    ]
 +                  ]
 +                }
 +              ]
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) == %{
 +               "configs" => [
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "Pleroma.Web.Endpoint.NotReal",
 +                   "value" => [
 +                     %{
 +                       "tuple" => [
 +                         ":http",
 +                         [
 +                           %{
 +                             "tuple" => [
 +                               ":key2",
 +                               [
 +                                 %{
 +                                   "tuple" => [
 +                                     ":_",
 +                                     [
 +                                       %{
 +                                         "tuple" => [
 +                                           "/api/v1/streaming",
 +                                           "Pleroma.Web.MastodonAPI.WebsocketHandler",
 +                                           []
 +                                         ]
 +                                       },
 +                                       %{
 +                                         "tuple" => [
 +                                           "/websocket",
 +                                           "Phoenix.Endpoint.CowboyWebSocket",
 +                                           %{
 +                                             "tuple" => [
 +                                               "Phoenix.Transports.WebSocket",
 +                                               %{
 +                                                 "tuple" => [
 +                                                   "Pleroma.Web.Endpoint",
 +                                                   "Pleroma.Web.UserSocket",
 +                                                   []
 +                                                 ]
 +                                               }
 +                                             ]
 +                                           }
 +                                         ]
 +                                       },
 +                                       %{
 +                                         "tuple" => [
 +                                           ":_",
 +                                           "Phoenix.Endpoint.Cowboy2Handler",
 +                                           %{"tuple" => ["Pleroma.Web.Endpoint", []]}
 +                                         ]
 +                                       }
 +                                     ]
 +                                   ]
 +                                 }
 +                               ]
 +                             ]
 +                           }
 +                         ]
 +                       ]
 +                     }
 +                   ]
 +                 }
 +               ]
 +             }
 +    end
 +
 +    test "settings with nesting map", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              "group" => "pleroma",
 +              "key" => ":key1",
 +              "value" => [
 +                %{"tuple" => [":key2", "some_val"]},
 +                %{
 +                  "tuple" => [
 +                    ":key3",
 +                    %{
 +                      ":max_options" => 20,
 +                      ":max_option_chars" => 200,
 +                      ":min_expiration" => 0,
 +                      ":max_expiration" => 31_536_000,
 +                      "nested" => %{
 +                        ":max_options" => 20,
 +                        ":max_option_chars" => 200,
 +                        ":min_expiration" => 0,
 +                        ":max_expiration" => 31_536_000
 +                      }
 +                    }
 +                  ]
 +                }
 +              ]
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) ==
 +               %{
 +                 "configs" => [
 +                   %{
 +                     "group" => "pleroma",
 +                     "key" => ":key1",
 +                     "value" => [
 +                       %{"tuple" => [":key2", "some_val"]},
 +                       %{
 +                         "tuple" => [
 +                           ":key3",
 +                           %{
 +                             ":max_expiration" => 31_536_000,
 +                             ":max_option_chars" => 200,
 +                             ":max_options" => 20,
 +                             ":min_expiration" => 0,
 +                             "nested" => %{
 +                               ":max_expiration" => 31_536_000,
 +                               ":max_option_chars" => 200,
 +                               ":max_options" => 20,
 +                               ":min_expiration" => 0
 +                             }
 +                           }
 +                         ]
 +                       }
 +                     ]
 +                   }
 +                 ]
 +               }
 +    end
 +
 +    test "value as map", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              "group" => "pleroma",
 +              "key" => ":key1",
 +              "value" => %{"key" => "some_val"}
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) ==
 +               %{
 +                 "configs" => [
 +                   %{
 +                     "group" => "pleroma",
 +                     "key" => ":key1",
 +                     "value" => %{"key" => "some_val"}
 +                   }
 +                 ]
 +               }
 +    end
 +
 +    test "dispatch setting", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              "group" => "pleroma",
 +              "key" => "Pleroma.Web.Endpoint.NotReal",
 +              "value" => [
 +                %{
 +                  "tuple" => [
 +                    ":http",
 +                    [
 +                      %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
 +                      %{"tuple" => [":dispatch", ["{:_,
 +       [
 +         {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
 +         {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
 +          {Phoenix.Transports.WebSocket,
 +           {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
 +         {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
 +       ]}"]]}
 +                    ]
 +                  ]
 +                }
 +              ]
 +            }
 +          ]
 +        })
 +
 +      dispatch_string =
 +        "{:_, [{\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, " <>
 +          "{\"/websocket\", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, " <>
 +          "{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}}, " <>
 +          "{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}]}"
 +
 +      assert json_response(conn, 200) == %{
 +               "configs" => [
 +                 %{
 +                   "group" => "pleroma",
 +                   "key" => "Pleroma.Web.Endpoint.NotReal",
 +                   "value" => [
 +                     %{
 +                       "tuple" => [
 +                         ":http",
 +                         [
 +                           %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
 +                           %{
 +                             "tuple" => [
 +                               ":dispatch",
 +                               [
 +                                 dispatch_string
 +                               ]
 +                             ]
 +                           }
 +                         ]
 +                       ]
 +                     }
 +                   ]
 +                 }
 +               ]
 +             }
 +    end
 +
 +    test "queues key as atom", %{conn: conn} do
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              "group" => "pleroma_job_queue",
 +              "key" => ":queues",
 +              "value" => [
 +                %{"tuple" => [":federator_incoming", 50]},
 +                %{"tuple" => [":federator_outgoing", 50]},
 +                %{"tuple" => [":web_push", 50]},
 +                %{"tuple" => [":mailer", 10]},
 +                %{"tuple" => [":transmogrifier", 20]},
 +                %{"tuple" => [":scheduled_activities", 10]},
 +                %{"tuple" => [":background", 5]}
 +              ]
 +            }
 +          ]
 +        })
 +
 +      assert json_response(conn, 200) == %{
 +               "configs" => [
 +                 %{
 +                   "group" => "pleroma_job_queue",
 +                   "key" => ":queues",
 +                   "value" => [
 +                     %{"tuple" => [":federator_incoming", 50]},
 +                     %{"tuple" => [":federator_outgoing", 50]},
 +                     %{"tuple" => [":web_push", 50]},
 +                     %{"tuple" => [":mailer", 10]},
 +                     %{"tuple" => [":transmogrifier", 20]},
 +                     %{"tuple" => [":scheduled_activities", 10]},
 +                     %{"tuple" => [":background", 5]}
 +                   ]
 +                 }
 +               ]
 +             }
 +    end
 +
 +    test "delete part of settings by atom subkeys", %{conn: conn} do
 +      config =
 +        insert(:config,
 +          key: "keyaa1",
 +          value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
 +        )
 +
 +      conn =
 +        post(conn, "/api/pleroma/admin/config", %{
 +          configs: [
 +            %{
 +              group: config.group,
 +              key: config.key,
 +              subkeys: [":subkey1", ":subkey3"],
 +              delete: "true"
 +            }
 +          ]
 +        })
 +
 +      assert(
 +        json_response(conn, 200) == %{
 +          "configs" => [
 +            %{
 +              "group" => "pleroma",
 +              "key" => "keyaa1",
 +              "value" => [%{"tuple" => [":subkey2", "val2"]}]
 +            }
 +          ]
 +        }
 +      )
 +    end
 +  end
 +
 +  describe "config mix tasks run" do
 +    setup %{conn: conn} do
 +      admin = insert(:user, info: %{is_admin: true})
 +
 +      temp_file = "config/test.exported_from_db.secret.exs"
 +
 +      Mix.shell(Mix.Shell.Quiet)
 +
 +      on_exit(fn ->
 +        Mix.shell(Mix.Shell.IO)
 +        :ok = File.rm(temp_file)
 +      end)
 +
 +      %{conn: assign(conn, :user, admin), admin: admin}
 +    end
 +
 +    clear_config([:instance, :dynamic_configuration]) do
 +      Pleroma.Config.put([:instance, :dynamic_configuration], true)
 +    end
 +
 +    test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
 +      assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
 +      conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
 +      assert json_response(conn, 200) == %{}
 +      assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0
 +
 +      conn =
 +        build_conn()
 +        |> assign(:user, admin)
 +        |> get("/api/pleroma/admin/config/migrate_from_db")
 +
 +      assert json_response(conn, 200) == %{}
 +      assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
 +    end
 +  end
 +
 +  describe "GET /api/pleroma/admin/users/:nickname/statuses" do
 +    setup do
 +      admin = insert(:user, info: %{is_admin: true})
 +      user = insert(:user)
 +
 +      date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
 +      date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
 +      date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
 +
 +      insert(:note_activity, user: user, published: date1)
 +      insert(:note_activity, user: user, published: date2)
 +      insert(:note_activity, user: user, published: date3)
 +
 +      conn =
 +        build_conn()
 +        |> assign(:user, admin)
 +
 +      {:ok, conn: conn, user: user}
 +    end
 +
 +    test "renders user's statuses", %{conn: conn, user: user} do
 +      conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
 +
 +      assert json_response(conn, 200) |> length() == 3
 +    end
 +
 +    test "renders user's statuses with a limit", %{conn: conn, user: user} do
 +      conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2")
 +
 +      assert json_response(conn, 200) |> length() == 2
 +    end
 +
 +    test "doesn't return private statuses by default", %{conn: conn, user: user} do
 +      {:ok, _private_status} =
 +        CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
 +
 +      {:ok, _public_status} =
 +        CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
 +
 +      conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
 +
 +      assert json_response(conn, 200) |> length() == 4
 +    end
 +
 +    test "returns private statuses with godmode on", %{conn: conn, user: user} do
 +      {:ok, _private_status} =
 +        CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
 +
 +      {:ok, _public_status} =
 +        CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
 +
 +      conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
 +
 +      assert json_response(conn, 200) |> length() == 5
 +    end
 +  end
 +end
 +
 +# Needed for testing
 +defmodule Pleroma.Web.Endpoint.NotReal do
 +end
 +
 +defmodule Pleroma.Captcha.NotReal do
  end