Merge remote-tracking branch 'remotes/origin/develop' into 2168-media-preview-proxy
authorIvan Tashkinov <ivantashkinov@gmail.com>
Thu, 2 Jul 2020 13:36:54 +0000 (16:36 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Thu, 2 Jul 2020 13:36:54 +0000 (16:36 +0300)
# Conflicts:
# config/config.exs
# lib/pleroma/web/media_proxy/media_proxy.ex
# lib/pleroma/web/media_proxy/media_proxy_controller.ex

1  2 
.gitlab-ci.yml
config/config.exs
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/media_proxy/media_proxy.ex
lib/pleroma/web/media_proxy/media_proxy_controller.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock

diff --combined .gitlab-ci.yml
index e596aa0ec29c94c13080a8dfbda6e6498c43abdc,b4bd59b43a32ddad040b3df83d746902e2c6060c..5c12647a00f2b6afce42f9a5f0456f7fd962637a
@@@ -1,4 -1,4 +1,4 @@@
- image: elixir:1.8.1
+ image: elixir:1.9.4
  
  variables: &global_variables
    POSTGRES_DB: pleroma_test
@@@ -6,8 -6,6 +6,8 @@@
    POSTGRES_PASSWORD: postgres
    DB_HOST: postgres
    MIX_ENV: test
 +  SHELL: /bin/sh
 +  USER: root
  
  cache: &global_cache_policy
    key: ${CI_COMMIT_REF_SLUG}
@@@ -172,8 -170,7 +172,7 @@@ stop_review_app
  
  amd64:
    stage: release
-   # TODO: Replace with upstream image when 1.9.0 comes out
-   image: rinpatch/elixir:1.9.0-rc.0
+   image: elixir:1.10.3
    only: &release-only
    - stable@pleroma/pleroma
    - develop@pleroma/pleroma
@@@ -210,8 -207,7 +209,7 @@@ amd64-musl
    stage: release
    artifacts: *release-artifacts
    only: *release-only
-   # TODO: Replace with upstream image when 1.9.0 comes out
-   image: rinpatch/elixir:1.9.0-rc.0-alpine
+   image: elixir:1.10.3-alpine 
    cache: *release-cache
    variables: *release-variables
    before_script: &before-release-musl
@@@ -227,8 -223,7 +225,7 @@@ arm
    only: *release-only
    tags:
      - arm32
-   # TODO: Replace with upstream image when 1.9.0 comes out
-   image: rinpatch/elixir:1.9.0-rc.0-arm
+   image: elixir:1.10.3
    cache: *release-cache
    variables: *release-variables
    before_script: *before-release
@@@ -240,8 -235,7 +237,7 @@@ arm-musl
    only: *release-only
    tags:
      - arm32
-   # TODO: Replace with upstream image when 1.9.0 comes out
-   image: rinpatch/elixir:1.9.0-rc.0-arm-alpine
+   image: elixir:1.10.3-alpine
    cache: *release-cache
    variables: *release-variables
    before_script: *before-release-musl
@@@ -253,8 -247,7 +249,7 @@@ arm64
    only: *release-only
    tags:
      - arm
-   # TODO: Replace with upstream image when 1.9.0 comes out
-   image: rinpatch/elixir:1.9.0-rc.0-arm64
+   image: elixir:1.10.3
    cache: *release-cache
    variables: *release-variables
    before_script: *before-release
@@@ -267,7 -260,7 +262,7 @@@ arm64-musl
    tags:
      - arm
    # TODO: Replace with upstream image when 1.9.0 comes out
-   image: rinpatch/elixir:1.9.0-rc.0-arm64-alpine
+   image: elixir:1.10.3-alpine
    cache: *release-cache
    variables: *release-variables
    before_script: *before-release-musl
diff --combined config/config.exs
index d1440b7bfbd32171b88a8951a6d2c6de82ff602c,9b550920cb144e469eda74394f67079e133cce37..c8b6c7fad0ffe581ccaed93cb770f99e9bc59904
@@@ -71,7 -71,8 +71,8 @@@ config :pleroma, Pleroma.Upload
        follow_redirect: true,
        pool: :upload
      ]
-   ]
+   ],
+   filename_display_max_length: 30
  
  config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
  
@@@ -170,7 -171,8 +171,8 @@@ config :mime, :types, %
    "application/ld+json" => ["activity+json"]
  }
  
