Merge develop
authorRoman Chvanikov <chvanikoff@pm.me>
Tue, 9 Jul 2019 18:21:09 +0000 (21:21 +0300)
committerRoman Chvanikov <chvanikoff@pm.me>
Tue, 9 Jul 2019 18:21:09 +0000 (21:21 +0300)
13 files changed:
1  2 
CHANGELOG.md
config/config.exs
config/test.exs
docs/config.md
lib/mix/tasks/pleroma/instance.ex
lib/pleroma/application.ex
lib/pleroma/user.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock
priv/templates/sample_config.eex
test/user_search_test.exs
test/user_test.exs

diff --combined CHANGELOG.md
index bd7ca14e44b10fe6779f7af6b5851b520b7a2085,b9212984951e23409d3ea67484a40b4b46509968..f486e01ac0349cdf4a5ba9f841d592821ba120a4
@@@ -6,21 -6,33 +6,34 @@@ The format is based on [Keep a Changelo
  ## [Unreleased]
  ### Added
  - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
+ 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
+ - 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
+ - Added synchronization of following/followers counters for external users
  
  ### Fixed
  - Not being able to pin unlisted posts
+ - Metadata rendering errors resulting in the entire page being inaccessible
+ - 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
  
  ### Changed
+ - Configuration: OpenGraph and TwitterCard providers enabled by default
  - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
  
+ ### Changed
+ - NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
  ## [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).
@@@ -47,7 -59,6 +60,7 @@@
  - 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
@@@ -75,6 -86,7 +88,7 @@@
  - 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`)
+ - Ability to reset avatar, profile banner and backgroud
  - MRF: Support for running subchains.
  - Configuration: `skip_thread_containment` option
  - Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
diff --combined config/config.exs
index cb3ee531ccd3c84597aab04f5fba74906db8bb48,09681f122ddbe8f8759cd005b7d09b1d3bb7f069..d8c8b1a6d385a34d312253a3d784cb40b851f44e
@@@ -218,6 -218,7 +218,7 @@@ config :pleroma, :instance
    },
    registrations_open: true,
    federating: true,
+   federation_incoming_replies_max_depth: 100,
    federation_reachability_timeout_days: 7,
    federation_publisher_modules: [
      Pleroma.Web.ActivityPub.Publisher,
    remote_post_retention_days: 90,
    skip_thread_containment: true,
    limit_to_local_content: :unauthenticated,
-   dynamic_configuration: false
+   dynamic_configuration: false,
+   external_user_synchronization: [
+     enabled: false,
+     # every 2 hours
+     interval: 60 * 60 * 2,
+     max_retries: 3,
+     limit: 500
+   ]
  
  config :pleroma, :markup,
    # XXX - unfortunately, inline images must be enabled by default right now, because
@@@ -358,7 -366,11 +366,11 @@@ config :pleroma, :gopher
    port: 9999
  
  config :pleroma, Pleroma.Web.Metadata,
-   providers: [Pleroma.Web.Metadata.Providers.RelMe],
+   providers: [
+     Pleroma.Web.Metadata.Providers.OpenGraph,
+     Pleroma.Web.Metadata.Providers.TwitterCard,
+     Pleroma.Web.Metadata.Providers.RelMe
+   ],
    unfurl_nsfw: false
  
  config :pleroma, :suggestions,
@@@ -498,14 -510,6 +510,14 @@@ config :pleroma, Pleroma.ScheduledActiv
    total_user_limit: 300,
    enabled: true
  
 +config :pleroma, :email_notifications,
 +  digest: %{
 +    active: false,
 +    schedule: "0 0 * * 0",
 +    interval: 7,
 +    inactivity_threshold: 7
 +  }
 +
  config :pleroma, :oauth2,
    token_expires_in: 600,
    issue_new_refresh_token: true,
diff --combined config/test.exs
index 1838cffc8eb15302b02bd05e8870e274e5fd343f,63443dde0115a1127df6133a645b329a01810be4..1635a5d92feaf8c5b114ba62dc478457af2a315a
@@@ -28,7 -28,8 +28,8 @@@ config :pleroma, Pleroma.Emails.Mailer
  config :pleroma, :instance,
    email: "admin@example.com",
    notify_email: "noreply@example.com",
-   skip_thread_containment: false
+   skip_thread_containment: false,
+   federating: false
  
  # Configure your database
  config :pleroma, Pleroma.Repo,
@@@ -74,8 -75,8 +75,10 @@@ rum_enabled = System.get_env("RUM_ENABL
  config :pleroma, :database, rum_enabled: rum_enabled
  IO.puts("RUM enabled: #{rum_enabled}")
  
 +config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp35v0RK9SO8WTPr6QZ"
 +
+ config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
  try do
    import_config "test.secret.exs"
  rescue
diff --combined docs/config.md
index 15af7f1b22fb1cec864ba8e287882c0557ecc5e8,931155fe9153978c4f9d99da9f54b2acce6ed5c3..cbaa790d1bcc325b0befcca672594b7000cfe2d6
@@@ -87,6 -87,7 +87,7 @@@ config :pleroma, Pleroma.Emails.Mailer
  * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
  * `account_activation_required`: Require users to confirm their emails before signing in.
  * `federating`: Enable federation with other instances
+ * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
  * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
  * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
  * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
  * `skip_thread_containment`: Skip filter out broken threads. The default is `false`.
  * `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`.
  * `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
+ * `external_user_synchronization`: Following/followers counters synchronization settings.
+   * `enabled`: Enables synchronization
+   * `interval`: Interval between synchronization.
+   * `max_retries`: Max rettries for host. After exceeding the limit, the check will not be carried out for users from this host.
+   * `limit`: Users batch size for processing in one time.
  
  
  ## :logger
@@@ -525,18 -532,6 +532,18 @@@ Authentication / authorization settings
  * `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`.
  * `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by `OAUTH_CONSUMER_STRATEGIES` environment variable. Each entry in this space-delimited string should be of format `<strategy>` or `<strategy>:<dependency>` (e.g. `twitter` or `keycloak:ueberauth_keycloak_strategy` in case dependency is named differently than `ueberauth_<strategy>`).
  
 +## :email_notifications
 +
 +Email notifications settings.
 +
 +  - digest - emails of "what you've missed" for users who have been
 +    inactive for a while.
 +    - active: globally enable or disable digest emails
 +    - schedule: When to send digest email, in [crontab format](https://en.wikipedia.org/wiki/Cron).
 +      "0 0 * * 0" is the default, meaning "once a week at midnight on Sunday morning"
 +    - interval: Minimum interval between digest emails to one user
 +    - inactivity_threshold: Minimum user inactivity threshold
 +
  ## OAuth consumer mode
  
  OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
index 0231b76cdeb67a7ebb2cdb1fe002c96ba83432df,2ae16adc0d73378f59a95364d3fbd26cb9ab8613..f08e5ff3fe15f675024b595578c52a2ce46d6d99
@@@ -149,7 -149,7 +149,7 @@@ defmodule Mix.Tasks.Pleroma.Instance d
        uploads_dir =
          get_option(
            options,
-           :upload_dir,
+           :uploads_dir,
            "What directory should media uploads go in (when using the local uploader)?",
            Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
          )
          )
  
        secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
 +      jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
        signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
        {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
        template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
            dbuser: dbuser,
            dbpass: dbpass,
            secret: secret,
 +          jwt_secret: jwt_secret,
            signing_salt: signing_salt,
            web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
            web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
index 29cd144770f5d4ca1d635ca328e541d9ca87c48a,86c348a0d64c5669a7b033fd59bd56a08b701eaa..8887e393536db2ef6a60bd8ee5c2ce979a40925a
@@@ -115,10 -115,6 +115,10 @@@ defmodule Pleroma.Application d
          %{
            id: Pleroma.ScheduledActivityWorker,
            start: {Pleroma.ScheduledActivityWorker, :start_link, []}
 +        },
 +        %{
 +          id: Pleroma.QuantumScheduler,
 +          start: {Pleroma.QuantumScheduler, :start_link, []}
          }
        ] ++
          hackney_pool_children() ++
              start: {Pleroma.Web.Endpoint, :start_link, []},
              type: :supervisor
            },
-           %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}
+           %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}},
+           %{
+             id: Pleroma.User.SynchronizationWorker,
+             start: {Pleroma.User.SynchronizationWorker, :start_link, []}
+           }
          ]
  
      # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
      # for other strategies and supported options
      opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
 -    Supervisor.start_link(children, opts)
 +    result = Supervisor.start_link(children, opts)
 +    :ok = after_supervisor_start()
 +    result
    end
  
    defp setup_instrumenters do
        :hackney_pool.child_spec(pool, options)
      end
    end
 +
 +  defp after_supervisor_start do
 +    with digest_config <- Application.get_env(:pleroma, :email_notifications)[:digest],
 +         true <- digest_config[:active],
 +         %Crontab.CronExpression{} = schedule <-
 +           Crontab.CronExpression.Parser.parse!(digest_config[:schedule]) do
 +      Pleroma.QuantumScheduler.new_job()
 +      |> Quantum.Job.set_name(:digest_emails)
 +      |> Quantum.Job.set_schedule(schedule)
 +      |> Quantum.Job.set_task(&Pleroma.DigestEmailWorker.run/0)
 +      |> Pleroma.QuantumScheduler.add_job()
 +    end
 +
 +    :ok
 +  end
  end
diff --combined lib/pleroma/user.ex
index 9be4b1483d3e7e314bcb7e363b3f15227c076f5e,d03810d1ad551e07ccbba54760dd53a2893c3ed9..791676ee28e5e9327714d4ef2304e04068fb5e32
@@@ -56,7 -56,6 +56,7 @@@ defmodule Pleroma.User d
      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
+   def user_info(%User{} = user, args \\ %{}) do
+     following_count =
+       if args[:following_count], do: args[:following_count], else: 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 set_info_cache(user, args) do
+     Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
    end
  
    def restrict_deactivated(query) do
    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
+   def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
+     blocks = info.blocks
+     domain_blocks = info.domain_blocks
      %{host: host} = URI.parse(ap_id)
  
-     Enum.member?(blocks, ap_id) ||
-       Enum.any?(domain_blocks, fn domain ->
-         host == domain
-       end)
+     Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
    end
  
    def subscribed_to?(user, %{ap_id: ap_id}) do
      )
    end
  
+   @spec sync_follow_counter() :: :ok
+   def sync_follow_counter,
+     do: PleromaJobQueue.enqueue(:background, __MODULE__, [:sync_follow_counters])
+   @spec perform(:sync_follow_counters) :: :ok
+   def perform(:sync_follow_counters) do
+     {:ok, _pid} = Agent.start_link(fn -> %{} end, name: :domain_errors)
+     config = Pleroma.Config.get([:instance, :external_user_synchronization])
+     :ok = sync_follow_counters(config)
+     Agent.stop(:domain_errors)
+   end
+   @spec sync_follow_counters(keyword()) :: :ok
+   def sync_follow_counters(opts \\ []) do
+     users = external_users(opts)
+     if length(users) > 0 do
+       errors = Agent.get(:domain_errors, fn state -> state end)
+       {last, updated_errors} = User.Synchronization.call(users, errors, opts)
+       Agent.update(:domain_errors, fn _state -> updated_errors end)
+       sync_follow_counters(max_id: last.id, limit: opts[:limit])
+     else
+       :ok
+     end
+   end
+   @spec external_users(keyword()) :: [User.t()]
+   def external_users(opts \\ []) do
+     query =
+       User.Query.build(%{
+         external: true,
+         active: true,
+         order_by: :id,
+         select: [: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__, [
      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
index 9cb8db7fdfe8343afc227e012a4c7bc85c7c5f57,d53fa8a350b56c1f9387af3d6a76d92c0462c1af..b25e4ffd87db0000fcc5bf98bfbf016933321297
@@@ -322,6 -322,10 +322,10 @@@ defmodule Pleroma.Web.Router d
  
        patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
  
+       patch("/accounts/update_avatar", MastodonAPIController, :update_avatar)
+       patch("/accounts/update_banner", MastodonAPIController, :update_banner)
+       patch("/accounts/update_background", MastodonAPIController, :update_background)
        post("/statuses", MastodonAPIController, :post_status)
        delete("/statuses/:id", MastodonAPIController, :delete_status)
  
      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)
 +
 +    get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
    end
  
    pipeline :activitypub do
@@@ -726,6 -728,7 +730,7 @@@ en
  
  defmodule Fallback.RedirectController do
    use Pleroma.Web, :controller
+   require Logger
    alias Pleroma.User
    alias Pleroma.Web.Metadata
  
  
    def redirector_with_meta(conn, params) do
      {:ok, index_content} = File.read(index_file_path())
-     tags = Metadata.build_tags(params)
+     tags =
+       try do
+         Metadata.build_tags(params)
+       rescue
+         e ->
+           Logger.error(
+             "Metadata rendering for #{conn.request_path} failed.\n" <>
+               Exception.format(:error, e, __STACKTRACE__)
+           )
+           ""
+       end
      response = String.replace(index_content, "<!--server-generated-meta-->", tags)
  
      conn
diff --combined mix.exs
index 45717ba07d23f2998c39e924ed0e77a047040559,8f64562ef947700b1d2407efc5845aa10dd458c7..b61bde168b60acf81dfa8f80cdc453416d50977c
+++ b/mix.exs
@@@ -109,7 -109,6 +109,6 @@@ defmodule Pleroma.Mixfile d
        {:phoenix_html, "~> 2.10"},
        {:calendar, "~> 0.17.4"},
        {:cachex, "~> 3.0.2"},
-       {:httpoison, "~> 1.2.0"},
        {:poison, "~> 3.0", override: true},
        {:tesla, "~> 1.2"},
        {:jason, "~> 1.0"},
        {:ex_doc, "~> 0.20.2", only: :dev, runtime: false},
        {:web_push_encryption, "~> 0.2.1"},
        {:swoosh, "~> 0.20"},
 +      {:phoenix_swoosh, "~> 0.2"},
        {:gen_smtp, "~> 0.13"},
        {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
        {:floki, "~> 0.20.0"},
        {:prometheus_ecto, "~> 1.4"},
        {:recon, github: "ferd/recon", tag: "2.4.0"},
        {:quack, "~> 0.1.1"},
 +      {:quantum, "~> 2.3"},
 +      {:joken, "~> 2.0"},
        {:benchee, "~> 1.0"},
        {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
        {:ex_rated, "~> 1.3"},
        {:plug_static_index_html, "~> 1.0.0"},
-       {:excoveralls, "~> 0.11.1", only: :test}
+       {:excoveralls, "~> 0.11.1", only: :test},
+       {:mox, "~> 0.5", only: :test}
      ] ++ oauth_deps()
    end
  
    # Builds a version string made of:
    # * the application version
    # * a pre-release if ahead of the tag: the describe string (-count-commithash)
-   # * build info:
+   # * branch name
+   # * build metadata:
    #   * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
    #   * the mix environment if different than prod
    defp version(version) do
+     identifier_filter = ~r/[^0-9a-z\-]+/i
+     # Pre-release version, denoted from patch version with a hyphen
      {git_tag, git_pre_release} =
        with {tag, 0} <-
               System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
        )
      end
  
+     # Branch name as pre-release version component, denoted with a dot
+     branch_name =
+       with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
+            branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
+            true <- branch_name != "master" do
+         branch_name =
+           branch_name
+           |> String.trim()
+           |> String.replace(identifier_filter, "-")
+         "." <> branch_name
+       end
      build_name =
        cond do
          name = Application.get_env(:pleroma, :build_name) -> name
        end
  
      env_name = if Mix.env() != :prod, do: to_string(Mix.env())
+     env_override = System.get_env("PLEROMA_BUILD_ENV")
  
-     build =
+     env_name =
+       case env_override do
+         nil -> env_name
+         env_override when env_override in ["", "prod"] -> nil
+         env_override -> env_override
+       end
+     # Build metadata, denoted with a plus sign
+     build_metadata =
        [build_name, env_name]
        |> Enum.filter(fn string -> string && string != "" end)
-       |> Enum.join("-")
+       |> Enum.join(".")
        |> (fn
              "" -> nil
-             string -> "+" <> string
+             string -> "+" <> String.replace(string, identifier_filter, "-")
            end).()
  
-     branch_name =
-       with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
-            branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
-            true <- branch_name != "master" do
-         branch_name =
-           String.trim(branch_name)
-           |> String.replace(~r/[^0-9a-z\-\.]+/i, "-")
-         "-" <> branch_name
-       end
-     [version, git_pre_release, branch_name, build]
+     [version, git_pre_release, branch_name, build_metadata]
      |> Enum.filter(fn string -> string && string != "" end)
      |> Enum.join()
    end
diff --combined mix.lock
index 02932b2c067cc673ff88d31fb64d6e7e39354a97,e711be635b801af7690f4ba9b4a181bae9aee6ca..addd72e4bf6498400206e54ec55ca844cbb376b6
+++ b/mix.lock
@@@ -5,17 -5,16 +5,17 @@@
    "bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
    "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
    "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
 -  "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
 -  "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
 +  "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
 +  "calendar": {:hex, :calendar, "0.17.5", "0ff5b09a60b9677683aa2a6fee948558660501c74a289103ea099806bc41a352", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
    "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
    "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
 -  "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
 +  "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
    "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
    "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
    "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"},
    "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
 +  "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
    "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
    "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
    "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"},
@@@ -35,9 -34,7 +35,9 @@@
    "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
    "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
    "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},
 -  "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
 +  "gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], [], "hexpm"},
 +  "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
 +  "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
    "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
    "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
    "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
    "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
    "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
    "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
 -  "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
 +  "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
 +  "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
 +  "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
    "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
    "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
    "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
    "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
    "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
    "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
 -  "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
 +  "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
    "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
    "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
+   "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"},
    "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
    "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
    "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
    "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
 +  "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"},
    "pleroma_job_queue": {:hex, :pleroma_job_queue, "0.2.0", "879e660aa1cebe8dc6f0aaaa6aa48b4875e89cd961d4a585fd128e0773b31a18", [:mix], [], "hexpm"},
    "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
    "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
    "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
-   "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
    "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
    "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"},
    "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
    "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
    "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
    "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"},
-   "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.3", "657386e8f142fc817347d95c1f3a05ab08710f7df9e7f86db6facaed107ed929", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
    "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},
 +  "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"},
    "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
    "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
    "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
 -  "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
 +  "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
 +  "swoosh": {:hex, :swoosh, "0.23.1", "209b7cc6d862c09d2a064c16caa4d4d1c9c936285476459e16591e0065f8432b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
    "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
    "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
    "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
@@@ -94,7 -85,7 +93,7 @@@
    "tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
    "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
 -  "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
 +  "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},
    "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
    "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
  }
index 7f275279e710d15e1d139db06184b7e2bdff87f9,5cc31c604b1eb8079ca2a0676b3183d8df36547d..5b4dad648aa8a1d08c7ec89c51c134939587b04b
@@@ -67,22 -67,3 +67,5 @@@ config :pleroma, Pleroma.Uploaders.Loca
  # For using third-party S3 clones like wasabi, also do:
  # config :ex_aws, :s3,
  #   host: "s3.wasabisys.com"
- # Configure Openstack Swift support if desired.
- #
- # Many openstack deployments are different, so config is left very open with
- # no assumptions made on which provider you're using. This should allow very
- # wide support without needing separate handlers for OVH, Rackspace, etc.
- #
- # config :pleroma, Pleroma.Uploaders.Swift,
- #  container: "some-container",
- #  username: "api-username-yyyy",
- #  password: "api-key-xxxx",
- #  tenant_id: "<openstack-project/tenant-id>",
- #  auth_url: "https://keystone-endpoint.provider.com",
- #  storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>",
- #  object_url: "https://cdn-endpoint.provider.com/<container>"
- #
 +
 +config :joken, default_signer: "<%= jwt_secret %>"
index 0000000000000000000000000000000000000000,1f0162486034104fa43568cebb9eff09e565b1e2..3c13d8b0f018a884227d5da18aadb9ae9b3cacd1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,252 +1,259 @@@
 -      assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.UserSearchTest do
+   alias Pleroma.Repo
+   alias Pleroma.User
+   use Pleroma.DataCase
+   import Pleroma.Factory
+   setup_all do
+     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
+   describe "User.search" do
+     test "accepts limit parameter" do
+       Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
+       assert length(User.search("john", limit: 3)) == 3
+       assert length(User.search("john")) == 5
+     end
+     test "accepts offset parameter" do
+       Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
+       assert length(User.search("john", limit: 3)) == 3
+       assert length(User.search("john", limit: 3, offset: 3)) == 2
+     end
+     test "finds a user by full or partial nickname" do
+       user = insert(:user, %{nickname: "john"})
+       Enum.each(["john", "jo", "j"], fn query ->
+         assert user ==
+                  User.search(query)
+                  |> List.first()
+                  |> Map.put(:search_rank, nil)
+                  |> Map.put(:search_type, nil)
+       end)
+     end
+     test "finds a user by full or partial name" do
+       user = insert(:user, %{name: "John Doe"})
+       Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
+         assert user ==
+                  User.search(query)
+                  |> List.first()
+                  |> Map.put(:search_rank, nil)
+                  |> Map.put(:search_type, nil)
+       end)
+     end
+     test "finds users, preferring nickname matches over name matches" do
+       u1 = insert(:user, %{name: "lain", nickname: "nick1"})
+       u2 = insert(:user, %{nickname: "lain", name: "nick1"})
+       assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
+     end
+     test "finds users, considering density of matched tokens" do
+       u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
+       u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
+       assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
+     end
+     test "finds users, ranking by similarity" do
+       u1 = insert(:user, %{name: "lain"})
+       _u2 = insert(:user, %{name: "ean"})
+       u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
+       u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
+       assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
+     end
+     test "finds users, handling misspelled requests" do
+       u1 = insert(:user, %{name: "lain"})
+       assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
+     end
+     test "finds users, boosting ranks of friends and followers" do
+       u1 = insert(:user)
+       u2 = insert(:user, %{name: "Doe"})
+       follower = insert(:user, %{name: "Doe"})
+       friend = insert(:user, %{name: "Doe"})
+       {:ok, follower} = User.follow(follower, u1)
+       {:ok, u1} = User.follow(u1, friend)
+       assert [friend.id, follower.id, u2.id] --
+                Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == []
+     end
+     test "finds followers of user by partial name" do
+       u1 = insert(:user)
+       u2 = insert(:user, %{name: "Jimi"})
+       follower_jimi = insert(:user, %{name: "Jimi Hendrix"})
+       follower_lizz = insert(:user, %{name: "Lizz Wright"})
+       friend = insert(:user, %{name: "Jimi"})
+       {:ok, follower_jimi} = User.follow(follower_jimi, u1)
+       {:ok, _follower_lizz} = User.follow(follower_lizz, u2)
+       {:ok, u1} = User.follow(u1, friend)
+       assert Enum.map(User.search("jimi", following: true, for_user: u1), & &1.id) == [
+                follower_jimi.id
+              ]
+       assert User.search("lizz", following: true, for_user: u1) == []
+     end
+     test "find local and remote users for authenticated users" do
+       u1 = insert(:user, %{name: "lain"})
+       u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+       u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+       results =
+         "lain"
+         |> User.search(for_user: u1)
+         |> Enum.map(& &1.id)
+         |> Enum.sort()
+       assert [u1.id, u2.id, u3.id] == results
+     end
+     test "find only local users for unauthenticated users" do
+       %{id: id} = insert(:user, %{name: "lain"})
+       insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+       insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+       assert [%{id: ^id}] = User.search("lain")
+     end
+     test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do
+       Pleroma.Config.put([:instance, :limit_to_local_content], :all)
+       %{id: id} = insert(:user, %{name: "lain"})
+       insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+       insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+       assert [%{id: ^id}] = User.search("lain")
+       Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+     end
+     test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do
+       Pleroma.Config.put([:instance, :limit_to_local_content], false)
+       u1 = insert(:user, %{name: "lain"})
+       u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+       u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+       results =
+         "lain"
+         |> User.search()
+         |> Enum.map(& &1.id)
+         |> Enum.sort()
+       assert [u1.id, u2.id, u3.id] == results
+       Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+     end
+     test "finds a user whose name is nil" do
+       _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
+       user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
+       assert user_two ==
+                User.search("lain@pleroma.soykaf.com")
+                |> List.first()
+                |> Map.put(:search_rank, nil)
+                |> Map.put(:search_type, nil)
+     end
+     test "does not yield false-positive matches" do
+       insert(:user, %{name: "John Doe"})
+       Enum.each(["mary", "a", ""], fn query ->
+         assert [] == User.search(query)
+       end)
+     end
+     test "works with URIs" do
+       user = insert(:user)
+       results =
+         User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user)
+       result = results |> List.first()
+       user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
+       assert length(results) == 1
++
++      expected =
++        result
++        |> Map.put(:search_rank, nil)
++        |> Map.put(:search_type, nil)
++        |> Map.put(:last_digest_emailed_at, nil)
++
++      assert user == expected
+     end
+     test "excludes a blocked users from search result" do
+       user = insert(:user, %{nickname: "Bill"})
+       [blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"}))
+       blocked_user2 =
+         insert(
+           :user,
+           %{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"}
+         )
+       User.block_domain(user, "awful-and-rude-instance.com")
+       User.block(user, blocked_user)
+       account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids
+       assert account_ids == collect_ids(users)
+       refute Enum.member?(account_ids, blocked_user.id)
+       refute Enum.member?(account_ids, blocked_user2.id)
+       assert length(account_ids) == 3
+     end
+     test "local user has the same search_rank as for users with the same nickname, but another domain" do
+       user = insert(:user)
+       insert(:user, nickname: "lain@mastodon.social")
+       insert(:user, nickname: "lain")
+       insert(:user, nickname: "lain@pleroma.social")
+       assert User.search("lain@localhost", resolve: true, for_user: user)
+              |> Enum.each(fn u -> u.search_rank == 0.5 end)
+     end
+     test "localhost is the part of the domain" do
+       user = insert(:user)
+       insert(:user, nickname: "another@somedomain")
+       insert(:user, nickname: "lain")
+       insert(:user, nickname: "lain@examplelocalhost")
+       result = User.search("lain@examplelocalhost", resolve: true, for_user: user)
+       assert Enum.each(result, fn u -> u.search_rank == 0.5 end)
+       assert length(result) == 2
+     end
+     test "local user search with users" do
+       user = insert(:user)
+       local_user = insert(:user, nickname: "lain")
+       insert(:user, nickname: "another@localhost.com")
+       insert(:user, nickname: "localhost@localhost.com")
+       [result] = User.search("lain@localhost", resolve: true, for_user: user)
+       assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user
+     end
+   end
+ end
diff --combined test/user_test.exs
index 90d2db2eb491ba26aaebb4ee8ebb714b7bc94608,0f27d73f7c0efd162a14ea2163fbf59d82487b40..6dec3959ce7addeae35e29f2e837784760a8dd64
@@@ -1012,192 -1012,6 +1012,6 @@@ defmodule Pleroma.UserTest d
      end
    end
  
-   describe "User.search" do
-     test "accepts limit parameter" do
-       Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
-       assert length(User.search("john", limit: 3)) == 3
-       assert length(User.search("john")) == 5
-     end
-     test "accepts offset parameter" do
-       Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
-       assert length(User.search("john", limit: 3)) == 3
-       assert length(User.search("john", limit: 3, offset: 3)) == 2
-     end
-     test "finds a user by full or partial nickname" do
-       user = insert(:user, %{nickname: "john"})
-       Enum.each(["john", "jo", "j"], fn query ->
-         assert user ==
-                  User.search(query)
-                  |> List.first()
-                  |> Map.put(:search_rank, nil)
-                  |> Map.put(:search_type, nil)
-       end)
-     end
-     test "finds a user by full or partial name" do
-       user = insert(:user, %{name: "John Doe"})
-       Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
-         assert user ==
-                  User.search(query)
-                  |> List.first()
-                  |> Map.put(:search_rank, nil)
-                  |> Map.put(:search_type, nil)
-       end)
-     end
-     test "finds users, preferring nickname matches over name matches" do
-       u1 = insert(:user, %{name: "lain", nickname: "nick1"})
-       u2 = insert(:user, %{nickname: "lain", name: "nick1"})
-       assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
-     end
-     test "finds users, considering density of matched tokens" do
-       u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
-       u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
-       assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
-     end
-     test "finds users, ranking by similarity" do
-       u1 = insert(:user, %{name: "lain"})
-       _u2 = insert(:user, %{name: "ean"})
-       u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
-       u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
-       assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
-     end
-     test "finds users, handling misspelled requests" do
-       u1 = insert(:user, %{name: "lain"})
-       assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
-     end
-     test "finds users, boosting ranks of friends and followers" do
-       u1 = insert(:user)
-       u2 = insert(:user, %{name: "Doe"})
-       follower = insert(:user, %{name: "Doe"})
-       friend = insert(:user, %{name: "Doe"})
-       {:ok, follower} = User.follow(follower, u1)
-       {:ok, u1} = User.follow(u1, friend)
-       assert [friend.id, follower.id, u2.id] --
-                Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == []
-     end
-     test "finds followers of user by partial name" do
-       u1 = insert(:user)
-       u2 = insert(:user, %{name: "Jimi"})
-       follower_jimi = insert(:user, %{name: "Jimi Hendrix"})
-       follower_lizz = insert(:user, %{name: "Lizz Wright"})
-       friend = insert(:user, %{name: "Jimi"})
-       {:ok, follower_jimi} = User.follow(follower_jimi, u1)
-       {:ok, _follower_lizz} = User.follow(follower_lizz, u2)
-       {:ok, u1} = User.follow(u1, friend)
-       assert Enum.map(User.search("jimi", following: true, for_user: u1), & &1.id) == [
-                follower_jimi.id
-              ]
-       assert User.search("lizz", following: true, for_user: u1) == []
-     end
-     test "find local and remote users for authenticated users" do
-       u1 = insert(:user, %{name: "lain"})
-       u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-       u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-       results =
-         "lain"
-         |> User.search(for_user: u1)
-         |> Enum.map(& &1.id)
-         |> Enum.sort()
-       assert [u1.id, u2.id, u3.id] == results
-     end
-     test "find only local users for unauthenticated users" do
-       %{id: id} = insert(:user, %{name: "lain"})
-       insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-       insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-       assert [%{id: ^id}] = User.search("lain")
-     end
-     test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do
-       Pleroma.Config.put([:instance, :limit_to_local_content], :all)
-       %{id: id} = insert(:user, %{name: "lain"})
-       insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-       insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-       assert [%{id: ^id}] = User.search("lain")
-       Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
-     end
-     test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do
-       Pleroma.Config.put([:instance, :limit_to_local_content], false)
-       u1 = insert(:user, %{name: "lain"})
-       u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-       u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-       results =
-         "lain"
-         |> User.search()
-         |> Enum.map(& &1.id)
-         |> Enum.sort()
-       assert [u1.id, u2.id, u3.id] == results
-       Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
-     end
-     test "finds a user whose name is nil" do
-       _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
-       user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
-       assert user_two ==
-                User.search("lain@pleroma.soykaf.com")
-                |> List.first()
-                |> Map.put(:search_rank, nil)
-                |> Map.put(:search_type, nil)
-     end
-     test "does not yield false-positive matches" do
-       insert(:user, %{name: "John Doe"})
-       Enum.each(["mary", "a", ""], fn query ->
-         assert [] == User.search(query)
-       end)
-     end
-     test "works with URIs" do
-       user = insert(:user)
-       [result] =
-         User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user)
-       user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
-       expected =
-         result
-         |> Map.put(:search_rank, nil)
-         |> Map.put(:search_type, nil)
-         |> Map.put(:last_digest_emailed_at, nil)
-       assert user == expected
-     end
-   end
    test "auth_active?/1 works correctly" do
      Pleroma.Config.put([:instance, :account_activation_required], true)
  
  
      assert Map.get(user_show, "followers_count") == 2
    end
 +
 +  describe "list_inactive_users_query/1" do
 +    defp days_ago(days) do
 +      NaiveDateTime.add(
 +        NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
 +        -days * 60 * 60 * 24,
 +        :second
 +      )
 +    end
 +
 +    test "Users are inactive by default" do
 +      total = 10
 +
 +      users =
 +        Enum.map(1..total, fn _ ->
 +          insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
 +        end)
 +
 +      inactive_users_ids =
 +        Pleroma.User.list_inactive_users_query()
 +        |> Pleroma.Repo.all()
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(users, fn user ->
 +        assert user.id in inactive_users_ids
 +      end)
 +    end
 +
 +    test "Only includes users who has no recent activity" do
 +      total = 10
 +
 +      users =
 +        Enum.map(1..total, fn _ ->
 +          insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
 +        end)
 +
 +      {inactive, active} = Enum.split(users, trunc(total / 2))
 +
 +      Enum.map(active, fn user ->
 +        to = Enum.random(users -- [user])
 +
 +        {:ok, _} =
 +          Pleroma.Web.TwitterAPI.TwitterAPI.create_status(user, %{
 +            "status" => "hey @#{to.nickname}"
 +          })
 +      end)
 +
 +      inactive_users_ids =
 +        Pleroma.User.list_inactive_users_query()
 +        |> Pleroma.Repo.all()
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(active, fn user ->
 +        refute user.id in inactive_users_ids
 +      end)
 +
 +      Enum.each(inactive, fn user ->
 +        assert user.id in inactive_users_ids
 +      end)
 +    end
 +
 +    test "Only includes users with no read notifications" do
 +      total = 10
 +
 +      users =
 +        Enum.map(1..total, fn _ ->
 +          insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
 +        end)
 +
 +      [sender | recipients] = users
 +      {inactive, active} = Enum.split(recipients, trunc(total / 2))
 +
 +      Enum.each(recipients, fn to ->
 +        {:ok, _} =
 +          Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{
 +            "status" => "hey @#{to.nickname}"
 +          })
 +
 +        {:ok, _} =
 +          Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{
 +            "status" => "hey again @#{to.nickname}"
 +          })
 +      end)
 +
 +      Enum.each(active, fn user ->
 +        [n1, _n2] = Pleroma.Notification.for_user(user)
 +        {:ok, _} = Pleroma.Notification.read_one(user, n1.id)
 +      end)
 +
 +      inactive_users_ids =
 +        Pleroma.User.list_inactive_users_query()
 +        |> Pleroma.Repo.all()
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(active, fn user ->
 +        refute user.id in inactive_users_ids
 +      end)
 +
 +      Enum.each(inactive, fn user ->
 +        assert user.id in inactive_users_ids
 +      end)
 +    end
 +  end
  
    describe "toggle_confirmation/1" do
      test "if user is confirmed" do
        assert user_two.ap_id in ap_ids
      end
    end
+   describe "sync followers count" do
+     setup do
+       user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed")
+       user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2")
+       insert(:user, local: true)
+       insert(:user, local: false, info: %{deactivated: true})
+       {:ok, user1: user1, user2: user2}
+     end
+     test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do
+       [fdb_user1] = User.external_users(limit: 1)
+       assert fdb_user1.ap_id
+       assert fdb_user1.ap_id == user1.ap_id
+       assert fdb_user1.id == user1.id
+       [fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1)
+       assert fdb_user2.ap_id
+       assert fdb_user2.ap_id == user2.ap_id
+       assert fdb_user2.id == user2.id
+       assert User.external_users(max_id: fdb_user2.id, limit: 1) == []
+     end
+     test "sync_follow_counters/1", %{user1: user1, user2: user2} do
+       {:ok, _pid} = Agent.start_link(fn -> %{} end, name: :domain_errors)
+       :ok = User.sync_follow_counters()
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user1)
+       assert followers == 437
+       assert following == 152
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user2)
+       assert followers == 527
+       assert following == 267
+       Agent.stop(:domain_errors)
+     end
+     test "sync_follow_counters/1 in separate batches", %{user1: user1, user2: user2} do
+       {:ok, _pid} = Agent.start_link(fn -> %{} end, name: :domain_errors)
+       :ok = User.sync_follow_counters(limit: 1)
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user1)
+       assert followers == 437
+       assert following == 152
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user2)
+       assert followers == 527
+       assert following == 267
+       Agent.stop(:domain_errors)
+     end
+     test "perform/1 with :sync_follow_counters", %{user1: user1, user2: user2} do
+       :ok = User.perform(:sync_follow_counters)
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user1)
+       assert followers == 437
+       assert following == 152
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user2)
+       assert followers == 527
+       assert following == 267
+     end
+   end
+   describe "set_info_cache/2" do
+     setup do
+       user = insert(:user)
+       {:ok, user: user}
+     end
+     test "update from args", %{user: user} do
+       User.set_info_cache(user, %{following_count: 15, follower_count: 18})
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
+       assert followers == 18
+       assert following == 15
+     end
+     test "without args", %{user: user} do
+       User.set_info_cache(user, %{})
+       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
+       assert followers == 0
+       assert following == 0
+     end
+   end
+   describe "user_info/2" do
+     setup do
+       user = insert(:user)
+       {:ok, user: user}
+     end
+     test "update from args", %{user: user} do
+       %{follower_count: followers, following_count: following} =
+         User.user_info(user, %{following_count: 15, follower_count: 18})
+       assert followers == 18
+       assert following == 15
+     end
+     test "without args", %{user: user} do
+       %{follower_count: followers, following_count: following} = User.user_info(user)
+       assert followers == 0
+       assert following == 0
+     end
+   end
  end