Skip to main content

lib/jido/chat/telegram/ex_gram_adapter.ex

defmodule Jido.Chat.Telegram.ExGramAdapter do
  @moduledoc """
  ExGram HTTP adapter backed by `Req`.
  """

  @behaviour ExGram.Adapter

  @base_url "https://api.telegram.org"

  @impl ExGram.Adapter
  def request(verb, path, body, opts \\ []) do
    _opts = opts

    [method: coerce_verb(verb), url: path]
    |> Req.Request.new()
    |> Req.Request.register_options([:base_url, :json, :form_multipart])
    |> Req.Request.put_new_option(:base_url, ExGram.Config.get(:ex_gram, :base_url, @base_url))
    |> put_body_option(body)
    |> Req.Steps.put_base_url()
    |> Req.Request.append_request_steps(custom_encode: &custom_encode/1)
    |> Req.Request.append_response_steps(custom_decode: &custom_decode/1)
    |> Req.Request.run_request()
    |> handle_result()
  end

  defp coerce_verb(:get), do: :post
  defp coerce_verb(verb), do: verb

  defp req_parts({:file, name, path}, parts), do: parts ++ [{name, File.stream!(path, 2048)}]

  defp req_parts({:file_content, name, content, filename}, parts),
    do: parts ++ [{name, {content, filename: filename}}]

  defp req_parts({name, value}, parts), do: parts ++ [{name, value}]

  defp put_body_option(req, {:multipart, parts}) do
    parts = Enum.reduce(parts, [], fn part, acc -> req_parts(part, acc) end)
    Req.Request.put_new_option(req, :form_multipart, parts)
  end

  defp put_body_option(req, body) when is_map(body),
    do: Req.Request.put_new_option(req, :json, body)

  defp custom_encode(request) do
    cond do
      data = request.options[:form_multipart] ->
        multipart = Req.Utils.encode_form_multipart(data)

        %{request | body: multipart.body}
        |> Req.Request.put_new_header("content-type", multipart.content_type)
        |> maybe_put_content_length(multipart.size)

      data = request.options[:json] ->
        %{request | body: ExGram.Adapter.encode(data)}
        |> Req.Request.put_new_header("content-type", "application/json")
        |> Req.Request.put_new_header("accept", "application/json")

      true ->
        request
    end
  end

  defp maybe_put_content_length(req, nil), do: req

  defp maybe_put_content_length(req, size),
    do: Req.Request.put_new_header(req, "content-length", Integer.to_string(size))

  defp custom_decode({request, response}) do
    case ExGram.Encoder.decode(response.body, keys: :atoms) do
      {:ok, decoded} -> {request, put_in(response.body, decoded)}
      {:error, error} -> {request, error}
    end
  end

  defp handle_result({_req, %Req.Response{status: status, body: %{ok: true, result: body}}})
       when status in 200..299,
       do: {:ok, body}

  defp handle_result({_req, %Req.Response{body: body}}),
    do: {:error, %ExGram.Error{code: :response_status_not_match, message: ExGram.Adapter.encode(body)}}

  defp handle_result({_req, exception}), do: {:error, %ExGram.Error{code: exception}}
end