- config :tesla, adapter: Tesla.Adapter.Gun
+ config :tesla, adapter: Tesla.Adapter.Hackney
  # Configures http settings, upstream proxy etc.
  config :pleroma, :http,
    proxy_url: nil,
@@@ -182,8 -184,9 +184,9 @@@ config :pleroma, :instance
    name: "Pleroma",
    email: "example@example.com",
    notify_email: "noreply@example.com",
-   description: "A Pleroma instance, an alternative fediverse server",
+   description: "Pleroma: An efficient and flexible fediverse server",
    background_image: "/images/city.jpg",
+   instance_thumbnail: "/instance/thumbnail.jpeg",
    limit: 5_000,
    chat_limit: 5_000,
    remote_limit: 100_000,
      Pleroma.Web.ActivityPub.Publisher
    ],
    allow_relay: true,
-   rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
    public: true,
    quarantined_instances: [],
    managed_config: true,
      "text/markdown",
      "text/bbcode"
    ],
-   mrf_transparency: true,
-   mrf_transparency_exclusions: [],
    autofollowed_nicknames: [],
    max_pinned_statuses: 1,
    attachment_links: false,
@@@ -272,20 -272,33 +272,33 @@@ config :pleroma, :markup
  
  config :pleroma, :frontend_configurations,
    pleroma_fe: %{
-     theme: "pleroma-dark",
-     logo: "/static/logo.png",
+     alwaysShowSubjectInput: true,
      background: "/images/city.jpg",
-     redirectRootNoLogin: "/main/all",
-     redirectRootLogin: "/main/friends",
-     showInstanceSpecificPanel: true,
-     scopeOptionsEnabled: false,
-     formattingOptionsEnabled: false,
      collapseMessageWithSubject: false,
+     disableChat: false,
+     greentext: false,
+     hideFilteredStatuses: false,
+     hideMutedPosts: false,
      hidePostStats: false,
+     hideSitename: false,
      hideUserStats: false,
+     loginMethod: "password",
+     logo: "/static/logo.png",
+     logoMargin: ".1em",
+     logoMask: true,
+     minimalScopesMode: false,
+     noAttachmentLinks: false,
+     nsfwCensorImage: "",
+     postContentType: "text/plain",
+     redirectRootLogin: "/main/friends",
+     redirectRootNoLogin: "/main/all",
      scopeCopy: true,
+     sidebarRight: false,
+     showFeaturesPanel: true,
+     showInstanceSpecificPanel: false,
      subjectLineBehavior: "email",
-     alwaysShowSubjectInput: true
+     theme: "pleroma-dark",
+     webPushNotifications: false
    },
    masto_fe: %{
      showInstanceSpecificPanel: true
@@@ -356,6 -369,8 +369,8 @@@ config :pleroma, :mrf_keyword
  
  config :pleroma, :mrf_subchain, match_actor: %{}
  
+ config :pleroma, :mrf_activity_expiration, days: 365
  config :pleroma, :mrf_vocabulary,
    accept: [],
    reject: []
@@@ -370,7 -385,6 +385,6 @@@ config :pleroma, :rich_media
    ignore_tld: ["local", "localdomain", "lan"],
    parsers: [
      Pleroma.Web.RichMedia.Parsers.TwitterCard,
-     Pleroma.Web.RichMedia.Parsers.OGP,
      Pleroma.Web.RichMedia.Parsers.OEmbed
    ],
    ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
@@@ -384,8 -398,6 +398,8 @@@ config :pleroma, :media_proxy
    proxy_opts: [
      redirect_on_failure: false,
      max_body_length: 25 * 1_048_576,
 +    # Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
 +    max_read_duration: 30_000,
      http: [
        follow_redirect: true,
        pool: :media
    ],
    whitelist: []
  
+ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
+   method: :purge,
+   headers: [],
+   options: []
+ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil
 +# Note: media preview proxy depends on media proxy to be enabled
 +config :pleroma, :media_preview_proxy,
 +  enabled: false,
 +  thumbnail_max_width: 400,
 +  thumbnail_max_height: 200,
 +  proxy_opts: [
 +    head_request_max_read_duration: 5_000,
 +    max_read_duration: 10_000
 +  ]
 +
  config :pleroma, :chat, enabled: true
  
  config :phoenix, :format_encoders, json: Jason
@@@ -425,6 -434,12 +446,12 @@@ config :pleroma, Pleroma.Web.Metadata
    ],
    unfurl_nsfw: false
  
+ config :pleroma, Pleroma.Web.Preload,
+   providers: [
+     Pleroma.Web.Preload.Providers.Instance,
+     Pleroma.Web.Preload.Providers.StatusNet
+   ]
  config :pleroma, :http_security,
    enabled: true,
    sts: false,
@@@ -681,10 -696,15 +708,19 @@@ config :pleroma, :restrict_unauthentica
  
  config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
  
+ config :pleroma, :mrf,
+   policies: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
+   transparency: true,
+   transparency_exclusions: []
+ config :tzdata, :http_client, Pleroma.HTTP.Tzdata
+ config :ex_aws, http_client: Pleroma.HTTP.ExAws
 +config :pleroma, :exexec,
 +  root_mode: false,
 +  options: %{}
 +
  # Import environment specific config. This must remain at the bottom
  # of this file so it overrides the configuration defined above.
  import_config "#{Mix.env()}.exs"
index e31e455531468225099eac9f4e73e70ad49702eb,6ee17f4dd05e3ad484db6c018500d2a96dc836d8..00d45bcd410afff54449318984e7d78789ba9ce6
@@@ -21,7 -21,7 +21,7 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
    alias Pleroma.Web.MastodonAPI.StatusView
    alias Pleroma.Web.MediaProxy
  
-   import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
+   import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
  
    # TODO: Add cached version.
    defp get_replied_to_activities([]), do: %{}
          expires_at: expires_at,
          direct_conversation_id: direct_conversation_id,
          thread_muted: thread_muted?,
-         emoji_reactions: emoji_reactions
+         emoji_reactions: emoji_reactions,
+         parent_visible: visible_for_user?(reply_to, opts[:for])
        }
      }
    end
      page_url_data = URI.parse(page_url)
  
      page_url_data =
