Current section

Files

Jump to
ex_gram lib ex_gram adapter tesla.ex
Raw

lib/ex_gram/adapter/tesla.ex

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