if Code.ensure_loaded?(Tesla) do defmodule ExGram.Adapter.Tesla do @moduledoc """ HTTP adapter that uses [Tesla](https://hexdocs.pm/tesla) for Telegram Bot API requests. This adapter supports multiple HTTP clients (Hackney, Finch, Gun, Mint, Httpc, Ibrowse) and provides configurable middleware support. See `ExGram.Adapter` behaviour for more details. """ @behaviour ExGram.Adapter alias Tesla.Adapter.Hackney require Logger @base_url "https://api.telegram.org" def middlewares do [ {Tesla.Middleware.BaseUrl, ExGram.Config.get(:ex_gram, :base_url, @base_url)}, {Tesla.Middleware.Headers, [{"Content-Type", "application/json"}]}, {Tesla.Middleware.Logger, ExGram.Config.get(:ex_gram, Tesla.Middleware.Logger, level: :info)}, { Tesla.Middleware.JSON, decode: &__MODULE__.custom_decode/1, encode: &__MODULE__.custom_encode/1 } ] end def custom_encode(x), do: ExGram.Encoder.encode(x) def custom_decode(x), do: ExGram.Encoder.decode(x, keys: :atoms) @impl ExGram.Adapter def request(verb, path, body, _opts) do body = encode_body(body) verb |> do_request(path, body) |> handle_result() end defp new do Tesla.client(middlewares() ++ custom_middlewares(), http_adapter()) end defp do_request(:get, path, body) do do_request(:post, path, body) end defp do_request(:post, path, body) do Tesla.post(new(), path, body, opts: opts()) end defp handle_result({:ok, %{body: %{ok: true, result: body}, status: status}}) when status in 200..299 do {:ok, body} end defp handle_result({:ok, %{body: %{ok: false, description: description, error_code: error_code} = body}}) do parameters = maybe_error_parameters(body) error = %ExGram.Error{code: error_code, message: description, metadata: %{parameters: parameters}} {:error, error} end defp handle_result({:ok, %{body: body}}) do {:error, %ExGram.Error{code: :response_status_not_match, message: ExGram.Adapter.encode(body)}} end defp handle_result({:error, reason}) do {:error, %ExGram.Error{code: reason}} end defp maybe_error_parameters(%{parameters: parameters}) do case ExGram.Cast.cast(parameters, {:array, ExGram.Model.ResponseParameters}) do {:ok, parameters} -> parameters _ -> [] end end defp maybe_error_parameters(_), do: [] defp encode_body(body) when is_map(body) do Map.new(body, fn {key, value} -> {key, ExGram.Adapter.encode(value)} end) end defp encode_body({:multipart, parts}) do mp = Tesla.Multipart.add_content_type_param(Tesla.Multipart.new(), "charset=utf-8") Enum.reduce(parts, mp, &add_multipart_part/2) end defp add_multipart_part({:file, name, path}, mp) do Tesla.Multipart.add_file(mp, path, name: name) end defp add_multipart_part({:file_content, name, content, filename}, mp) do Tesla.Multipart.add_file_content(mp, content, filename, name: name) end defp add_multipart_part({name, value}, mp) do Tesla.Multipart.add_field(mp, name, value) end defp http_adapter, do: Application.get_env(:tesla, :adapter) || Hackney defp opts, do: [adapter: adapter_opts()] defp adapter_opts do adapter_module = case http_adapter() do module when is_atom(module) -> module {module, _opts} -> module _ -> nil end adapter_opts(adapter_module) end defp adapter_opts(Hackney) do [connect_timeout: 20_000, timeout: 60_000, recv_timeout: 60_000] end defp adapter_opts(Tesla.Adapter.Finch) do [pool_timeout: 20_000, receive_timeout: 60_000] end defp adapter_opts(Tesla.Adapter.Gun) do [connect_timeout: 20_000, timeout: 60_000] end defp adapter_opts(Tesla.Adapter.Mint) do [timeout: 60_000] end defp adapter_opts(Tesla.Adapter.Httpc) do [connect_timeout: 20_000, timeout: 60_000] end defp adapter_opts(Tesla.Adapter.Ibrowse) do [connect_timeout: 20_000, timeout: 60_000] end defp adapter_opts(_), do: [] defp format_middleware({m, f, a}) do case apply(m, f, a) do {_, _} = middleware -> {:ok, middleware} _ -> :error end end defp format_middleware({_, _} = mf), do: {:ok, mf} defp format_middleware(_), do: :error defp custom_middlewares do middlewares = Application.get_env(:ex_gram, __MODULE__, [])[:middlewares] || [] middlewares |> Enum.reduce([], fn elem, acc -> case format_middleware(elem) do {:ok, middleware} -> [middleware | acc] :error -> Logger.warning("Discarded, element is not a middleware: #{inspect(elem)}") acc end end) |> Enum.reverse() end end end