-       if rich_media[:url] != nil do
-         URI.merge(page_url_data, URI.parse(rich_media[:url]))
+       if is_binary(rich_media["url"]) do
+         URI.merge(page_url_data, URI.parse(rich_media["url"]))
        else
          page_url_data
        end
      page_url = page_url_data |> to_string
  
      image_url =
-       if rich_media[:image] != nil do
-         URI.merge(page_url_data, URI.parse(rich_media[:image]))
+       if is_binary(rich_media["image"]) do
+         URI.merge(page_url_data, URI.parse(rich_media["image"]))
          |> to_string
-       else
-         nil
        end
  
      %{
        provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
        url: page_url,
        image: image_url |> MediaProxy.url(),
-       title: rich_media[:title] || "",
-       description: rich_media[:description] || "",
+       title: rich_media["title"] || "",
+       description: rich_media["description"] || "",
        pleroma: %{
          opengraph: rich_media
        }
      [attachment_url | _] = attachment["url"]
      media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
      href = attachment_url["href"] |> MediaProxy.url()
 +    href_preview = attachment_url["href"] |> MediaProxy.preview_url()
  
      type =
        cond do
        id: to_string(attachment["id"] || hash_id),
        url: href,
        remote_url: href,
 -      preview_url: href,
 +      preview_url: href_preview,
        text_url: href,
        type: type,
        description: attachment["name"],
index 4e01c14e4a765bf0047e2d649f89538413a7441e,077fabe47bf6fbab92ff175dfe44269dcf4e9de8..1b6242cb4d0fa9fad178216094956d9f088145a9
@@@ -6,39 -6,58 +6,72 @@@ defmodule Pleroma.Web.MediaProxy d
    alias Pleroma.Config
    alias Pleroma.Upload
    alias Pleroma.Web
+   alias Pleroma.Web.MediaProxy.Invalidation
  
    @base64_opts [padding: false]
  
+   @spec in_banned_urls(String.t()) :: boolean()
+   def in_banned_urls(url), do: elem(Cachex.exists?(:banned_urls_cache, url(url)), 1)
+   def remove_from_banned_urls(urls) when is_list(urls) do
+     Cachex.execute!(:banned_urls_cache, fn cache ->
+       Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1))
+     end)
+   end
+   def remove_from_banned_urls(url) when is_binary(url) do
+     Cachex.del(:banned_urls_cache, url(url))
+   end
+   def put_in_banned_urls(urls) when is_list(urls) do
+     Cachex.execute!(:banned_urls_cache, fn cache ->
+       Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true))
+     end)
+   end
+   def put_in_banned_urls(url) when is_binary(url) do
+     Cachex.put(:banned_urls_cache, url(url), true)
+   end
    def url(url) when is_nil(url) or url == "", do: nil
    def url("/" <> _ = url), do: url
  
    def url(url) do
-     if not enabled?() or local?(url) or whitelisted?(url) do
 -    if disabled?() or not url_proxiable?(url) do
++    if not enabled?() or not url_proxiable?(url) do
        url
      else
        encode_url(url)
      end
    end
  
 -  defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
+   @spec url_proxiable?(String.t()) :: boolean()
+   def url_proxiable?(url) do
+     if local?(url) or whitelisted?(url) do
+       false
+     else
+       true
+     end
+   end
 +  # Note: routing all URLs to preview handler (even local and whitelisted).
 +  #   Preview handler will call url/1 on decoded URLs, and applicable ones will detour media proxy.
 +  def preview_url(url) do
 +    if preview_enabled?() do
 +      encode_preview_url(url)
 +    else
 +      url
 +    end
 +  end
  
 -  defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
 +  def enabled?, do: Config.get([:media_proxy, :enabled], false)
  
 -  defp whitelisted?(url) do
 +  # Note: media proxy must be enabled for media preview proxy in order to load all
 +  #   non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
 +  def preview_enabled?, do: enabled?() and Config.get([:media_preview_proxy, :enabled], false)
 +
 +  def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
 +
 +  def whitelisted?(url) do
      %{host: domain} = URI.parse(url)
  
      mediaproxy_whitelist = Config.get([:media_proxy, :whitelist])
      end)
    end
  
 -  def encode_url(url) do
 +  defp base64_sig64(url) do
      base64 = Base.url_encode64(url, @base64_opts)
  
      sig64 =
        base64
 -      |> signed_url
 +      |> signed_url()
        |> Base.url_encode64(@base64_opts)
  
 +    {base64, sig64}
 +  end
 +
 +  def encode_url(url) do
 +    {base64, sig64} = base64_sig64(url)
 +
      build_url(sig64, base64, filename(url))
    end
  
 +  def encode_preview_url(url) do
 +    {base64, sig64} = base64_sig64(url)
 +
 +    build_preview_url(sig64, base64, filename(url))
 +  end
 +
    def decode_url(sig, url) do
      with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
           signature when signature == sig <- signed_url(url) do
      if path = URI.parse(url_or_path).path, do: Path.basename(path)
    end
  
 -  def build_url(sig_base64, url_base64, filename \\ nil) do
 +  defp proxy_url(path, sig_base64, url_base64, filename) do
      [
        Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),
 -      "proxy",
 +      path,
        sig_base64,
        url_base64,
        filename
      |> Enum.filter(& &1)
      |> Path.join()
    end
 +
 +  def build_url(sig_base64, url_base64, filename \\ nil) do
 +    proxy_url("proxy", sig_base64, url_base64, filename)
 +  end
 +
 +  def build_preview_url(sig_base64, url_base64, filename \\ nil) do
 +    proxy_url("proxy/preview", sig_base64, url_base64, filename)
 +  end
 +
 +  def verify_request_path_and_url(
 +        %Plug.Conn{params: %{"filename" => _}, request_path: request_path},
 +        url
 +      ) do
 +    verify_request_path_and_url(request_path, url)
 +  end
 +
 +  def verify_request_path_and_url(request_path, url) when is_binary(request_path) do
 +    filename = filename(url)
 +
 +    if filename && not basename_matches?(request_path, filename) do
 +      {:wrong_filename, filename}
 +    else
 +      :ok
 +    end
 +  end
 +
 +  def verify_request_path_and_url(_, _), do: :ok
 +
 +  defp basename_matches?(path, filename) do
 +    basename = Path.basename(path)
 +    basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename
 +  end
  end
index 12d4401faf2eac7734f276fa6d7a69337bc9dc7a,9a64b0ef35776f9b2a3c189481fed15c30ae698a..0f4575e2f20d0782ade5a3183074ab57d491efd5
@@@ -5,21 -5,22 +5,25 @@@
  defmodule Pleroma.Web.MediaProxy.MediaProxyController do
    use Pleroma.Web, :controller
  
 +  alias Pleroma.Config
 +  alias Pleroma.Helpers.MediaHelper
    alias Pleroma.ReverseProxy
    alias Pleroma.Web.MediaProxy
  
 -  @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
 -
 -  def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
 -    with config <- Pleroma.Config.get([:media_proxy], []),
 -         true <- Keyword.get(config, :enabled, false),
 -         {:ok, url} <- MediaProxy.decode_url(sig64, url64),
 +  def remote(conn, %{"sig" => sig64, "url" => url64}) do
 +    with {_, true} <- {:enabled, MediaProxy.enabled?()},
+          {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
 -         :ok <- filename_matches(params, conn.request_path, url) do
 -      ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
 +         {:ok, url} <- MediaProxy.decode_url(sig64, url64),
 +         :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
 +      proxy_opts = Config.get([:media_proxy, :proxy_opts], [])
 +      ReverseProxy.call(conn, url, proxy_opts)
      else
 -      error when error in [false, {:in_banned_urls, true}] ->
 +      {:enabled, false} ->
 +        send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
 +
++      {:in_banned_urls, true} ->
+         send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
        {:error, :invalid_signature} ->
          send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
  
      end
    end
  
 -  def filename_matches(%{"filename" => _} = _, path, url) do
 -    filename = MediaProxy.filename(url)
 +  def preview(conn, %{"sig" => sig64, "url" => url64}) do
 +    with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
 +         {:ok, url} <- MediaProxy.decode_url(sig64, url64),
 +         :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
 +      handle_preview(conn, url)
 +    else
 +      {:enabled, false} ->
 +        send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
 +
 +      {:error, :invalid_signature} ->
 +        send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
 +
 +      {:wrong_filename, filename} ->
 +        redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
 +    end
 +  end
  
 -    if filename && does_not_match(path, filename) do
 -      {:wrong_filename, filename}
 +  defp handle_preview(conn, url) do
 +    with {:ok, %{status: status} = head_response} when status in 200..299 <-
 +           Tesla.head(url, opts: [adapter: [timeout: preview_head_request_timeout()]]) do
 +      content_type = Tesla.get_header(head_response, "content-type")
 +      handle_preview(content_type, conn, url)
      else
 -      :ok
 +      {_, %{status: status}} ->
 +        send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
 +
 +      {:error, :recv_response_timeout} ->
 +        send_resp(conn, :failed_dependency, "HEAD request timeout.")
 +
 +      _ ->
 +        send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.")
 +    end
 +  end
 +
 +  defp thumbnail_max_dimensions(params) do
 +    config = Config.get([:media_preview_proxy], [])
 +
 +    thumbnail_max_width =
 +      if w = params["thumbnail_max_width"] do
 +        String.to_integer(w)
 +      else
 +        Keyword.fetch!(config, :thumbnail_max_width)
 +      end
 +
 +    thumbnail_max_height =
 +      if h = params["thumbnail_max_height"] do
 +        String.to_integer(h)
 +      else
 +        Keyword.fetch!(config, :thumbnail_max_height)
 +      end
 +
 +    {thumbnail_max_width, thumbnail_max_height}
 +  end
 +
 +  defp handle_preview("image/" <> _ = _content_type, %{params: params} = conn, url) do
 +    with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params),
 +         media_proxy_url <- MediaProxy.url(url),
 +         {:ok, thumbnail_binary} <-
 +           MediaHelper.ffmpeg_resize_remote(
 +             media_proxy_url,
 +             %{max_width: thumbnail_max_width, max_height: thumbnail_max_height}
 +           ) do
 +      conn
 +      |> put_resp_header("content-type", "image/jpeg")
 +      |> send_resp(200, thumbnail_binary)
 +    else
 +      _ ->
 +        send_resp(conn, :failed_dependency, "Can't handle image preview.")
      end
    end
  
 -  def filename_matches(_, _, _), do: :ok
 +  defp handle_preview(content_type, conn, _url) do
 +    send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.")
 +  end
 +
 +  defp preview_head_request_timeout do
 +    Config.get([:media_preview_proxy, :proxy_opts, :head_request_max_read_duration]) ||
 +      preview_timeout()
 +  end
  
 -  defp does_not_match(path, filename) do
 -    basename = Path.basename(path)
 -    basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
 +  defp preview_timeout do
 +    Config.get([:media_preview_proxy, :proxy_opts, :max_read_duration]) ||
 +      Config.get([:media_proxy, :proxy_opts, :max_read_duration]) ||
 +      ReverseProxy.max_read_duration_default()
    end
  end
index c0599a39c1fdb54d013254fcfcee24dc36439be4,9e457848e5a574abf0193f06ac99525e5162e660..94f77378b29f3f513857f507cb518d1be6ec3250
@@@ -160,14 -160,14 +160,14 @@@ defmodule Pleroma.Web.Router d
        :right_delete_multiple
      )
  
-     get("/relay", AdminAPIController, :relay_list)
-     post("/relay", AdminAPIController, :relay_follow)
-     delete("/relay", AdminAPIController, :relay_unfollow)
+     get("/relay", RelayController, :index)
+     post("/relay", RelayController, :follow)
+     delete("/relay", RelayController, :unfollow)
  
-     post("/users/invite_token", AdminAPIController, :create_invite_token)
-     get("/users/invites", AdminAPIController, :invites)
-     post("/users/revoke_invite", AdminAPIController, :revoke_invite)
-     post("/users/email_invite", AdminAPIController, :email_invite)
+     post("/users/invite_token", InviteController, :create)
+     get("/users/invites", InviteController, :index)
+     post("/users/revoke_invite", InviteController, :revoke)
+     post("/users/email_invite", InviteController, :email)
  
      get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
      patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
      patch("/users/confirm_email", AdminAPIController, :confirm_email)
      patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
  
-     get("/reports", AdminAPIController, :list_reports)
-     get("/reports/:id", AdminAPIController, :report_show)
-     patch("/reports", AdminAPIController, :reports_update)
-     post("/reports/:id/notes", AdminAPIController, :report_notes_create)
-     delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
+     get("/reports", ReportController, :index)
+     get("/reports/:id", ReportController, :show)
+     patch("/reports", ReportController, :update)
+     post("/reports/:id/notes", ReportController, :notes_create)
+     delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
  
-     get("/statuses/:id", AdminAPIController, :status_show)
-     put("/statuses/:id", AdminAPIController, :status_update)
-     delete("/statuses/:id", AdminAPIController, :status_delete)
-     get("/statuses", AdminAPIController, :list_statuses)
+     get("/statuses/:id", StatusController, :show)
+     put("/statuses/:id", StatusController, :update)
+     delete("/statuses/:id", StatusController, :delete)
+     get("/statuses", StatusController, :index)
  
-     get("/config", AdminAPIController, :config_show)
-     post("/config", AdminAPIController, :config_update)
-     get("/config/descriptions", AdminAPIController, :config_descriptions)
+     get("/config", ConfigController, :show)
+     post("/config", ConfigController, :update)
+     get("/config/descriptions", ConfigController, :descriptions)
      get("/need_reboot", AdminAPIController, :need_reboot)
      get("/restart", AdminAPIController, :restart)
  
      post("/reload_emoji", AdminAPIController, :reload_emoji)
      get("/stats", AdminAPIController, :stats)
  
-     get("/oauth_app", AdminAPIController, :oauth_app_list)
-     post("/oauth_app", AdminAPIController, :oauth_app_create)
-     patch("/oauth_app/:id", AdminAPIController, :oauth_app_update)
-     delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)
+     get("/oauth_app", OAuthAppController, :index)
+     post("/oauth_app", OAuthAppController, :create)
+     patch("/oauth_app/:id", OAuthAppController, :update)
+     delete("/oauth_app/:id", OAuthAppController, :delete)
+     get("/media_proxy_caches", MediaProxyCacheController, :index)
+     post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
+     post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
    end
  
    scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
    scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
      pipe_through(:api)
  
-     get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by)
-     get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by)
+     get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
+     get("/statuses/:id/reactions", EmojiReactionController, :index)
    end
  
    scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
      scope [] do
        pipe_through(:authenticated_api)
  
-       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
-       get("/conversations/:id", PleromaAPIController, :conversation)
-       post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
-     end
+       post("/chats/by-account-id/:id", ChatController, :create)
+       get("/chats", ChatController, :index)
+       get("/chats/:id", ChatController, :show)
+       get("/chats/:id/messages", ChatController, :messages)
+       post("/chats/:id/messages", ChatController, :post_chat_message)
+       delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
+       post("/chats/:id/read", ChatController, :mark_as_read)
+       post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
  
-     scope [] do
-       pipe_through(:authenticated_api)
+       get("/conversations/:id/statuses", ConversationController, :statuses)
+       get("/conversations/:id", ConversationController, :show)
+       post("/conversations/read", ConversationController, :mark_as_read)
+       patch("/conversations/:id", ConversationController, :update)
  
-       patch("/conversations/:id", PleromaAPIController, :update_conversation)
-       put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
-       delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
-       post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
+       put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
+       delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
+       post("/notifications/read", NotificationController, :mark_as_read)
  
        patch("/accounts/update_avatar", AccountController, :update_avatar)
        patch("/accounts/update_banner", AccountController, :update_banner)
    scope "/api/web", Pleroma.Web do
      pipe_through(:authenticated_api)
  
+     # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere
      put("/settings", MastoFEController, :put_settings)
    end
  
      get("/notice/:id", OStatus.OStatusController, :notice)
      get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
  
+     # Mastodon compatibility routes
+     get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
+     get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
      get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
      get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
  
      get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
    end
  
-   scope "/", Pleroma.Web.ActivityPub do
-     # XXX: not really ostatus
-     pipe_through(:ostatus)
-     get("/users/:nickname/outbox", ActivityPubController, :outbox)
-   end
    pipeline :ap_service_actor do
      plug(:accepts, ["activity+json", "json"])
    end
      get("/api/ap/whoami", ActivityPubController, :whoami)
      get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
  
+     get("/users/:nickname/outbox", ActivityPubController, :outbox)
      post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
      post("/api/ap/upload_media", ActivityPubController, :upload_media)
  
      post("/auth/password", MastodonAPI.AuthController, :password_reset)
  
      get("/web/*path", MastoFEController, :index)
+     get("/embed/:id", EmbedController, :show)
    end
  
    scope "/proxy/", Pleroma.Web.MediaProxy do
 +    get("/preview/:sig/:url", MediaProxyController, :preview)
 +    get("/preview/:sig/:url/:filename", MediaProxyController, :preview)
      get("/:sig/:url", MediaProxyController, :remote)
      get("/:sig/:url/:filename", MediaProxyController, :remote)
    end
      get("/registration/:token", RedirectController, :registration_page)
      get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
      get("/api*path", RedirectController, :api_not_implemented)
-     get("/*path", RedirectController, :redirector)
+     get("/*path", RedirectController, :redirector_with_preload)
  
      options("/*path", RedirectController, :empty)
    end
diff --combined mix.exs
index 3215086ca953430721370031533c87d84c8438ed,e2ab53bdeee0f73d91f7676b8b085038714d6337..dc25741cf0c078b2a87c656b89f6c4515ae4cd81
+++ b/mix.exs
@@@ -5,7 -5,7 +5,7 @@@ defmodule Pleroma.Mixfile d
      [
        app: :pleroma,
        version: version("2.0.50"),
-       elixir: "~> 1.8",
+       elixir: "~> 1.9",
        elixirc_paths: elixirc_paths(Mix.env()),
        compilers: [:phoenix, :gettext] ++ Mix.compilers(),
        elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())],
    defp deps do
      [
        {:phoenix, "~> 1.4.8"},
-       {:tzdata, "~> 0.5.21"},
+       {:tzdata, "~> 1.0.3"},
        {:plug_cowboy, "~> 2.0"},
        {:phoenix_pubsub, "~> 1.1"},
        {:phoenix_ecto, "~> 4.0"},
        {:cors_plug, "~> 1.5"},
        {:ex_doc, "~> 0.21", only: :dev, runtime: false},
        {:web_push_encryption, "~> 0.2.1"},
-       {:swoosh, "~> 0.23.2"},
+       {:swoosh,
+        git: "https://github.com/swoosh/swoosh",
+        ref: "c96e0ca8a00d8f211ec1f042a4626b09f249caa5",
+        override: true},
        {:phoenix_swoosh, "~> 0.2"},
        {:gen_smtp, "~> 0.13"},
        {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
         ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
        {:mox, "~> 0.5", only: :test},
        {:restarter, path: "./restarter"},
 +      # Note: `runtime: true` for :exexec makes CI fail due to `root` user (see Pleroma.Exec)
 +      {:exexec, "~> 0.2", runtime: false},
        {:open_api_spex,
         git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git",
         ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"}
    defp version(version) do
      identifier_filter = ~r/[^0-9a-z\-]+/i
  
-     # Pre-release version, denoted from patch version with a hyphen
-     {tag, tag_err} =
-       System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
-     {describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
-     {commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
+     {_cmdgit, cmdgit_err} = System.cmd("sh", ["-c", "command -v git"])
  
      git_pre_release =
-       cond do
-         tag_err == 0 and describe_err == 0 ->
-           describe
-           |> String.trim()
-           |> String.replace(String.trim(tag), "")
-           |> String.trim_leading("-")
-           |> String.trim()
+       if cmdgit_err == 0 do
+         {tag, tag_err} =
+           System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
+         {describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
+         {commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
  
-         commit_hash_err == 0 ->
-           "0-g" <> String.trim(commit_hash)
+         # Pre-release version, denoted from patch version with a hyphen
+         cond do
+           tag_err == 0 and describe_err == 0 ->
+             describe
+             |> String.trim()
+             |> String.replace(String.trim(tag), "")
+             |> String.trim_leading("-")
+             |> String.trim()
  
-         true ->
-           ""
+           commit_hash_err == 0 ->
+             "0-g" <> String.trim(commit_hash)
+           true ->
+             nil
+         end
        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"]),
+       with 0 <- cmdgit_err,
+            {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
             branch_name <- String.trim(branch_name),
             branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
             true <-
  
          branch_name
        else
-         _ -> "stable"
+         _ -> ""
        end
  
      build_name =
diff --combined mix.lock
index 962f5d0f4fde2e05533d1561750a99aa88e1a664,4f2777fa721b0a9b98217d95a46533d3e40720cb..a1d0bf0d294e11182dec6ed900dcae5cc841b193
+++ b/mix.lock
@@@ -12,7 -12,7 +12,7 @@@
    "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "738d0e17a93c2ccfe4ddc707bdc8e672e9074c8569498483feb1c4530fb91b2b"},
    "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
    "castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"},
-   "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
+   "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
    "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
    "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
    "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
@@@ -30,9 -30,7 +30,9 @@@
    "ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
    "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
    "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"},
 +  "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
    "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
 +  "erlexec": {:hex, :erlexec, "1.10.9", "3cbb3476f942bfb8b68b85721c21c1835061cf6dd35f5285c2362e85b100ddc7", [:rebar3], [], "hexpm", "271e5b5f2d91cdb9887efe74d89026c199bfc69f074cade0d08dab60993fa14e"},
    "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
    "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
    "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@@@ -43,7 -41,6 +43,7 @@@
    "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"},
    "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
    "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"},
 +  "exexec": {:hex, :exexec, "0.2.0", "a6ffc48cba3ac9420891b847e4dc7120692fb8c08c9e82220ebddc0bb8d96103", [:mix], [{:erlexec, "~> 1.10", [hex: :erlexec, repo: "hexpm", optional: false]}], "hexpm", "312cd1c9befba9e078e57f3541e4f4257eabda6eb9c348154fe899d6ac633299"},
    "fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"},
    "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"},
    "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
    "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
    "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
    "gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]},
-   "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [: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.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
+   "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [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]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
    "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
    "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
    "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
    "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
-   "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
+   "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
    "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
    "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
    "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
@@@ -80,7 -77,6 +80,7 @@@
    "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
    "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
    "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]},
 +  "p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"},
    "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
    "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
    "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
    "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
    "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
    "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
-   "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
+   "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
    "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
-   "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [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", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
+   "swoosh": {:git, "https://github.com/swoosh/swoosh", "c96e0ca8a00d8f211ec1f042a4626b09f249caa5", [ref: "c96e0ca8a00d8f211ec1f042a4626b09f249caa5"]},
    "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
-   "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
+   "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
    "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]},
    "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
    "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
-   "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"},
+   "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
    "ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "db9fbfb5ac707bc4f85a297758406340bf0358b4af737a88113c1a9eee120ac7"},
-   "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
+   "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
    "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
    "web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [: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", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"},
    "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},